diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 455a8766..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -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" -} diff --git a/.docker/entrypoint.d/config.json.tmpl b/.docker/entrypoint.d/config.json.tmpl deleted file mode 100644 index d1088d6e..00000000 --- a/.docker/entrypoint.d/config.json.tmpl +++ /dev/null @@ -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"] - } - } - } - } -} diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh deleted file mode 100755 index 16ada386..00000000 --- a/.docker/entrypoint.sh +++ /dev/null @@ -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 "$@" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f157646a..5d471d6f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 48e0233e..2e8fd122 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db415684..6638b875 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b27a853..369765aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.gitignore b/.gitignore index 8f3636c2..4f3b64dc 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.htaccess b/.htaccess index 27fccda1..26850176 100644 --- a/.htaccess +++ b/.htaccess @@ -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] diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 9fd655e9..8a4d93aa 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -14,5 +14,4 @@ return $_phpcs_config->setRules([ 'array_syntax' => ['syntax' => 'short'], ]) ->setFinder($_phpcs_finder) - ->setCacheFile("data/php-cs-fixer.cache") -; +; \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f4d83d91..75508f59 100644 --- a/Dockerfile +++ b/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"] diff --git a/README.md b/README.md index d68483c8..3d37e9e5 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,10 @@ # Shimmie -[![Tests](https://github.com/shish/shimmie2/workflows/Tests/badge.svg?branch=main)](https://github.com/shish/shimmie2/actions) -[![Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=main) -[![Code Coverage](https://scrutinizer-ci.com/g/shish/shimmie2/badges/coverage.png?b=main)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=main) -[![Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#shimmie:matrix.org) +[![Test & Publish](https://github.com/shish/shimmie2/workflows/Test%20&%20Publish/badge.svg)](https://github.com/shish/shimmie2/actions) +[![Code Quality](https://scrutinizer-ci.com/g/shish/shimmie2/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/shish/shimmie2/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/shish/shimmie2/?branch=master) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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. diff --git a/composer.json b/composer.json index a525e48b..22e36479 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index b7c1cc65..2276eea2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,157 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cc4804abf9dc372f6dc18abd353bb0e2", + "content-hash": "e4d47676467ba492d3be2d43d3950b9d", "packages": [ - { - "name": "aws/aws-crt-php", - "version": "v1.2.4", - "source": { - "type": "git", - "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", - "yoast/phpunit-polyfills": "^1.0" - }, - "suggest": { - "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "AWS SDK Common Runtime Team", - "email": "aws-sdk-common-runtime@amazon.com" - } - ], - "description": "AWS Common Runtime for PHP", - "homepage": "https://github.com/awslabs/aws-crt-php", - "keywords": [ - "amazon", - "aws", - "crt", - "sdk" - ], - "support": { - "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4" - }, - "time": "2023-11-08T00:42:13+00:00" - }, - { - "name": "aws/aws-sdk-php", - "version": "3.296.5", - "source": { - "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "23b009f305278e227bc5149bcb8fc9c1503fb130" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/23b009f305278e227bc5149bcb8fc9c1503fb130", - "reference": "23b009f305278e227bc5149bcb8fc9c1503fb130", - "shasum": "" - }, - "require": { - "aws/aws-crt-php": "^1.2.3", - "ext-json": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", - "mtdowling/jmespath.php": "^2.6", - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0" - }, - "require-dev": { - "andrewsville/php-token-reflection": "^1.4", - "aws/aws-php-sns-message-validator": "~1.0", - "behat/behat": "~3.0", - "composer/composer": "^1.10.22", - "dms/phpunit-arraysubset-asserts": "^0.4.0", - "doctrine/cache": "~1.4", - "ext-dom": "*", - "ext-openssl": "*", - "ext-pcntl": "*", - "ext-sockets": "*", - "nette/neon": "^2.3", - "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", - "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3 || ^4.0", - "yoast/phpunit-polyfills": "^1.0" - }, - "suggest": { - "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", - "doctrine/cache": "To use the DoctrineCacheAdapter", - "ext-curl": "To send requests using cURL", - "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", - "ext-sockets": "To use client-side monitoring" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Aws\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" - } - ], - "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", - "homepage": "http://aws.amazon.com/sdkforphp", - "keywords": [ - "amazon", - "aws", - "cloud", - "dynamodb", - "ec2", - "glacier", - "s3", - "sdk" - ], - "support": { - "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", - "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.296.5" - }, - "time": "2024-01-18T19:06:27+00:00" - }, { "name": "bower-asset/jquery", "version": "1.12.4", @@ -199,7 +50,7 @@ "version": "v2.2.1", "source": { "type": "git", - "url": "git@github.com:js-cookie/js-cookie.git", + "url": "https://github.com/js-cookie/js-cookie.git", "reference": "54962f884e9ae33f93e13ac903ffaf1d5a523598" }, "dist": { @@ -260,20 +111,20 @@ }, { "name": "ezyang/htmlpurifier", - "version": "v4.17.0", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -315,9 +166,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" }, - "time": "2023-11-17T15:01:25+00:00" + "time": "2022-09-18T07:06:19+00:00" }, { "name": "flexihash/flexihash", @@ -429,333 +280,6 @@ }, "time": "2023-02-20T17:27:30+00:00" }, - { - "name": "guzzlehttp/guzzle", - "version": "7.9.x-dev", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", - "shasum": "" - }, - "require": { - "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", - "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "provide": { - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", - "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", - "psr/log": "^1.1 || ^2.0 || ^3.0" - }, - "suggest": { - "ext-curl": "Required for CURL handler support", - "ext-intl": "Required for Internationalized Domain Name (IDN) support", - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "psr-18", - "psr-7", - "rest", - "web service" - ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", - "type": "tidelift" - } - ], - "time": "2023-12-03T20:35:24+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "2.0.x-dev", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" - }, - "default-branch": true, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", - "type": "tidelift" - } - ], - "time": "2023-12-03T20:19:20+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "2.6.x-dev", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" - }, - "default-branch": true, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", - "type": "tidelift" - } - ], - "time": "2023-12-03T20:05:35+00:00" - }, { "name": "ifixit/php-akismet", "version": "1.1", @@ -766,73 +290,6 @@ }, "type": "library" }, - { - "name": "mtdowling/jmespath.php", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "b243cacd2a9803b4cbc259246aa5081208238c10" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/b243cacd2a9803b4cbc259246aa5081208238c10", - "reference": "b243cacd2a9803b4cbc259246aa5081208238c10", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "symfony/polyfill-mbstring": "^1.17" - }, - "require-dev": { - "composer/xdebug-handler": "^3.0.3", - "phpunit/phpunit": "^8.5.33" - }, - "default-branch": true, - "bin": [ - "bin/jp.php" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "files": [ - "src/JmesPath.php" - ], - "psr-4": { - "JmesPath\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Declaratively specify how to extract elements from a JSON document", - "keywords": [ - "json", - "jsonpath" - ], - "support": { - "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/master" - }, - "time": "2023-11-30T16:26:47+00:00" - }, { "name": "naroga/redis-cache", "version": "dev-master", @@ -887,12 +344,12 @@ "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "deee2b6d605eb6401446f6f6354414ab7571a5a0" + "reference": "bb8cce7bcf0d790dd17dde01922230d411efb99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/deee2b6d605eb6401446f6f6354414ab7571a5a0", - "reference": "deee2b6d605eb6401446f6f6354414ab7571a5a0", + "url": "https://api.github.com/repos/predis/predis/zipball/bb8cce7bcf0d790dd17dde01922230d411efb99b", + "reference": "bb8cce7bcf0d790dd17dde01922230d411efb99b", "shasum": "" }, "require": { @@ -945,224 +402,7 @@ "type": "github" } ], - "time": "2023-09-19T16:11:21+00:00" - }, - { - "name": "psr/container", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "707984727bd5b2b670e59559d3ed2500240cf875" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/707984727bd5b2b670e59559d3ed2500240cf875", - "reference": "707984727bd5b2b670e59559d3ed2500240cf875", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container" - }, - "time": "2023-09-22T11:11:30+00:00" - }, - { - "name": "psr/http-client", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client" - }, - "time": "2023-09-23T14:17:50+00:00" - }, - { - "name": "psr/http-factory", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/7037f4b0950474e9d1350e8df89b15f1842085f6", - "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6", - "shasum": "" - }, - "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2023-09-22T11:16:44+00:00" - }, - { - "name": "psr/http-message", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-01-10T16:48:39+00:00" }, { "name": "psr/simple-cache", @@ -1215,50 +455,6 @@ }, "time": "2017-10-23T01:57:42+00:00" }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, { "name": "sabre/cache", "version": "2.0.1", @@ -1376,26 +572,26 @@ }, { "name": "shish/ffsphp", - "version": "v1.3.2", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/shish/ffsphp.git", - "reference": "d69223f4317de302b6cd485d0a43709788dd6f69" + "reference": "47d7e96a129502275fb8a4ae0c4f36bd3f59e95b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shish/ffsphp/zipball/d69223f4317de302b6cd485d0a43709788dd6f69", - "reference": "d69223f4317de302b6cd485d0a43709788dd6f69", + "url": "https://api.github.com/repos/shish/ffsphp/zipball/47d7e96a129502275fb8a4ae0c4f36bd3f59e95b", + "reference": "47d7e96a129502275fb8a4ae0c4f36bd3f59e95b", "shasum": "" }, "require": { "ext-pdo": "*", - "php": "^8.1" + "php": "^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "3.41.1", - "phpstan/phpstan": "1.10.50", - "phpunit/phpunit": "10.5.3" + "friendsofphp/php-cs-fixer": "^3.12", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^9.0" }, "type": "library", "autoload": { @@ -1419,9 +615,9 @@ "homepage": "https://github.com/shish/ffsphp", "support": { "issues": "https://github.com/shish/ffsphp/issues", - "source": "https://github.com/shish/ffsphp/tree/v1.3.2" + "source": "https://github.com/shish/ffsphp/tree/v1.1.0" }, - "time": "2024-01-04T18:38:54+00:00" + "time": "2023-02-04T12:34:44+00:00" }, { "name": "shish/gqla", @@ -1476,77 +672,23 @@ }, "time": "2023-03-03T00:12:44+00:00" }, - { - "name": "shish/microbundler", - "version": "v1.0.1", - "source": { - "type": "git", - "url": "https://github.com/shish/microbundler.git", - "reference": "5f48327e92d3601f2c86caef2856b9671a63b6b0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/shish/microbundler/zipball/5f48327e92d3601f2c86caef2856b9671a63b6b0", - "reference": "5f48327e92d3601f2c86caef2856b9671a63b6b0", - "shasum": "" - }, - "require": { - "php": "^8.1", - "shish/ffsphp": "^1.3" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "3.41.1", - "phpstan/phpstan": "1.10.50", - "phpunit/phpunit": "10.5.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "MicroBundler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Shish", - "email": "webmaster@shishnet.org", - "homepage": "http://shishnet.org", - "role": "Developer" - } - ], - "description": "A minimal CSS / JS bundler", - "homepage": "https://github.com/shish/microbundler", - "keywords": [ - "JS", - "bundler", - "css" - ], - "support": { - "issues": "https://github.com/shish/microbundler/issues", - "source": "https://github.com/shish/microbundler/tree/v1.0.1" - }, - "time": "2024-01-01T19:58:28+00:00" - }, { "name": "shish/microcrud", - "version": "v2.2.2", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/shish/microcrud.git", - "reference": "e696f35b494b78bf1bfcddd47b4d542e98a468cb" + "reference": "c7398edf6b1ed0ee508769a73d656deca8f6a4be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shish/microcrud/zipball/e696f35b494b78bf1bfcddd47b4d542e98a468cb", - "reference": "e696f35b494b78bf1bfcddd47b4d542e98a468cb", + "url": "https://api.github.com/repos/shish/microcrud/zipball/c7398edf6b1ed0ee508769a73d656deca8f6a4be", + "reference": "c7398edf6b1ed0ee508769a73d656deca8f6a4be", "shasum": "" }, "require": { "ext-pdo": "*", - "php": "^8.1", + "php": "^8.0", "shish/ffsphp": "^1.0", "shish/microhtml": "^2.0.2" }, @@ -1581,22 +723,22 @@ ], "support": { "issues": "https://github.com/shish/microcrud/issues", - "source": "https://github.com/shish/microcrud/tree/v2.2.2" + "source": "https://github.com/shish/microcrud/tree/v2.1.0" }, - "time": "2024-01-19T17:35:05+00:00" + "time": "2023-02-04T13:09:53+00:00" }, { "name": "shish/microhtml", - "version": "v2.2.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/shish/microhtml.git", - "reference": "824d8541c7f0662e26d03d65d865d8f13ea57a72" + "reference": "afef3aac229b514ed5b4afbfb2d970d1419313c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shish/microhtml/zipball/824d8541c7f0662e26d03d65d865d8f13ea57a72", - "reference": "824d8541c7f0662e26d03d65d865d8f13ea57a72", + "url": "https://api.github.com/repos/shish/microhtml/zipball/afef3aac229b514ed5b4afbfb2d970d1419313c8", + "reference": "afef3aac229b514ed5b4afbfb2d970d1419313c8", "shasum": "" }, "require": { @@ -1633,687 +775,22 @@ ], "support": { "issues": "https://github.com/shish/microhtml/issues", - "source": "https://github.com/shish/microhtml/tree/v2.2.1" + "source": "https://github.com/shish/microhtml/tree/v2.1.0" }, - "time": "2023-08-17T16:39:06+00:00" - }, - { - "name": "symfony/console", - "version": "6.4.x-dev", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "f88dd6369c238e127aba7212252301945f47ee53" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f88dd6369c238e127aba7212252301945f47ee53", - "reference": "f88dd6369c238e127aba7212252301945f47ee53", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" - }, - "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/6.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-19T13:57:07+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "dev-main", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/2c438b99bb2753c1628c1e6f523991edea5b03a4", - "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/main" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-02T14:07:37+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "1.x-dev", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "1.x-dev", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "1.x-dev", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-26T09:26:14+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "1.x-dev", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-07-28T09:04:16+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "dev-main", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", - "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/main" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-02T14:07:37+00:00" - }, - { - "name": "symfony/string", - "version": "6.4.x-dev", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "1b4a76ca2c0bd2916edb36b2a0a62087a13c0358" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1b4a76ca2c0bd2916edb36b2a0a62087a13c0358", - "reference": "1b4a76ca2c0bd2916edb36b2a0a62087a13c0358", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" - }, - "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/6.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-19T13:57:07+00:00" + "time": "2023-02-04T13:02:26+00:00" }, { "name": "webonyx/graphql-php", - "version": "v15.8.1", + "version": "v15.4.0", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "575ac95f13adfb38219a748572355385c101fdf7" + "reference": "99290f7945a5b39ad823f7600fa196de62597e9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/575ac95f13adfb38219a748572355385c101fdf7", - "reference": "575ac95f13adfb38219a748572355385c101fdf7", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/99290f7945a5b39ad823f7600fa196de62597e9d", + "reference": "99290f7945a5b39ad823f7600fa196de62597e9d", "shasum": "" }, "require": { @@ -2324,23 +801,22 @@ "require-dev": { "amphp/amp": "^2.6", "amphp/http-server": "^2.1", - "dms/phpunit-arraysubset-asserts": "dev-master", + "dms/phpunit-arraysubset-asserts": "^0.4", "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "3.30.0", - "mll-lab/php-cs-fixer-config": "^5", + "mll-lab/php-cs-fixer-config": "^5.0", "nyholm/psr7": "^1.5", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "1.10.47", - "phpstan/phpstan-phpunit": "1.3.15", - "phpstan/phpstan-strict-rules": "1.5.2", - "phpunit/phpunit": "^9.5 || ^10", + "phpstan/phpstan": "1.10.15", + "phpstan/phpstan-phpunit": "1.3.11", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "^9.5", "psr/http-message": "^1 || ^2", "react/http": "^1.6", "react/promise": "^2.9", - "rector/rector": "^0.18", + "rector/rector": "^0.16.0", "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5 || ^6 || ^7", + "symfony/var-exporter": "^5 || ^6", "thecodingmachine/safe": "^1.3 || ^2" }, "suggest": { @@ -2366,7 +842,7 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v15.8.1" + "source": "https://github.com/webonyx/graphql-php/tree/v15.4.0" }, "funding": [ { @@ -2374,7 +850,7 @@ "type": "open_collective" } ], - "time": "2023-12-05T17:23:35+00:00" + "time": "2023-05-11T10:26:08+00:00" } ], "packages-dev": [ @@ -2384,12 +860,12 @@ "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", "shasum": "" }, "require": { @@ -2432,7 +908,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.1" + "source": "https://github.com/composer/pcre/tree/3.1.0" }, "funding": [ { @@ -2448,7 +924,7 @@ "type": "tidelift" } ], - "time": "2023-10-11T07:11:09+00:00" + "time": "2022-11-17T09:50:14+00:00" }, { "name": "composer/semver", @@ -2456,12 +932,12 @@ "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "1d09200268e7d1052ded8e5da9c73c96a63d18f5" + "reference": "fa1ec24f0ab1efe642671ec15c51a3ab879f59bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/1d09200268e7d1052ded8e5da9c73c96a63d18f5", - "reference": "1d09200268e7d1052ded8e5da9c73c96a63d18f5", + "url": "https://api.github.com/repos/composer/semver/zipball/fa1ec24f0ab1efe642671ec15c51a3ab879f59bf", + "reference": "fa1ec24f0ab1efe642671ec15c51a3ab879f59bf", "shasum": "" }, "require": { @@ -2530,7 +1006,7 @@ "type": "tidelift" } ], - "time": "2023-08-31T12:20:31+00:00" + "time": "2023-01-13T15:47:53+00:00" }, { "name": "composer/xdebug-handler", @@ -2604,12 +1080,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "94f40ad7ecbc6931958faa8a57c48dce5da2b468" + "reference": "4b68cf86b766ec429f4f68af648817cdfb360582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/94f40ad7ecbc6931958faa8a57c48dce5da2b468", - "reference": "94f40ad7ecbc6931958faa8a57c48dce5da2b468", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/4b68cf86b766ec429f4f68af648817cdfb360582", + "reference": "4b68cf86b766ec429f4f68af648817cdfb360582", "shasum": "" }, "require": { @@ -2673,36 +1149,78 @@ "issues": "https://github.com/doctrine/annotations/issues", "source": "https://github.com/doctrine/annotations/tree/2.0.x" }, - "time": "2023-08-23T17:36:07+00:00" + "time": "2023-03-27T17:43:32+00:00" }, { - "name": "doctrine/instantiator", - "version": "2.0.x-dev", + "name": "doctrine/deprecations", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "6c0ee619435c5d4f3bc515ab1514cf4cf1006c6e" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/6c0ee619435c5d4f3bc515ab1514cf4cf1006c6e", - "reference": "6c0ee619435c5d4f3bc515ab1514cf4cf1006c6e", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^7.1|^8.0" }, "require-dev": { - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "psr/log": "^1|^2|^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + }, + "time": "2022-05-02T15:47:09+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.5.x-dev", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.4" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2728,7 +1246,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.x" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -2744,32 +1262,34 @@ "type": "tidelift" } ], - "time": "2023-12-09T14:19:21+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "doctrine/lexer", - "version": "3.1.x-dev", + "version": "2.1.x-dev", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "0d54c073afb397d5896df60cc34170cf37dfad5e" + "reference": "e74756f7517d72c238b9163fcd1ed54eb1f92bd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/0d54c073afb397d5896df60cc34170cf37dfad5e", - "reference": "0d54c073afb397d5896df60cc34170cf37dfad5e", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e74756f7517d72c238b9163fcd1ed54eb1f92bd0", + "reference": "e74756f7517d72c238b9163fcd1ed54eb1f92bd0", "shasum": "" }, "require": { - "php": "^8.1" + "doctrine/deprecations": "^1.0", + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^9.5", + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^5.0" + "vimeo/psalm": "^4.11 || ^5.0" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2805,7 +1325,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/3.1.x" + "source": "https://github.com/doctrine/lexer/tree/2.1.x" }, "funding": [ { @@ -2821,52 +1341,57 @@ "type": "tidelift" } ], - "time": "2023-07-05T07:23:35+00:00" + "time": "2022-12-29T09:22:42+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.41.1", + "version": "v3.17.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "8b6ae8dcbaf23f09680643ab832a4a3a260265f6" + "reference": "3f0ed862f22386c55a767461ef5083bddceeed79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/8b6ae8dcbaf23f09680643ab832a4a3a260265f6", - "reference": "8b6ae8dcbaf23f09680643ab832a4a3a260265f6", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/3f0ed862f22386c55a767461ef5083bddceeed79", + "reference": "3f0ed862f22386c55a767461ef5083bddceeed79", "shasum": "" }, "require": { - "composer/semver": "^3.4", + "composer/semver": "^3.3", "composer/xdebug-handler": "^3.0.3", + "doctrine/annotations": "^2", + "doctrine/lexer": "^2 || ^3", "ext-json": "*", "ext-tokenizer": "*", "php": "^7.4 || ^8.0", "sebastian/diff": "^4.0 || ^5.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/finder": "^5.4 || ^6.0 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", - "symfony/polyfill-mbstring": "^1.28", - "symfony/polyfill-php80": "^1.28", - "symfony/polyfill-php81": "^1.28", - "symfony/process": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", + "symfony/polyfill-mbstring": "^1.27", + "symfony/polyfill-php80": "^1.27", + "symfony/polyfill-php81": "^1.27", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" }, "require-dev": { - "facile-it/paraunit": "^1.3 || ^2.0", "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^2.1", + "keradus/cli-executor": "^2.0", "mikey179/vfsstream": "^1.6.11", - "php-coveralls/php-coveralls": "^2.7", + "php-coveralls/php-coveralls": "^2.5.3", "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", - "phpunit/phpunit": "^9.6", - "symfony/phpunit-bridge": "^6.3.8 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy": "^1.16", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "phpunitgoodpractices/polyfill": "^1.6", + "phpunitgoodpractices/traits": "^1.9.2", + "symfony/phpunit-bridge": "^6.2.3", + "symfony/yaml": "^5.4 || ^6.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -2904,7 +1429,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.41.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.17.0" }, "funding": [ { @@ -2912,7 +1437,334 @@ "type": "github" } ], - "time": "2023-12-10T19:59:27+00:00" + "time": "2023-05-22T19:59:32+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.7.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "default-branch": true, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-05-21T14:04:53+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "4a94655427efd6906ed3eb628c79693291264713" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4a94655427efd6906ed3eb628c79693291264713", + "reference": "4a94655427efd6906ed3eb628c79693291264713", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "default-branch": true, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T19:15:14+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.x-dev", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-04-17T16:11:26+00:00" }, { "name": "jms/metadata", @@ -2920,19 +1772,19 @@ "source": { "type": "git", "url": "https://github.com/schmittjoh/metadata.git", - "reference": "1e72a1482cb6faa15915ba79086fa42a0ed2ec54" + "reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/1e72a1482cb6faa15915ba79086fa42a0ed2ec54", - "reference": "1e72a1482cb6faa15915ba79086fa42a0ed2ec54", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/7ca240dcac0c655eb15933ee55736ccd2ea0d7a6", + "reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6", "shasum": "" }, "require": { "php": "^7.2|^8.0" }, "require-dev": { - "doctrine/cache": "^1.0|^2.0", + "doctrine/cache": "^1.0", "doctrine/coding-standard": "^8.0", "mikey179/vfsstream": "^1.6.7", "phpunit/phpunit": "^8.5|^9.0", @@ -2975,9 +1827,9 @@ ], "support": { "issues": "https://github.com/schmittjoh/metadata/issues", - "source": "https://github.com/schmittjoh/metadata/tree/master" + "source": "https://github.com/schmittjoh/metadata/tree/2.8.0" }, - "time": "2023-08-04T08:12:29+00:00" + "time": "2023-02-15T13:44:18+00:00" }, { "name": "jms/serializer", @@ -2985,44 +1837,43 @@ "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "39096dd64dd6c66afc7eb7f294918243fd80d383" + "reference": "d5cc4674015e362370cbd370948e2ae03496a7cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/39096dd64dd6c66afc7eb7f294918243fd80d383", - "reference": "39096dd64dd6c66afc7eb7f294918243fd80d383", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/d5cc4674015e362370cbd370948e2ae03496a7cc", + "reference": "d5cc4674015e362370cbd370948e2ae03496a7cc", "shasum": "" }, "require": { - "doctrine/annotations": "^1.14 || ^2.0", - "doctrine/instantiator": "^1.3.1 || ^2.0", - "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/annotations": "^1.13 || ^2.0", + "doctrine/instantiator": "^1.0.3", + "doctrine/lexer": "^1.1 || ^2", "jms/metadata": "^2.6", - "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^1.20" + "php": "^7.2||^8.0", + "phpstan/phpdoc-parser": "^0.4 || ^0.5 || ^1.0" }, "require-dev": { - "doctrine/coding-standard": "^12.0", - "doctrine/orm": "^2.14 || ^3.0", - "doctrine/persistence": "^2.5.2 || ^3.0", - "doctrine/phpcr-odm": "^1.5.2 || ^2.0", + "doctrine/coding-standard": "^8.1", + "doctrine/orm": "~2.1", + "doctrine/persistence": "^1.3.3|^2.0|^3.0", + "doctrine/phpcr-odm": "^1.3|^2.0", "ext-pdo_sqlite": "*", - "jackalope/jackalope-doctrine-dbal": "^1.3", - "ocramius/proxy-manager": "^1.0 || ^2.0", + "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "ocramius/proxy-manager": "^1.0|^2.0", "phpbench/phpbench": "^1.0", "phpstan/phpstan": "^1.0.2", - "phpunit/phpunit": "^9.0 || ^10.0", - "psr/container": "^1.0 || ^2.0", - "rector/rector": "^0.18.13", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/form": "^5.4 || ^6.0 || ^7.0", - "symfony/translation": "^5.4 || ^6.0 || ^7.0", - "symfony/uid": "^5.4 || ^6.0 || ^7.0", - "symfony/validator": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0", - "twig/twig": "^1.34 || ^2.4 || ^3.0" + "phpunit/phpunit": "^8.5.21||^9.0||^10.0", + "psr/container": "^1.0|^2.0", + "symfony/dependency-injection": "^3.0|^4.0|^5.0|^6.0", + "symfony/expression-language": "^3.2|^4.0|^5.0|^6.0", + "symfony/filesystem": "^3.0|^4.0|^5.0|^6.0", + "symfony/form": "^3.0|^4.0|^5.0|^6.0", + "symfony/translation": "^3.0|^4.0|^5.0|^6.0", + "symfony/uid": "^5.1|^6.0", + "symfony/validator": "^3.1.9|^4.0|^5.0|^6.0", + "symfony/yaml": "^3.3|^4.0|^5.0|^6.0", + "twig/twig": "~1.34|~2.4|^3.0" }, "suggest": { "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", @@ -3075,7 +1926,7 @@ "type": "github" } ], - "time": "2024-01-03T21:21:26+00:00" + "time": "2023-05-18T04:58:29+00:00" }, { "name": "myclabs/deep-copy", @@ -3083,12 +1934,12 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "202aaf6b7c2e1e0a622b0298e9f3f537e4d84018" + "reference": "928a96f585b86224ebc78f8f09d0482cf15b04f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/202aaf6b7c2e1e0a622b0298e9f3f537e4d84018", - "reference": "202aaf6b7c2e1e0a622b0298e9f3f537e4d84018", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/928a96f585b86224ebc78f8f09d0482cf15b04f5", + "reference": "928a96f585b86224ebc78f8f09d0482cf15b04f5", "shasum": "" }, "require": { @@ -3136,31 +1987,29 @@ "type": "tidelift" } ], - "time": "2023-11-01T08:01:43+00:00" + "time": "2023-03-08T17:24:01+00:00" }, { "name": "nikic/php-parser", - "version": "dev-master", + "version": "4.x-dev", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ce019e9ad711e31ee87c2c4c72e538b5240970c3" + "reference": "c9e5a13d68486e9fd75f9be1b4639644e54e7f4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ce019e9ad711e31ee87c2c4c72e538b5240970c3", - "reference": "ce019e9ad711e31ee87c2c4c72e538b5240970c3", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c9e5a13d68486e9fd75f9be1b4639644e54e7f4f", + "reference": "c9e5a13d68486e9fd75f9be1b4639644e54e7f4f", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.4" + "php": ">=7.0" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" }, "default-branch": true, "bin": [ @@ -3169,7 +2018,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "4.9-dev" } }, "autoload": { @@ -3193,9 +2042,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/master" + "source": "https://github.com/nikic/PHP-Parser/tree/4.x" }, - "time": "2024-01-14T09:02:54+00:00" + "time": "2023-05-21T19:22:47+00:00" }, { "name": "phar-io/manifest", @@ -3203,12 +2052,12 @@ "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "67729272c564ab9f953c81f48db44e8b1cb1e1c3" + "reference": "36d8a21e851a9512db2b086dc5ac2c61308f0138" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/67729272c564ab9f953c81f48db44e8b1cb1e1c3", - "reference": "67729272c564ab9f953c81f48db44e8b1cb1e1c3", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/36d8a21e851a9512db2b086dc5ac2c61308f0138", + "reference": "36d8a21e851a9512db2b086dc5ac2c61308f0138", "shasum": "" }, "require": { @@ -3217,7 +2066,7 @@ "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", - "php": "^7.3 || ^8.0" + "php": "^7.2 || ^8.0" }, "default-branch": true, "type": "library", @@ -3263,7 +2112,7 @@ "type": "github" } ], - "time": "2023-06-01T14:19:47+00:00" + "time": "2022-02-21T19:55:33+00:00" }, { "name": "phar-io/version", @@ -3322,12 +2171,12 @@ "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "416ca2ac2a84555b785a98002d613fe13d1d1c2f" + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/416ca2ac2a84555b785a98002d613fe13d1d1c2f", - "reference": "416ca2ac2a84555b785a98002d613fe13d1d1c2f", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", "shasum": "" }, "require": { @@ -3335,14 +2184,14 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" }, "default-branch": true, "type": "library", "extra": { "bamarni-bin": { "bin-links": true, - "forward-command": false + "forward-command": true }, "branch-alias": { "dev-master": "1.9-dev" @@ -3378,7 +2227,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/master" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" }, "funding": [ { @@ -3390,27 +2239,26 @@ "type": "tidelift" } ], - "time": "2023-11-12T22:52:20+00:00" + "time": "2023-02-25T19:38:58+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.25.0", + "version": "1.21.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + "reference": "7f78fd1ff463a7884a331fdb84a25f724dbfd9ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7f78fd1ff463a7884a331fdb84a25f724dbfd9ea", + "reference": "7f78fd1ff463a7884a331fdb84a25f724dbfd9ea", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/annotations": "^2.0", "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", @@ -3420,6 +2268,7 @@ "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -3435,22 +2284,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.21.x" }, - "time": "2024-01-04T17:06:16+00:00" + "time": "2023-05-17T16:44:57+00:00" }, { "name": "phpstan/phpstan", - "version": "1.10.50", + "version": "1.10.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" + "reference": "42afb02dce13d12623865f068f32ad340ee8ed6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/42afb02dce13d12623865f068f32ad340ee8ed6f", + "reference": "42afb02dce13d12623865f068f32ad340ee8ed6f", "shasum": "" }, "require": { @@ -3499,39 +2348,39 @@ "type": "tidelift" } ], - "time": "2023-12-13T10:59:42+00:00" + "time": "2023-05-25T11:20:07+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.x-dev", + "version": "9.2.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "e8a1b365339597e7268340f4df827c2688be7f83" + "reference": "100663232669bdacd3ac18f4cc12c38beec9aff1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e8a1b365339597e7268340f4df827c2688be7f83", - "reference": "e8a1b365339597e7268340f4df827c2688be7f83", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/100663232669bdacd3ac18f4cc12c38beec9aff1", + "reference": "100663232669bdacd3ac18f4cc12c38beec9aff1", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-text-template": "^3.0", - "sebastian/code-unit-reverse-lookup": "^3.0", - "sebastian/complexity": "^3.0", - "sebastian/environment": "^6.0", - "sebastian/lines-of-code": "^2.0", - "sebastian/version": "^4.0", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "phpunit/phpunit": "^10.1" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3540,7 +2389,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-master": "9.2-dev" } }, "autoload": { @@ -3569,7 +2418,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2" }, "funding": [ { @@ -3577,32 +2426,32 @@ "type": "github" } ], - "time": "2023-12-31T07:35:59+00:00" + "time": "2023-05-25T06:20:28+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "4.1.x-dev", + "version": "3.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "b36c308bfdd69e7bbafc4348f31c83a822458ec5" + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/b36c308bfdd69e7bbafc4348f31c83a822458ec5", - "reference": "b36c308bfdd69e7bbafc4348f31c83a822458ec5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/38b24367e1b340aa78b96d7cab042942d917bb84", + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.1-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3629,8 +2478,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0" }, "funding": [ { @@ -3638,28 +2486,28 @@ "type": "github" } ], - "time": "2023-12-31T07:36:54+00:00" + "time": "2022-02-11T16:23:04+00:00" }, { "name": "phpunit/php-invoker", - "version": "4.0.x-dev", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "4dc48195da24dd8200c891fc80f4c42675837d40" + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/4dc48195da24dd8200c891fc80f4c42675837d40", - "reference": "4dc48195da24dd8200c891fc80f4c42675837d40", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-pcntl": "*" @@ -3667,7 +2515,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -3693,8 +2541,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" }, "funding": [ { @@ -3702,32 +2549,32 @@ "type": "github" } ], - "time": "2023-12-31T07:37:20+00:00" + "time": "2020-09-28T05:58:55+00:00" }, { "name": "phpunit/php-text-template", - "version": "3.0.x-dev", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "647402cf7377b645e13d5dc7b842549296149ae9" + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/647402cf7377b645e13d5dc7b842549296149ae9", - "reference": "647402cf7377b645e13d5dc7b842549296149ae9", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -3753,8 +2600,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" }, "funding": [ { @@ -3762,32 +2608,32 @@ "type": "github" } ], - "time": "2023-12-31T07:38:21+00:00" + "time": "2020-10-26T05:33:50+00:00" }, { "name": "phpunit/php-timer", - "version": "6.0.x-dev", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "ec2a787f80646fbaca26bdcf16bb52c5d358a716" + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/ec2a787f80646fbaca26bdcf16bb52c5d358a716", - "reference": "ec2a787f80646fbaca26bdcf16bb52c5d358a716", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -3813,8 +2659,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0" + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" }, "funding": [ { @@ -3822,23 +2667,24 @@ "type": "github" } ], - "time": "2023-12-31T07:38:42+00:00" + "time": "2020-10-26T13:16:10+00:00" }, { "name": "phpunit/phpunit", - "version": "10.5.3", + "version": "9.6.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6fce887c71076a73f32fd3e0774a6833fc5c7f19" + "reference": "9d8ffd638716761de07b60262fceb553cb4afcd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6fce887c71076a73f32fd3e0774a6833fc5c7f19", - "reference": "6fce887c71076a73f32fd3e0774a6833fc5c7f19", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9d8ffd638716761de07b60262fceb553cb4afcd9", + "reference": "9d8ffd638716761de07b60262fceb553cb4afcd9", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -3848,26 +2694,27 @@ "myclabs/deep-copy": "^1.10.1", "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", - "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.5", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-invoker": "^4.0", - "phpunit/php-text-template": "^3.0", - "phpunit/php-timer": "^6.0", - "sebastian/cli-parser": "^2.0", - "sebastian/code-unit": "^2.0", - "sebastian/comparator": "^5.0", - "sebastian/diff": "^5.0", - "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.1", - "sebastian/global-state": "^6.0.1", - "sebastian/object-enumerator": "^5.0", - "sebastian/recursion-context": "^5.0", - "sebastian/type": "^4.0", - "sebastian/version": "^4.0" + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -3875,7 +2722,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -3907,7 +2754,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.3" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6" }, "funding": [ { @@ -3923,7 +2770,7 @@ "type": "tidelift" } ], - "time": "2023-12-13T07:25:23+00:00" + "time": "2023-05-25T06:23:23+00:00" }, { "name": "psr/cache", @@ -3978,18 +2825,72 @@ }, "time": "2021-02-24T03:25:37+00:00" }, + { + "name": "psr/container", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5", + "reference": "90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, + "time": "2022-07-19T17:36:59+00:00" + }, { "name": "psr/event-dispatcher", "version": "dev-master", "source": { "type": "git", "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "977ffcf551e3bfb73d90aac3e8e1583fd8d2f89a" + "reference": "e275e2d67d53964a3f13e056886ecd769edee021" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/977ffcf551e3bfb73d90aac3e8e1583fd8d2f89a", - "reference": "977ffcf551e3bfb73d90aac3e8e1583fd8d2f89a", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/e275e2d67d53964a3f13e056886ecd769edee021", + "reference": "e275e2d67d53964a3f13e056886ecd769edee021", "shasum": "" }, "require": { @@ -4027,9 +2928,172 @@ "psr-14" ], "support": { - "source": "https://github.com/php-fig/event-dispatcher" + "source": "https://github.com/php-fig/event-dispatcher/tree/master" }, - "time": "2023-09-22T11:10:57+00:00" + "time": "2022-06-29T17:22:39+00:00" + }, + { + "name": "psr/http-client", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/1.0.2" + }, + "time": "2023-04-10T20:12:12+00:00" + }, + { + "name": "psr/http-factory", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "6d70f402f0eddb2b154b22950b5381bbf5b28469" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/6d70f402f0eddb2b154b22950b5381bbf5b28469", + "reference": "6d70f402f0eddb2b154b22950b5381bbf5b28469", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2023-05-17T18:32:11+00:00" + }, + { + "name": "psr/http-message", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/log", @@ -4082,9 +3146,53 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "scrutinizer/ocular", - "version": "1.9", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/scrutinizer-ci/ocular.git", @@ -4108,6 +3216,7 @@ "phpunit/phpunit": "^9.0.0", "symfony/filesystem": "~2.0|~3.0|~4.0|~5.0|^6.0" }, + "default-branch": true, "bin": [ "bin/ocular" ], @@ -4129,28 +3238,28 @@ }, { "name": "sebastian/cli-parser", - "version": "2.0.x-dev", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "c7c703fd197a868d43a4cd838de640ee66714c7f" + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c7c703fd197a868d43a4cd838de640ee66714c7f", - "reference": "c7c703fd197a868d43a4cd838de640ee66714c7f", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -4173,8 +3282,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" }, "funding": [ { @@ -4182,32 +3290,32 @@ "type": "github" } ], - "time": "2023-12-31T07:30:32+00:00" + "time": "2020-09-28T06:08:49+00:00" }, { "name": "sebastian/code-unit", - "version": "2.0.x-dev", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1901c2cd4d50e5b1c86e067e078032708122b960" + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1901c2cd4d50e5b1c86e067e078032708122b960", - "reference": "1901c2cd4d50e5b1c86e067e078032708122b960", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -4230,8 +3338,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0" + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" }, "funding": [ { @@ -4239,32 +3346,32 @@ "type": "github" } ], - "time": "2023-12-31T07:31:01+00:00" + "time": "2020-10-26T13:08:54+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "3.0.x-dev", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "35abdc17ebae0ddd0e174f9743cfe88936851758" + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/35abdc17ebae0ddd0e174f9743cfe88936851758", - "reference": "35abdc17ebae0ddd0e174f9743cfe88936851758", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -4286,8 +3393,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" }, "funding": [ { @@ -4295,36 +3401,34 @@ "type": "github" } ], - "time": "2023-12-31T07:31:25+00:00" + "time": "2020-09-28T05:30:19+00:00" }, { "name": "sebastian/comparator", - "version": "5.0.x-dev", + "version": "4.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "06c9db507d5d6fb5efaa1a0b1a229a12e0ce8574" + "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/06c9db507d5d6fb5efaa1a0b1a229a12e0ce8574", - "reference": "06c9db507d5d6fb5efaa1a0b1a229a12e0ce8574", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b247957a1c8dc81a671770f74b479c0a78a818f1", + "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/diff": "^5.0", - "sebastian/exporter": "^5.0" + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^10.4" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -4363,8 +3467,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0" }, "funding": [ { @@ -4372,33 +3475,33 @@ "type": "github" } ], - "time": "2023-12-31T07:31:46+00:00" + "time": "2022-09-14T12:46:14+00:00" }, { "name": "sebastian/complexity", - "version": "3.2.x-dev", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "7337abb9e9beaf34af915ec1497599d75b5e2f4d" + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/7337abb9e9beaf34af915ec1497599d75b5e2f4d", - "reference": "7337abb9e9beaf34af915ec1497599d75b5e2f4d", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" + "nikic/php-parser": "^4.7", + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.2-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -4421,8 +3524,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/3.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" }, "funding": [ { @@ -4430,33 +3532,33 @@ "type": "github" } ], - "time": "2023-12-31T07:32:09+00:00" + "time": "2020-10-26T15:52:27+00:00" }, { "name": "sebastian/diff", - "version": "5.1.x-dev", + "version": "4.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "6fc9529a8e8eff86ad1f9125bad481b7c898f08f" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6fc9529a8e8eff86ad1f9125bad481b7c898f08f", - "reference": "6fc9529a8e8eff86ad1f9125bad481b7c898f08f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0", - "symfony/process": "^6.4" + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -4488,8 +3590,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0" }, "funding": [ { @@ -4497,27 +3598,27 @@ "type": "github" } ], - "time": "2023-12-31T07:32:59+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", - "version": "6.0.x-dev", + "version": "5.1.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "a7b60a4fd8d7ee68f06d2986ae38ebfbd7224399" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a7b60a4fd8d7ee68f06d2986ae38ebfbd7224399", - "reference": "a7b60a4fd8d7ee68f06d2986ae38ebfbd7224399", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-posix": "*" @@ -4525,7 +3626,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4544,7 +3645,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", + "homepage": "http://www.github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -4552,8 +3653,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.0" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1" }, "funding": [ { @@ -4561,34 +3661,34 @@ "type": "github" } ], - "time": "2023-12-31T07:33:18+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "5.1.x-dev", + "version": "4.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "0af67d5d3b795b3a034a5ecb9c494658de9ef55d" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0af67d5d3b795b3a034a5ecb9c494658de9ef55d", - "reference": "0af67d5d3b795b3a034a5ecb9c494658de9ef55d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/recursion-context": "^5.0" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -4630,8 +3730,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0" }, "funding": [ { @@ -4639,35 +3738,38 @@ "type": "github" } ], - "time": "2023-12-31T07:33:37+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.x-dev", + "version": "5.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "f15849a2a7207eff11f73919dfd279e2cc85dd1b" + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/f15849a2a7207eff11f73919dfd279e2cc85dd1b", - "reference": "f15849a2a7207eff11f73919dfd279e2cc85dd1b", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -4686,14 +3788,13 @@ } ], "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" }, "funding": [ { @@ -4701,33 +3802,33 @@ "type": "github" } ], - "time": "2023-12-31T07:33:55+00:00" + "time": "2022-02-14T08:28:10+00:00" }, { "name": "sebastian/lines-of-code", - "version": "2.0.x-dev", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "29f7720f283062763daf59d02499b569fd0b53e4" + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/29f7720f283062763daf59d02499b569fd0b53e4", - "reference": "29f7720f283062763daf59d02499b569fd0b53e4", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" + "nikic/php-parser": "^4.6", + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -4750,8 +3851,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" }, "funding": [ { @@ -4759,34 +3859,34 @@ "type": "github" } ], - "time": "2023-12-31T07:34:13+00:00" + "time": "2020-11-28T06:42:11+00:00" }, { "name": "sebastian/object-enumerator", - "version": "5.0.x-dev", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "8df0d10e04532a48d17078a4cb504bfcfc3a1101" + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/8df0d10e04532a48d17078a4cb504bfcfc3a1101", - "reference": "8df0d10e04532a48d17078a4cb504bfcfc3a1101", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -4808,8 +3908,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" }, "funding": [ { @@ -4817,32 +3916,32 @@ "type": "github" } ], - "time": "2023-12-31T07:34:38+00:00" + "time": "2020-10-26T13:12:34+00:00" }, { "name": "sebastian/object-reflector", - "version": "3.0.x-dev", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "f6eb82d03f39cb1d13ae9ad9b7cb745144ecacda" + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f6eb82d03f39cb1d13ae9ad9b7cb745144ecacda", - "reference": "f6eb82d03f39cb1d13ae9ad9b7cb745144ecacda", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -4864,8 +3963,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" }, "funding": [ { @@ -4873,32 +3971,32 @@ "type": "github" } ], - "time": "2023-12-31T07:35:14+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/recursion-context", - "version": "5.0.x-dev", + "version": "4.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "f7894029dc8a280837f77d643e881b53b6a04677" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f7894029dc8a280837f77d643e881b53b6a04677", - "reference": "f7894029dc8a280837f77d643e881b53b6a04677", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -4928,8 +4026,7 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -4937,32 +4034,87 @@ "type": "github" } ], - "time": "2023-12-31T07:40:00+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { - "name": "sebastian/type", - "version": "4.0.x-dev", + "name": "sebastian/resource-operations", + "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "4337b83f7f8e9260afb858162b7c801a728c0353" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "20bdda85c7c585ab265c0c37ec052a019bae29c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/4337b83f7f8e9260afb858162b7c801a728c0353", - "reference": "4337b83f7f8e9260afb858162b7c801a728c0353", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/20bdda85c7c585ab265c0c37ec052a019bae29c4", + "reference": "20bdda85c7c585ab265c0c37ec052a019bae29c4", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-03-25T08:11:39+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -4985,8 +4137,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/4.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2" }, "funding": [ { @@ -4994,29 +4145,29 @@ "type": "github" } ], - "time": "2023-12-31T07:40:32+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", - "version": "4.0.x-dev", + "version": "3.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "116978f15ca072d5df4dab9419a32a66e386830d" + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/116978f15ca072d5df4dab9419a32a66e386830d", - "reference": "116978f15ca072d5df4dab9419a32a66e386830d", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -5039,8 +4190,7 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/4.0" + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, "funding": [ { @@ -5048,7 +4198,164 @@ "type": "github" } ], - "time": "2023-12-31T07:41:08+00:00" + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/console", + "version": "6.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6d340bfbfc082c9ed7eec4844f12132a26b2d344" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6d340bfbfc082c9ed7eec4844f12132a26b2d344", + "reference": "6d340bfbfc082c9ed7eec4844f12132a26b2d344", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/6.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T16:34:37+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/event-dispatcher", @@ -5056,12 +4363,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e95216850555cd55e71b857eb9d6c2674124603a" + "reference": "9ebe352542105f5f7186610a83deb18b90fae3d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e95216850555cd55e71b857eb9d6c2674124603a", - "reference": "e95216850555cd55e71b857eb9d6c2674124603a", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9ebe352542105f5f7186610a83deb18b90fae3d3", + "reference": "9ebe352542105f5f7186610a83deb18b90fae3d3", "shasum": "" }, "require": { @@ -5128,31 +4435,30 @@ "type": "tidelift" } ], - "time": "2023-12-27T22:16:42+00:00" + "time": "2023-05-23T16:34:37+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "dev-main", + "version": "3.4.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "705c57c64120840dc3043ef1d43916f46af4f986" + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/705c57c64120840dc3043ef1d43916f46af4f986", - "reference": "705c57c64120840dc3043ef1d43916f46af4f986", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", "shasum": "" }, "require": { "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -5189,7 +4495,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/main" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/3.4" }, "funding": [ { @@ -5205,7 +4511,7 @@ "type": "tidelift" } ], - "time": "2024-01-02T14:07:37+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/filesystem", @@ -5213,12 +4519,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59" + "reference": "c2196aa8b563ed0bc645ee316c40ead823adfead" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/952a8cb588c3bc6ce76f6023000fb932f16a6e59", - "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c2196aa8b563ed0bc645ee316c40ead823adfead", + "reference": "c2196aa8b563ed0bc645ee316c40ead823adfead", "shasum": "" }, "require": { @@ -5252,7 +4558,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/6.4" + "source": "https://github.com/symfony/filesystem/tree/v6.3.0-RC1" }, "funding": [ { @@ -5268,7 +4574,7 @@ "type": "tidelift" } ], - "time": "2023-07-26T17:27:13+00:00" + "time": "2023-04-28T16:05:33+00:00" }, { "name": "symfony/finder", @@ -5276,12 +4582,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + "reference": "1bb60aa99f06979e6078007a812eb7c5ffc8efc2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "url": "https://api.github.com/repos/symfony/finder/zipball/1bb60aa99f06979e6078007a812eb7c5ffc8efc2", + "reference": "1bb60aa99f06979e6078007a812eb7c5ffc8efc2", "shasum": "" }, "require": { @@ -5332,7 +4638,7 @@ "type": "tidelift" } ], - "time": "2023-10-31T17:30:12+00:00" + "time": "2023-05-23T16:34:37+00:00" }, { "name": "symfony/options-resolver", @@ -5340,12 +4646,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a10f19f5198d589d5c33333cffe98dc9820332dd", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd", "shasum": "" }, "require": { @@ -5383,7 +4689,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/6.4" + "source": "https://github.com/symfony/options-resolver/tree/v6.3.0-RC1" }, "funding": [ { @@ -5399,11 +4705,345 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:16:24+00:00" + "time": "2023-05-12T14:21:09+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/main" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/main" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/main" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f9c7affe77a00ae32ca127ca6833d034e6d33f25", + "reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/main" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-30T17:25:47+00:00" }, { "name": "symfony/polyfill-php80", - "version": "1.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -5467,7 +5107,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/main" }, "funding": [ { @@ -5487,7 +5127,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "1.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -5547,7 +5187,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php81/tree/main" }, "funding": [ { @@ -5571,12 +5211,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d33b31f8d8de203ed1feb7c3b7cc003174e32fd8" + "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d33b31f8d8de203ed1feb7c3b7cc003174e32fd8", - "reference": "d33b31f8d8de203ed1feb7c3b7cc003174e32fd8", + "url": "https://api.github.com/repos/symfony/process/zipball/8741e3ed7fe2e91ec099e02446fb86667a0f1628", + "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628", "shasum": "" }, "require": { @@ -5608,7 +5248,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/6.4" + "source": "https://github.com/symfony/process/tree/v6.3.0-RC1" }, "funding": [ { @@ -5624,7 +5264,89 @@ "type": "tidelift" } ], - "time": "2024-01-19T13:57:07+00:00" + "time": "2023-05-19T08:06:44+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/stopwatch", @@ -5689,17 +5411,103 @@ "time": "2023-02-16T10:14:28+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.2", + "name": "symfony/string", + "version": "6.4.x-dev", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "url": "https://github.com/symfony/string.git", + "reference": "89bc6d5dcc94c89781e1f986e4d01b7ee91d684b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/symfony/string/zipball/89bc6d5dcc94c89781e1f986e4d01b7ee91d684b", + "reference": "89bc6d5dcc94c89781e1f986e4d01b7ee91d684b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/6.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T16:34:37+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", "shasum": "" }, "require": { @@ -5728,7 +5536,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" }, "funding": [ { @@ -5736,7 +5544,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2021-07-28T10:34:58+00:00" } ], "aliases": [], @@ -5744,7 +5552,8 @@ "stability-flags": { "shish/gqla": 20, "naroga/redis-cache": 20, - "symfony/console": 20 + "scrutinizer/ocular": 20, + "phpstan/phpstan": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/core/basepage.php b/core/basepage.php index 9580c135..2acdb953 100644 --- a/core/basepage.php +++ b/core/basepage.php @@ -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("", 40); + # static handler will map these to themes/foo/static/bar.ico or ext/static_files/static/bar.ico $this->add_html_header("", 41); $this->add_html_header("", 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("", 43); - - $initjs_cache_file = $this->get_initjs_cache_file($theme_name, $config_latest); - $this->add_html_header("", 44); - - $js_cache_file = $this->get_js_cache_file($theme_name, $config_latest); - $this->add_html_header("", 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("", 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("", 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(""), - HTML( - ["lang" => "en"], - HEAD(rawHTML($head)), - BODY($body_attrs, rawHTML($body)) - ) - ); + print << + + $head_html + $body_html + +EOD; } protected function head_html(): string @@ -574,8 +508,10 @@ class BasePage $html_header_html = $this->get_all_html_headers(); return " - {$this->title} - $html_header_html + + {$this->title} + $html_header_html + "; } @@ -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 ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; return " -
-

{$this->heading}

- $sub_block_html -
- -
- $flash_html - $main_block_html -
-
- $footer_html -
+ +
+ {$this->heading} + $sub_block_html +
+ +
+ $flash_html + $main_block_html +
+
+ $footer_html +
+ "; } @@ -633,7 +576,7 @@ class BasePage Shimmie © Shish & The Team - 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; +} diff --git a/core/basethemelet.php b/core/basethemelet.php index b3cc0545..d646e851 100644 --- a/core/basethemelet.php +++ b/core/basethemelet.php @@ -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(""); if ($page_number < $total_pages) { - $page->add_html_header(""); - $page->add_html_header(""); + $page->add_html_header(""); + $page->add_html_header(""); } if ($page_number > 1) { - $page->add_html_header(""); + $page->add_html_header(""); } $page->add_html_header(""); } 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, diff --git a/core/block.php b/core/block.php index bd433229..0242656a 100644 --- a/core/block.php +++ b/core/block.php @@ -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; diff --git a/core/cacheengine.php b/core/cacheengine.php index 0ae69a8e..01a66f90 100644 --- a/core/cacheengine.php +++ b/core/cacheengine.php @@ -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 - */ 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 $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)) { diff --git a/core/cli_app.php b/core/cli_app.php deleted file mode 100644 index 47fec20b..00000000 --- a/core/cli_app.php +++ /dev/null @@ -1,66 +0,0 @@ -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); - } -} diff --git a/core/command_builder.php b/core/command_builder.php index 22b02742..34b4532e 100644 --- a/core/command_builder.php +++ b/core/command_builder.php @@ -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; diff --git a/core/config.php b/core/config.php index 8f930e0e..3372bf17 100644 --- a/core/config.php +++ b/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 */ 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|null - * @param T $default - * @return T|array - */ - 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 diff --git a/core/database.php b/core/database.php index d15e897e..72dd60a9 100644 --- a/core/database.php +++ b/core/database.php @@ -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 */ - public array $args; - - /** - * @param array $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 */ 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> */ 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 */ 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 */ 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 */ 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) { diff --git a/core/dbengine.php b/core/dbengine.php index 53a28136..bf32cd47 100644 --- a/core/dbengine.php +++ b/core/dbengine.php @@ -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]; } } diff --git a/core/event.php b/core/event.php index c615af10..db6798ab 100644 --- a/core/event.php +++ b/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"; + } } } diff --git a/core/exceptions.php b/core/exceptions.php index 9683d33c..3d1dff4f 100644 --- a/core/exceptions.php +++ b/core/exceptions.php @@ -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; } } diff --git a/core/extension.php b/core/extension.php index 5b1bfe2e..c5b7edec 100644 --- a/core/extension.php +++ b/core/extension.php @@ -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 */ 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 */ private static array $all_info_by_key = []; - /** @var array */ 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 = []; diff --git a/core/imageboard/autocomplete_column.php b/core/imageboard/autocomplete_column.php deleted file mode 100644 index b7d63bdc..00000000 --- a/core/imageboard/autocomplete_column.php +++ /dev/null @@ -1,34 +0,0 @@ - "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}"] - ]); - } -} diff --git a/core/imageboard/event.php b/core/imageboard/event.php index f7be1f2b..1580f848 100644 --- a/core/imageboard/event.php +++ b/core/imageboard/event.php @@ -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; diff --git a/core/imageboard/image.php b/core/imageboard/image.php index 84af71cb..df4e5de7 100644 --- a/core/imageboard/image.php +++ b/core/imageboard/image.php @@ -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 */ +#[\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 */ - public static array $prop_types = []; - /** @var array */ - 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|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; + } } diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php index 8c6f0989..09def27e 100644 --- a/core/imageboard/misc.php +++ b/core/imageboard/misc.php @@ -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); } diff --git a/core/imageboard/search.php b/core/imageboard/search.php index 34f0dcc8..11ac9b03 100644 --- a/core/imageboard/search.php +++ b/core/imageboard/search.php @@ -4,17 +4,11 @@ declare(strict_types=1); namespace Shimmie2; -use GQLA\Query; - class Querylet { - /** - * @param string $sql - * @param array $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 */ - public static array $_search_path = []; - - /** - * @param list $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 $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 $tags - * @return \Generator - */ - 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 $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 - */ - 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; - } -} diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php index d5eddd6b..0780aaaa 100644 --- a/core/imageboard/tag.php +++ b/core/imageboard/tag.php @@ -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 */ - 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; + } } diff --git a/core/install.php b/core/install.php index 4ba4e666..b18c14f8 100644 --- a/core/install.php +++ b/core/install.php @@ -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) ? '' : ""; $db_m = in_array(DatabaseDriverID::MYSQL->value, $drivers) ? '' : ""; $db_p = in_array(DatabaseDriverID::PGSQL->value, $drivers) ? '' : ""; + $db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '' : ""; $warn_msg = $warnings ? "

Warnings

".implode("\n

", $warnings) : ""; $err_msg = $errors ? "

Errors

".implode("\n

", $errors) : ""; @@ -137,9 +132,9 @@ function ask_questions(): void Type: @@ -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; + }); } @@ -179,9 +178,8 @@ function ask_questions(): void The username provided must have access to create tables within the database.

- 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.

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", - "

If you aren't redirected, click here to Continue." - ); - } + header("Location: index.php?flash=Installation%20complete"); + die_nicely( + "Installation Successful", + "

If you aren't redirected, click here to Continue." + ); } else { $h_file_content = htmlentities($file_content); throw new InstallerException( diff --git a/core/logging.php b/core/logging.php index 0d113724..7b5c3694 100644 --- a/core/logging.php +++ b/core/logging.php @@ -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); } diff --git a/core/microhtml.php b/core/microhtml.php index 33fb375c..4cb1a943 100644 --- a/core/microhtml.php +++ b/core/microhtml.php @@ -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 $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 $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 $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 . - * @param array $options An array of pairs of parameters for

diff --git a/core/send_event.php b/core/send_event.php index 553a4b15..33eed65f 100644 --- a/core/send_event.php +++ b/core/send_event.php @@ -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> $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; } } diff --git a/core/stdlib_ex.php b/core/stdlib_ex.php deleted file mode 100644 index 516d0ea0..00000000 --- a/core/stdlib_ex.php +++ /dev/null @@ -1,96 +0,0 @@ - $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)); -} diff --git a/core/sys_config.php b/core/sys_config.php index 4d58efad..2f5cb0c2 100644 --- a/core/sys_config.php +++ b/core/sys_config.php @@ -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 diff --git a/core/testcase.php b/core/testcase.php deleted file mode 100644 index 2dfe126c..00000000 --- a/core/testcase.php +++ /dev/null @@ -1,282 +0,0 @@ -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 $args - * @return array - */ - 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 $get_args - * @param array $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 $args - */ - protected static function get_page(string $page_name, array $args = []): Page - { - return self::request("GET", $page_name, $args, []); - } - - /** - * @param array $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 - { - } -} diff --git a/core/tests/ImageTest.php b/core/tests/ImageTest.php deleted file mode 100644 index 8c860a9a..00000000 --- a/core/tests/ImageTest.php +++ /dev/null @@ -1,18 +0,0 @@ -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); - } -} diff --git a/core/tests/SearchTest.php b/core/tests/SearchTest.php deleted file mode 100644 index 3054af3e..00000000 --- a/core/tests/SearchTest.php +++ /dev/null @@ -1,524 +0,0 @@ -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); - } -} diff --git a/core/tests/StdLibExTest.php b/core/tests/StdLibExTest.php deleted file mode 100644 index c6a3476c..00000000 --- a/core/tests/StdLibExTest.php +++ /dev/null @@ -1,27 +0,0 @@ -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() - ); - } -} diff --git a/core/tests/TagTest.php b/core/tests/TagTest.php deleted file mode 100644 index a678e406..00000000 --- a/core/tests/TagTest.php +++ /dev/null @@ -1,21 +0,0 @@ -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"])); - } -} diff --git a/core/tests/UrlsTest.php b/core/tests/UrlsTest.php deleted file mode 100644 index 07c1e1ec..00000000 --- a/core/tests/UrlsTest.php +++ /dev/null @@ -1,273 +0,0 @@ - $vars - * @return array - */ - $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" => "", - "SCRIPT_FILENAME" => "/var/www/html/index.php", - "DOCUMENT_ROOT" => "/var/www/html", - ]), "root directory"); - $this->assertEquals("/mydir", get_base_href([ - "PHP_SELF" => "", - "SCRIPT_FILENAME" => "/var/www/html/mydir/index.php", - "DOCUMENT_ROOT" => "/var/www/html", - ]), "subdirectory"); - $this->assertEquals("", get_base_href([ - "PHP_SELF" => "", - "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" => "", - "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(); - } -} diff --git a/core/tests/BasePageTest.php b/core/tests/basepage.test.php similarity index 87% rename from core/tests/BasePageTest.php rename to core/tests/basepage.test.php index 922bc6a5..fdec72c1 100644 --- a/core/tests/BasePageTest.php +++ b/core/tests/basepage.test.php @@ -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); diff --git a/core/tests/BlockTest.php b/core/tests/block.test.php similarity index 91% rename from core/tests/BlockTest.php rename to core/tests/block.test.php index b8ae0b78..1051e773 100644 --- a/core/tests/BlockTest.php +++ b/core/tests/block.test.php @@ -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( diff --git a/core/tests/InitTest.php b/core/tests/init.test.php similarity index 69% rename from core/tests/InitTest.php rename to core/tests/init.test.php index 8cb5965e..02d3a0ff 100644 --- a/core/tests/InitTest.php +++ b/core/tests/init.test.php @@ -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); diff --git a/core/tests/PolyfillsTest.php b/core/tests/polyfills.test.php similarity index 74% rename from core/tests/PolyfillsTest.php rename to core/tests/polyfills.test.php index 2ca5328f..a196da3b 100644 --- a/core/tests/PolyfillsTest.php +++ b/core/tests/polyfills.test.php @@ -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( "", @@ -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)); - } } diff --git a/core/tests/tag.test.php b/core/tests/tag.test.php new file mode 100644 index 00000000..1fdbcdf4 --- /dev/null +++ b/core/tests/tag.test.php @@ -0,0 +1,35 @@ +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"])); + } +} diff --git a/core/tests/urls.test.php b/core/tests/urls.test.php new file mode 100644 index 00000000..fb34d4ab --- /dev/null +++ b/core/tests/urls.test.php @@ -0,0 +1,105 @@ +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"]) + ); + } +} diff --git a/core/tests/UtilTest.php b/core/tests/util.test.php similarity index 85% rename from core/tests/UtilTest.php rename to core/tests/util.test.php index d622cc2d..30fffbf7 100644 --- a/core/tests/UtilTest.php +++ b/core/tests/util.test.php @@ -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") ); } diff --git a/core/urls.php b/core/urls.php index 980bced8..9ef20b41 100644 --- a/core/urls.php +++ b/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 - * - 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|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 $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 $changes */ function modify_current_url(array $changes): string { return modify_url($_SERVER['REQUEST_URI'], $changes); } -/** - * Take a URL and modify some parameters - * - * @param array $changes - */ function modify_url(string $url, array $changes): string { - /** @var array */ $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; diff --git a/core/user.php b/core/user.php index bbfafb8f..4692699d 100644 --- a/core/user.php +++ b/core/user.php @@ -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 $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 diff --git a/core/userclass.php b/core/userclass.php index e9da051a..8677cc78 100644 --- a/core/userclass.php +++ b/core/userclass.php @@ -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 */ - public static array $known_classes = []; - #[Field] public ?string $name = null; public ?UserClass $parent = null; - - /** @var array */ public array $abilities = []; - /** - * @param array $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, diff --git a/core/util.php b/core/util.php index 76895b84..25d83ae4 100644 --- a/core/util.php +++ b/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 - */ -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 - */ 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 ? "

Hash: $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 .= "

Query: " . html_escape($query); - $q .= "

Args: " . html_escape(var_export($e->args, true)); - } + $q = $query ? "" : "

Query: " . 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)); -} diff --git a/ext/admin/main.php b/ext/admin/main.php index c95cd3a7..43783a83 100644 --- a/ext/admin/main.php +++ b/ext/admin/main.php @@ -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 \n"; + print "\t\teg 'get-page post/list'\n\n"; + print "\tpost-page \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 \n"; + print "\t\tregenerate a thumbnail\n\n"; + print "\tcache [get|set|del] [key] \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)) { diff --git a/ext/admin/test.php b/ext/admin/test.php index 6975d1e8..a4cbfcae 100644 --- a/ext/admin/test.php +++ b/ext/admin/test.php @@ -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); + } } diff --git a/ext/admin/theme.php b/ext/admin/theme.php index 521e0104..27bacdfa 100644 --- a/ext/admin/theme.php +++ b/ext/admin/theme.php @@ -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; diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php index 58b11bbd..c62286bf 100644 --- a/ext/alias_editor/main.php +++ b/ext/alias_editor/main.php @@ -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; diff --git a/ext/alias_editor/test.php b/ext/alias_editor/test.php index 55522cde..78437026 100644 --- a/ext/alias_editor/test.php +++ b/ext/alias_editor/test.php @@ -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(); diff --git a/ext/alias_editor/theme.php b/ext/alias_editor/theme.php index af21f8ff..14ece72e 100644 --- a/ext/alias_editor/theme.php +++ b/ext/alias_editor/theme.php @@ -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); diff --git a/ext/approval/info.php b/ext/approval/info.php index b5f51db2..3ed711a4 100644 --- a/ext/approval/info.php +++ b/ext/approval/info.php @@ -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."; } diff --git a/ext/approval/main.php b/ext/approval/main.php index 23b83536..935091b9 100644 --- a/ext/approval/main.php +++ b/ext/approval/main.php @@ -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; diff --git a/ext/approval/theme.php b/ext/approval/theme.php index 2dc28226..07c063bd 100644 --- a/ext/approval/theme.php +++ b/ext/approval/theme.php @@ -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)); diff --git a/ext/artists/info.php b/ext/artists/info.php index 870ece6d..2cd26752 100644 --- a/ext/artists/info.php +++ b/ext/artists/info.php @@ -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; diff --git a/ext/artists/main.php b/ext/artists/main.php index 30303069..0c1cb158 100644 --- a/ext/artists/main.php +++ b/ext/artists/main.php @@ -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 */ 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++) { diff --git a/ext/artists/test.php b/ext/artists/test.php index 36bdaed8..47f312bd 100644 --- a/ext/artists/test.php +++ b/ext/artists/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class ArtistsTest extends ShimmiePHPUnitTestCase { - public function testSearch(): void + public function testSearch() { global $user; $this->log_in_as_user(); diff --git a/ext/artists/theme.php b/ext/artists/theme.php index 8d2531fb..c367518f 100644 --- a/ext/artists/theme.php +++ b/ext/artists/theme.php @@ -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 = "".str_replace("_", " ", $artist['name']).""; + $artist_link = "".str_replace("_", " ", $artist['name']).""; $html = " @@ -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 = ""; diff --git a/ext/auto_tagger/info.php b/ext/auto_tagger/info.php index a1c8697d..ad1a7969 100644 --- a/ext/auto_tagger/info.php +++ b/ext/auto_tagger/info.php @@ -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"; } diff --git a/ext/auto_tagger/main.php b/ext/auto_tagger/main.php index 3d328991..d67c3cf3 100644 --- a/ext/auto_tagger/main.php +++ b/ext/auto_tagger/main.php @@ -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; diff --git a/ext/auto_tagger/test.php b/ext/auto_tagger/test.php index 49ca03fe..e8ed6293 100644 --- a/ext/auto_tagger/test.php +++ b/ext/auto_tagger/test.php @@ -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(); diff --git a/ext/auto_tagger/theme.php b/ext/auto_tagger/theme.php index abe99281..a96ab76c 100644 --- a/ext/auto_tagger/theme.php +++ b/ext/auto_tagger/theme.php @@ -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; diff --git a/ext/autocomplete/info.php b/ext/autocomplete/info.php index dd1ce7d2..bf853976 100644 --- a/ext/autocomplete/info.php +++ b/ext/autocomplete/info.php @@ -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."; } diff --git a/ext/autocomplete/lib/jquery-ui.min.css b/ext/autocomplete/lib/jquery-ui.min.css new file mode 100644 index 00000000..91f16a3b --- /dev/null +++ b/ext/autocomplete/lib/jquery-ui.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.11.2 - 2014-10-16 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px +* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:0.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:0.4em 2.1em 0.4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_228ef1_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_ffd27a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} \ No newline at end of file diff --git a/ext/autocomplete/lib/jquery-ui.min.js b/ext/autocomplete/lib/jquery-ui.min.js new file mode 100644 index 00000000..17eab790 --- /dev/null +++ b/ext/autocomplete/lib/jquery-ui.min.js @@ -0,0 +1,13 @@ +/*! jQuery UI - v1.11.2 - 2014-10-16 +* http://jqueryui.com +* Includes: core.js, widget.js, mouse.js, position.js, accordion.js, autocomplete.js, button.js, datepicker.js, dialog.js, draggable.js, droppable.js, effect.js, effect-blind.js, effect-bounce.js, effect-clip.js, effect-drop.js, effect-explode.js, effect-fade.js, effect-fold.js, effect-highlight.js, effect-puff.js, effect-pulsate.js, effect-scale.js, effect-shake.js, effect-size.js, effect-slide.js, effect-transfer.js, menu.js, progressbar.js, resizable.js, selectable.js, selectmenu.js, slider.js, sortable.js, spinner.js, tabs.js, tooltip.js +* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ + +(function(e){"function"==typeof define&&define.amd?define(["jquery"],e):e(jQuery)})(function(e){function t(t,s){var n,a,o,r=t.nodeName.toLowerCase();return"area"===r?(n=t.parentNode,a=n.name,t.href&&a&&"map"===n.nodeName.toLowerCase()?(o=e("img[usemap='#"+a+"']")[0],!!o&&i(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||s:s)&&i(t)}function i(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}function s(e){for(var t,i;e.length&&e[0]!==document;){if(t=e.css("position"),("absolute"===t||"relative"===t||"fixed"===t)&&(i=parseInt(e.css("zIndex"),10),!isNaN(i)&&0!==i))return i;e=e.parent()}return 0}function n(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},e.extend(this._defaults,this.regional[""]),this.regional.en=e.extend(!0,{},this.regional[""]),this.regional["en-US"]=e.extend(!0,{},this.regional.en),this.dpDiv=a(e("
"))}function a(t){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return t.delegate(i,"mouseout",function(){e(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).removeClass("ui-datepicker-next-hover")}).delegate(i,"mouseover",o)}function o(){e.datepicker._isDisabledDatepicker(v.inline?v.dpDiv.parent()[0]:v.input[0])||(e(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),e(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).addClass("ui-datepicker-next-hover"))}function r(t,i){e.extend(t,i);for(var s in i)null==i[s]&&(t[s]=i[s]);return t}function h(e){return function(){var t=this.element.val();e.apply(this,arguments),this._refresh(),t!==this.element.val()&&this._trigger("change")}}e.ui=e.ui||{},e.extend(e.ui,{version:"1.11.2",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({scrollParent:function(t){var i=this.css("position"),s="absolute"===i,n=t?/(auto|scroll|hidden)/:/(auto|scroll)/,a=this.parents().filter(function(){var t=e(this);return s&&"static"===t.css("position")?!1:n.test(t.css("overflow")+t.css("overflow-y")+t.css("overflow-x"))}).eq(0);return"fixed"!==i&&a.length?a:e(this[0].ownerDocument||document)},uniqueId:function(){var e=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++e)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,s){return!!e.data(t,s[3])},focusable:function(i){return t(i,!isNaN(e.attr(i,"tabindex")))},tabbable:function(i){var s=e.attr(i,"tabindex"),n=isNaN(s);return(n||s>=0)&&t(i,!n)}}),e("").outerWidth(1).jquery||e.each(["Width","Height"],function(t,i){function s(t,i,s,a){return e.each(n,function(){i-=parseFloat(e.css(t,"padding"+this))||0,s&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),a&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var n="Width"===i?["Left","Right"]:["Top","Bottom"],a=i.toLowerCase(),o={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+i]=function(t){return void 0===t?o["inner"+i].call(this):this.each(function(){e(this).css(a,s(this,t)+"px")})},e.fn["outer"+i]=function(t,n){return"number"!=typeof t?o["outer"+i].call(this,t):this.each(function(){e(this).css(a,s(this,t,!0,n)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.fn.extend({focus:function(t){return function(i,s){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),s&&s.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),disableSelection:function(){var e="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.bind(e+".ui-disableSelection",function(e){e.preventDefault()})}}(),enableSelection:function(){return this.unbind(".ui-disableSelection")},zIndex:function(t){if(void 0!==t)return this.css("zIndex",t);if(this.length)for(var i,s,n=e(this[0]);n.length&&n[0]!==document;){if(i=n.css("position"),("absolute"===i||"relative"===i||"fixed"===i)&&(s=parseInt(n.css("zIndex"),10),!isNaN(s)&&0!==s))return s;n=n.parent()}return 0}}),e.ui.plugin={add:function(t,i,s){var n,a=e.ui[t].prototype;for(n in s)a.plugins[n]=a.plugins[n]||[],a.plugins[n].push([i,s[n]])},call:function(e,t,i,s){var n,a=e.plugins[t];if(a&&(s||e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType))for(n=0;a.length>n;n++)e.options[a[n][0]]&&a[n][1].apply(e.element,i)}};var l=0,u=Array.prototype.slice;e.cleanData=function(t){return function(i){var s,n,a;for(a=0;null!=(n=i[a]);a++)try{s=e._data(n,"events"),s&&s.remove&&e(n).triggerHandler("remove")}catch(o){}t(i)}}(e.cleanData),e.widget=function(t,i,s){var n,a,o,r,h={},l=t.split(".")[0];return t=t.split(".")[1],n=l+"-"+t,s||(s=i,i=e.Widget),e.expr[":"][n.toLowerCase()]=function(t){return!!e.data(t,n)},e[l]=e[l]||{},a=e[l][t],o=e[l][t]=function(e,t){return this._createWidget?(arguments.length&&this._createWidget(e,t),void 0):new o(e,t)},e.extend(o,a,{version:s.version,_proto:e.extend({},s),_childConstructors:[]}),r=new i,r.options=e.widget.extend({},r.options),e.each(s,function(t,s){return e.isFunction(s)?(h[t]=function(){var e=function(){return i.prototype[t].apply(this,arguments)},n=function(e){return i.prototype[t].apply(this,e)};return function(){var t,i=this._super,a=this._superApply;return this._super=e,this._superApply=n,t=s.apply(this,arguments),this._super=i,this._superApply=a,t}}(),void 0):(h[t]=s,void 0)}),o.prototype=e.widget.extend(r,{widgetEventPrefix:a?r.widgetEventPrefix||t:t},h,{constructor:o,namespace:l,widgetName:t,widgetFullName:n}),a?(e.each(a._childConstructors,function(t,i){var s=i.prototype;e.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete a._childConstructors):i._childConstructors.push(o),e.widget.bridge(t,o),o},e.widget.extend=function(t){for(var i,s,n=u.call(arguments,1),a=0,o=n.length;o>a;a++)for(i in n[a])s=n[a][i],n[a].hasOwnProperty(i)&&void 0!==s&&(t[i]=e.isPlainObject(s)?e.isPlainObject(t[i])?e.widget.extend({},t[i],s):e.widget.extend({},s):s);return t},e.widget.bridge=function(t,i){var s=i.prototype.widgetFullName||t;e.fn[t]=function(n){var a="string"==typeof n,o=u.call(arguments,1),r=this;return n=!a&&o.length?e.widget.extend.apply(null,[n].concat(o)):n,a?this.each(function(){var i,a=e.data(this,s);return"instance"===n?(r=a,!1):a?e.isFunction(a[n])&&"_"!==n.charAt(0)?(i=a[n].apply(a,o),i!==a&&void 0!==i?(r=i&&i.jquery?r.pushStack(i.get()):i,!1):void 0):e.error("no such method '"+n+"' for "+t+" widget instance"):e.error("cannot call methods on "+t+" prior to initialization; "+"attempted to call method '"+n+"'")}):this.each(function(){var t=e.data(this,s);t?(t.option(n||{}),t._init&&t._init()):e.data(this,s,new i(n,this))}),r}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(t,i){i=e(i||this.defaultElement||this)[0],this.element=e(i),this.uuid=l++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=e(),this.hoverable=e(),this.focusable=e(),i!==this&&(e.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===i&&this.destroy()}}),this.document=e(i.style?i.ownerDocument:i.document||i),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(t,i){var s,n,a,o=t;if(0===arguments.length)return e.widget.extend({},this.options);if("string"==typeof t)if(o={},s=t.split("."),t=s.shift(),s.length){for(n=o[t]=e.widget.extend({},this.options[t]),a=0;s.length-1>a;a++)n[s[a]]=n[s[a]]||{},n=n[s[a]];if(t=s.pop(),1===arguments.length)return void 0===n[t]?null:n[t];n[t]=i}else{if(1===arguments.length)return void 0===this.options[t]?null:this.options[t];o[t]=i}return this._setOptions(o),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,"disabled"===e&&(this.widget().toggleClass(this.widgetFullName+"-disabled",!!t),t&&(this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus"))),this},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_on:function(t,i,s){var n,a=this;"boolean"!=typeof t&&(s=i,i=t,t=!1),s?(i=n=e(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),e.each(s,function(s,o){function r(){return t||a.options.disabled!==!0&&!e(this).hasClass("ui-state-disabled")?("string"==typeof o?a[o]:o).apply(a,arguments):void 0}"string"!=typeof o&&(r.guid=o.guid=o.guid||r.guid||e.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+a.eventNamespace,u=h[2];u?n.delegate(u,l,r):i.bind(l,r)})},_off:function(t,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(i).undelegate(i),this.bindings=e(this.bindings.not(t).get()),this.focusable=e(this.focusable.not(t).get()),this.hoverable=e(this.hoverable.not(t).get())},_delay:function(e,t){function i(){return("string"==typeof e?s[e]:e).apply(s,arguments)}var s=this;return setTimeout(i,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,i,s){var n,a,o=this.options[t];if(s=s||{},i=e.Event(i),i.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(e.isFunction(o)&&o.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,i){e.Widget.prototype["_"+t]=function(s,n,a){"string"==typeof n&&(n={effect:n});var o,r=n?n===!0||"number"==typeof n?i:n.effect||i:t;n=n||{},"number"==typeof n&&(n={duration:n}),o=!e.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),o&&e.effects&&e.effects.effect[r]?s[t](n):r!==t&&s[r]?s[r](n.duration,n.easing,a):s.queue(function(i){e(this)[t](),a&&a.call(s[0]),i()})}}),e.widget;var d=!1;e(document).mouseup(function(){d=!1}),e.widget("ui.mouse",{version:"1.11.2",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var t=this;this.element.bind("mousedown."+this.widgetName,function(e){return t._mouseDown(e)}).bind("click."+this.widgetName,function(i){return!0===e.data(i.target,t.widgetName+".preventClickEvent")?(e.removeData(i.target,t.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&this.document.unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(t){if(!d){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(t),this._mouseDownEvent=t;var i=this,s=1===t.which,n="string"==typeof this.options.cancel&&t.target.nodeName?e(t.target).closest(this.options.cancel).length:!1;return s&&!n&&this._mouseCapture(t)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(t)!==!1,!this._mouseStarted)?(t.preventDefault(),!0):(!0===e.data(t.target,this.widgetName+".preventClickEvent")&&e.removeData(t.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(e){return i._mouseMove(e)},this._mouseUpDelegate=function(e){return i._mouseUp(e)},this.document.bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),t.preventDefault(),d=!0,!0)):!0}},_mouseMove:function(t){if(this._mouseMoved){if(e.ui.ie&&(!document.documentMode||9>document.documentMode)&&!t.button)return this._mouseUp(t);if(!t.which)return this._mouseUp(t)}return(t.which||t.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,t)!==!1,this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted)},_mouseUp:function(t){return this.document.unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&e.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),d=!1,!1},_mouseDistanceMet:function(e){return Math.max(Math.abs(this._mouseDownEvent.pageX-e.pageX),Math.abs(this._mouseDownEvent.pageY-e.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),function(){function t(e,t,i){return[parseFloat(e[0])*(p.test(e[0])?t/100:1),parseFloat(e[1])*(p.test(e[1])?i/100:1)]}function i(t,i){return parseInt(e.css(t,i),10)||0}function s(t){var i=t[0];return 9===i.nodeType?{width:t.width(),height:t.height(),offset:{top:0,left:0}}:e.isWindow(i)?{width:t.width(),height:t.height(),offset:{top:t.scrollTop(),left:t.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:t.outerWidth(),height:t.outerHeight(),offset:t.offset()}}e.ui=e.ui||{};var n,a,o=Math.max,r=Math.abs,h=Math.round,l=/left|center|right/,u=/top|center|bottom/,d=/[\+\-]\d+(\.[\d]+)?%?/,c=/^\w+/,p=/%$/,f=e.fn.position;e.position={scrollbarWidth:function(){if(void 0!==n)return n;var t,i,s=e("
"),a=s.children()[0];return e("body").append(s),t=a.offsetWidth,s.css("overflow","scroll"),i=a.offsetWidth,t===i&&(i=s[0].clientWidth),s.remove(),n=t-i},getScrollInfo:function(t){var i=t.isWindow||t.isDocument?"":t.element.css("overflow-x"),s=t.isWindow||t.isDocument?"":t.element.css("overflow-y"),n="scroll"===i||"auto"===i&&t.widthi?"left":t>0?"right":"center",vertical:0>a?"top":s>0?"bottom":"middle"};d>m&&m>r(t+i)&&(h.horizontal="center"),c>g&&g>r(s+a)&&(h.vertical="middle"),h.important=o(r(t),r(i))>o(r(s),r(a))?"horizontal":"vertical",n.using.call(this,e,h)}),u.offset(e.extend(M,{using:l}))})},e.ui.position={fit:{left:function(e,t){var i,s=t.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=e.left-t.collisionPosition.marginLeft,h=n-r,l=r+t.collisionWidth-a-n;t.collisionWidth>a?h>0&&0>=l?(i=e.left+h+t.collisionWidth-a-n,e.left+=h-i):e.left=l>0&&0>=h?n:h>l?n+a-t.collisionWidth:n:h>0?e.left+=h:l>0?e.left-=l:e.left=o(e.left-r,e.left)},top:function(e,t){var i,s=t.within,n=s.isWindow?s.scrollTop:s.offset.top,a=t.within.height,r=e.top-t.collisionPosition.marginTop,h=n-r,l=r+t.collisionHeight-a-n;t.collisionHeight>a?h>0&&0>=l?(i=e.top+h+t.collisionHeight-a-n,e.top+=h-i):e.top=l>0&&0>=h?n:h>l?n+a-t.collisionHeight:n:h>0?e.top+=h:l>0?e.top-=l:e.top=o(e.top-r,e.top)}},flip:{left:function(e,t){var i,s,n=t.within,a=n.offset.left+n.scrollLeft,o=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=e.left-t.collisionPosition.marginLeft,u=l-h,d=l+t.collisionWidth-o-h,c="left"===t.my[0]?-t.elemWidth:"right"===t.my[0]?t.elemWidth:0,p="left"===t.at[0]?t.targetWidth:"right"===t.at[0]?-t.targetWidth:0,f=-2*t.offset[0];0>u?(i=e.left+c+p+f+t.collisionWidth-o-a,(0>i||r(u)>i)&&(e.left+=c+p+f)):d>0&&(s=e.left-t.collisionPosition.marginLeft+c+p+f-h,(s>0||d>r(s))&&(e.left+=c+p+f))},top:function(e,t){var i,s,n=t.within,a=n.offset.top+n.scrollTop,o=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=e.top-t.collisionPosition.marginTop,u=l-h,d=l+t.collisionHeight-o-h,c="top"===t.my[1],p=c?-t.elemHeight:"bottom"===t.my[1]?t.elemHeight:0,f="top"===t.at[1]?t.targetHeight:"bottom"===t.at[1]?-t.targetHeight:0,m=-2*t.offset[1];0>u?(s=e.top+p+f+m+t.collisionHeight-o-a,e.top+p+f+m>u&&(0>s||r(u)>s)&&(e.top+=p+f+m)):d>0&&(i=e.top-t.collisionPosition.marginTop+p+f+m-h,e.top+p+f+m>d&&(i>0||d>r(i))&&(e.top+=p+f+m))}},flipfit:{left:function(){e.ui.position.flip.left.apply(this,arguments),e.ui.position.fit.left.apply(this,arguments)},top:function(){e.ui.position.flip.top.apply(this,arguments),e.ui.position.fit.top.apply(this,arguments)}}},function(){var t,i,s,n,o,r=document.getElementsByTagName("body")[0],h=document.createElement("div");t=document.createElement(r?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},r&&e.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(o in s)t.style[o]=s[o];t.appendChild(h),i=r||document.documentElement,i.insertBefore(t,i.firstChild),h.style.cssText="position: absolute; left: 10.7432222px;",n=e(h).offset().left,a=n>10&&11>n,t.innerHTML="",i.removeChild(t)}()}(),e.ui.position,e.widget("ui.accordion",{version:"1.11.2",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var t=this.options;this.prevShow=this.prevHide=e(),this.element.addClass("ui-accordion ui-widget ui-helper-reset").attr("role","tablist"),t.collapsible||t.active!==!1&&null!=t.active||(t.active=0),this._processPanels(),0>t.active&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():e()}},_createIcons:function(){var t=this.options.icons;t&&(e("").addClass("ui-accordion-header-icon ui-icon "+t.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(t.header).addClass(t.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var e;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").removeUniqueId(),this._destroyIcons(),e=this.headers.next().removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").css("display","").removeAttr("role").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&e.css("height","")},_setOption:function(e,t){return"active"===e?(this._activate(t),void 0):("event"===e&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(t)),this._super(e,t),"collapsible"!==e||t||this.options.active!==!1||this._activate(0),"icons"===e&&(this._destroyIcons(),t&&this._createIcons()),"disabled"===e&&(this.element.toggleClass("ui-state-disabled",!!t).attr("aria-disabled",t),this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!t)),void 0)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var i=e.ui.keyCode,s=this.headers.length,n=this.headers.index(t.target),a=!1;switch(t.keyCode){case i.RIGHT:case i.DOWN:a=this.headers[(n+1)%s];break;case i.LEFT:case i.UP:a=this.headers[(n-1+s)%s];break;case i.SPACE:case i.ENTER:this._eventHandler(t);break;case i.HOME:a=this.headers[0];break;case i.END:a=this.headers[s-1]}a&&(e(t.target).attr("tabIndex",-1),e(a).attr("tabIndex",0),a.focus(),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===e.ui.keyCode.UP&&t.ctrlKey&&e(t.currentTarget).prev().focus()},refresh:function(){var t=this.options;this._processPanels(),t.active===!1&&t.collapsible===!0||!this.headers.length?(t.active=!1,this.active=e()):t.active===!1?this._activate(0):this.active.length&&!e.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=e()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var e=this.headers,t=this.panels;this.headers=this.element.find(this.options.header).addClass("ui-accordion-header ui-state-default ui-corner-all"),this.panels=this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").filter(":not(.ui-accordion-content-active)").hide(),t&&(this._off(e.not(this.headers)),this._off(t.not(this.panels)))},_refresh:function(){var t,i=this.options,s=i.heightStyle,n=this.element.parent();this.active=this._findActive(i.active).addClass("ui-accordion-header-active ui-state-active ui-corner-top").removeClass("ui-corner-all"),this.active.next().addClass("ui-accordion-content-active").show(),this.headers.attr("role","tab").each(function(){var t=e(this),i=t.uniqueId().attr("id"),s=t.next(),n=s.uniqueId().attr("id");t.attr("aria-controls",n),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(i.event),"fill"===s?(t=n.height(),this.element.siblings(":visible").each(function(){var i=e(this),s=i.css("position");"absolute"!==s&&"fixed"!==s&&(t-=i.outerHeight(!0))}),this.headers.each(function(){t-=e(this).outerHeight(!0)}),this.headers.next().each(function(){e(this).height(Math.max(0,t-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):"auto"===s&&(t=0,this.headers.next().each(function(){t=Math.max(t,e(this).css("height","").height())}).height(t))},_activate:function(t){var i=this._findActive(t)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:e.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):e()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&e.each(t.split(" "),function(e,t){i[t]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var i=this.options,s=this.active,n=e(t.currentTarget),a=n[0]===s[0],o=a&&i.collapsible,r=o?e():n.next(),h=s.next(),l={oldHeader:s,oldPanel:h,newHeader:o?e():n,newPanel:r};t.preventDefault(),a&&!i.collapsible||this._trigger("beforeActivate",t,l)===!1||(i.active=o?!1:this.headers.index(n),this.active=a?e():n,this._toggle(l),s.removeClass("ui-accordion-header-active ui-state-active"),i.icons&&s.children(".ui-accordion-header-icon").removeClass(i.icons.activeHeader).addClass(i.icons.header),a||(n.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),i.icons&&n.children(".ui-accordion-header-icon").removeClass(i.icons.header).addClass(i.icons.activeHeader),n.next().addClass("ui-accordion-content-active")))},_toggle:function(t){var i=t.newPanel,s=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=s,this.options.animate?this._animate(i,s,t):(s.hide(),i.show(),this._toggleComplete(t)),s.attr({"aria-hidden":"true"}),s.prev().attr("aria-selected","false"),i.length&&s.length?s.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===e(this).attr("tabIndex")}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true",tabIndex:0,"aria-expanded":"true"})},_animate:function(e,t,i){var s,n,a,o=this,r=0,h=e.length&&(!t.length||e.index()",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},items:"> *",menus:"ul",position:{my:"left-1 top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item":function(e){e.preventDefault()},"click .ui-menu-item":function(t){var i=e(t.target);!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(t),t.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(t):!this.element.is(":focus")&&e(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(t){if(!this.previousFilter){var i=e(t.currentTarget);i.siblings(".ui-state-active").removeClass("ui-state-active"),this.focus(t,i) +}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(e,t){var i=this.active||this.element.find(this.options.items).eq(0);t||this.focus(e,i)},blur:function(t){this._delay(function(){e.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){this._closeOnDocumentClick(e)&&this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-menu-icons ui-front").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").removeUniqueId().removeClass("ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var t=e(this);t.data("ui-menu-submenu-carat")&&t.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(t){var i,s,n,a,o=!0;switch(t.keyCode){case e.ui.keyCode.PAGE_UP:this.previousPage(t);break;case e.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case e.ui.keyCode.HOME:this._move("first","first",t);break;case e.ui.keyCode.END:this._move("last","last",t);break;case e.ui.keyCode.UP:this.previous(t);break;case e.ui.keyCode.DOWN:this.next(t);break;case e.ui.keyCode.LEFT:this.collapse(t);break;case e.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case e.ui.keyCode.ENTER:case e.ui.keyCode.SPACE:this._activate(t);break;case e.ui.keyCode.ESCAPE:this.collapse(t);break;default:o=!1,s=this.previousFilter||"",n=String.fromCharCode(t.keyCode),a=!1,clearTimeout(this.filterTimer),n===s?a=!0:n=s+n,i=this._filterMenuItems(n),i=a&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(t.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(t,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}o&&t.preventDefault()},_activate:function(e){this.active.is(".ui-state-disabled")||(this.active.is("[aria-haspopup='true']")?this.expand(e):this.select(e))},refresh:function(){var t,i,s=this,n=this.options.icons.submenu,a=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),a.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-front").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=e(this),i=t.parent(),s=e("").addClass("ui-menu-icon ui-icon "+n).data("ui-menu-submenu-carat",!0);i.attr("aria-haspopup","true").prepend(s),t.attr("aria-labelledby",i.attr("id"))}),t=a.add(this.element),i=t.find(this.options.items),i.not(".ui-menu-item").each(function(){var t=e(this);s._isDivider(t)&&t.addClass("ui-widget-content ui-menu-divider")}),i.not(".ui-menu-item, .ui-menu-divider").addClass("ui-menu-item").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!e.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(e,t){"icons"===e&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(t.submenu),"disabled"===e&&this.element.toggleClass("ui-state-disabled",!!t).attr("aria-disabled",t),this._super(e,t)},focus:function(e,t){var i,s;this.blur(e,e&&"focus"===e.type),this._scrollIntoView(t),this.active=t.first(),s=this.active.addClass("ui-state-focus").removeClass("ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").addClass("ui-state-active"),e&&"keydown"===e.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=t.children(".ui-menu"),i.length&&e&&/^mouse/.test(e.type)&&this._startOpening(i),this.activeMenu=t.parent(),this._trigger("focus",e,{item:t})},_scrollIntoView:function(t){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(e.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(e.css(this.activeMenu[0],"paddingTop"))||0,n=t.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=t.outerHeight(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(e,t){t||clearTimeout(this.timer),this.active&&(this.active.removeClass("ui-state-focus"),this.active=null,this._trigger("blur",e,{item:this.active}))},_startOpening:function(e){clearTimeout(this.timer),"true"===e.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(e)},this.delay))},_open:function(t){var i=e.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(t.parents(".ui-menu")).hide().attr("aria-hidden","true"),t.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(t,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:e(t&&t.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(t),this.activeMenu=s},this.delay)},_close:function(e){e||(e=this.active?this.active.parent():this.element),e.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find(".ui-state-active").not(".ui-state-focus").removeClass("ui-state-active")},_closeOnDocumentClick:function(t){return!e(t.target).closest(".ui-menu").length},_isDivider:function(e){return!/[^\-\u2014\u2013\s]/.test(e.text())},collapse:function(e){var t=this.active&&this.active.parent().closest(".ui-menu-item",this.element);t&&t.length&&(this._close(),this.focus(e,t))},expand:function(e){var t=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();t&&t.length&&(this._open(t.parent()),this._delay(function(){this.focus(e,t)}))},next:function(e){this._move("next","first",e)},previous:function(e){this._move("prev","last",e)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(e,t,i){var s;this.active&&(s="first"===e||"last"===e?this.active["first"===e?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[e+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[t]()),this.focus(i,s)},nextPage:function(t){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=e(this),0>i.offset().top-s-n}),this.focus(t,i)):this.focus(t,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(t),void 0)},previousPage:function(t){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=e(this),i.offset().top-s+n>0}),this.focus(t,i)):this.focus(t,this.activeMenu.find(this.options.items).first())),void 0):(this.next(t),void 0)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,void 0;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),void 0):(this._searchTimeout(e),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(e),this._change(e),void 0)}}),this._initSource(),this.menu=e("
"+"",C=d?"":"",x=0;7>x;x++)N=(x+u)%7,C+="";for(M+=C+"",A=this._getDaysInMonth(et,Z),et===e.selectedYear&&Z===e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,A)),P=(this._getFirstDayOfMonth(et,Z)-u+7)%7,I=Math.ceil((P+A)/7),z=Q?this.maxRows>I?this.maxRows:I:I,this.maxRows=z,H=this._daylightSavingAdjust(new Date(et,Z,1-P)),F=0;z>F;F++){for(M+="",E=d?"":"",x=0;7>x;x++)O=g?g.apply(e.input?e.input[0]:null,[H]):[!0,""],j=H.getMonth()!==Z,W=j&&!y||!O[0]||X&&X>H||$&&H>$,E+="",H.setDate(H.getDate()+1),H=this._daylightSavingAdjust(H);M+=E+""}Z++,Z>11&&(Z=0,et++),M+="
"+this._get(e,"weekHeader")+"=5?" class='ui-datepicker-week-end'":"")+">"+""+p[N]+"
"+this._get(e,"calculateWeek")(H)+""+(j&&!v?" ":W?""+H.getDate()+"":""+H.getDate()+"")+"
"+(Q?"

"+(K[0]>0&&T===K[1]-1?"
":""):""),k+=M}_+=k}return _+=l,e._keyEvent=!1,_},_generateMonthYearHeader:function(e,t,i,s,n,a,o,r){var h,l,u,d,c,p,f,m,g=this._get(e,"changeMonth"),v=this._get(e,"changeYear"),y=this._get(e,"showMonthAfterYear"),b="
",_="";if(a||!g)_+=""+o[t]+"";else{for(h=s&&s.getFullYear()===i,l=n&&n.getFullYear()===i,_+=""}if(y||(b+=_+(!a&&g&&v?"":" ")),!e.yearshtml)if(e.yearshtml="",a||!v)b+=""+i+"";else{for(d=this._get(e,"yearRange").split(":"),c=(new Date).getFullYear(),p=function(e){var t=e.match(/c[+\-].*/)?i+parseInt(e.substring(1),10):e.match(/[+\-].*/)?c+parseInt(e,10):parseInt(e,10);return isNaN(t)?c:t},f=p(d[0]),m=Math.max(f,p(d[1]||"")),f=s?Math.max(f,s.getFullYear()):f,m=n?Math.min(m,n.getFullYear()):m,e.yearshtml+="",b+=e.yearshtml,e.yearshtml=null}return b+=this._get(e,"yearSuffix"),y&&(b+=(!a&&g&&v?"":" ")+_),b+="
"},_adjustInstDate:function(e,t,i){var s=e.drawYear+("Y"===i?t:0),n=e.drawMonth+("M"===i?t:0),a=Math.min(e.selectedDay,this._getDaysInMonth(s,n))+("D"===i?t:0),o=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(s,n,a)));e.selectedDay=o.getDate(),e.drawMonth=e.selectedMonth=o.getMonth(),e.drawYear=e.selectedYear=o.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(e)},_restrictMinMax:function(e,t){var i=this._getMinMaxDate(e,"min"),s=this._getMinMaxDate(e,"max"),n=i&&i>t?i:t;return s&&n>s?s:n},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return null==t?[1,1]:"number"==typeof t?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return new Date(e,t,1).getDay()},_canAdjustMonth:function(e,t,i,s){var n=this._getNumberOfMonths(e),a=this._daylightSavingAdjust(new Date(i,s+(0>t?t:n[0]*n[1]),1));return 0>t&&a.setDate(this._getDaysInMonth(a.getFullYear(),a.getMonth())),this._isInRange(e,a)},_isInRange:function(e,t){var i,s,n=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max"),o=null,r=null,h=this._get(e,"yearRange");return h&&(i=h.split(":"),s=(new Date).getFullYear(),o=parseInt(i[0],10),r=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(o+=s),i[1].match(/[+\-].*/)&&(r+=s)),(!n||t.getTime()>=n.getTime())&&(!a||t.getTime()<=a.getTime())&&(!o||t.getFullYear()>=o)&&(!r||r>=t.getFullYear())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,i,s){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var n=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(s,i,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),n,this._getFormatConfig(e))}}),e.fn.datepicker=function(t){if(!this.length)return this;e.datepicker.initialized||(e(document).mousedown(e.datepicker._checkExternalClick),e.datepicker.initialized=!0),0===e("#"+e.datepicker._mainDivId).length&&e("body").append(e.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof t||"isDisabled"!==t&&"getDate"!==t&&"widget"!==t?"option"===t&&2===arguments.length&&"string"==typeof arguments[1]?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof t?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this].concat(i)):e.datepicker._attachDatepicker(this,t)}):e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i))},e.datepicker=new n,e.datepicker.initialized=!1,e.datepicker.uuid=(new Date).getTime(),e.datepicker.version="1.11.2",e.datepicker,e.widget("ui.draggable",e.ui.mouse,{version:"1.11.2",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"===this.options.helper&&this._setPositionRelative(),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._setHandleClassName(),this._mouseInit()},_setOption:function(e,t){this._super(e,t),"handle"===e&&(this._removeHandleClassName(),this._setHandleClassName())},_destroy:function(){return(this.helper||this.element).is(".ui-draggable-dragging")?(this.destroyOnClear=!0,void 0):(this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._removeHandleClassName(),this._mouseDestroy(),void 0)},_mouseCapture:function(t){var i=this.options;return this._blurActiveElement(t),this.helper||i.disabled||e(t.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(t),this.handle?(this._blockFrames(i.iframeFix===!0?"iframe":i.iframeFix),!0):!1)},_blockFrames:function(t){this.iframeBlocks=this.document.find(t).map(function(){var t=e(this);return e("
").css("position","absolute").appendTo(t.parent()).outerWidth(t.outerWidth()).outerHeight(t.outerHeight()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(t){var i=this.document[0];if(this.handleElement.is(t.target))try{i.activeElement&&"body"!==i.activeElement.nodeName.toLowerCase()&&e(i.activeElement).blur()}catch(s){}},_mouseStart:function(t){var i=this.options;return this.helper=this._createHelper(t),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),e.ui.ddmanager&&(e.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=this.helper.parents().filter(function(){return"fixed"===e(this).css("position")}).length>0,this.positionAbs=this.element.offset(),this._refreshOffsets(t),this.originalPosition=this.position=this._generatePosition(t,!1),this.originalPageX=t.pageX,this.originalPageY=t.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",t)===!1?(this._clear(),!1):(this._cacheHelperProportions(),e.ui.ddmanager&&!i.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t),this._normalizeRightBottom(),this._mouseDrag(t,!0),e.ui.ddmanager&&e.ui.ddmanager.dragStart(this,t),!0)},_refreshOffsets:function(e){this.offset={top:this.positionAbs.top-this.margins.top,left:this.positionAbs.left-this.margins.left,scroll:!1,parent:this._getParentOffset(),relative:this._getRelativeOffset()},this.offset.click={left:e.pageX-this.offset.left,top:e.pageY-this.offset.top}},_mouseDrag:function(t,i){if(this.hasFixedAncestor&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(t,!0),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",t,s)===!1)return this._mouseUp({}),!1;this.position=s.position}return this.helper[0].style.left=this.position.left+"px",this.helper[0].style.top=this.position.top+"px",e.ui.ddmanager&&e.ui.ddmanager.drag(this,t),!1},_mouseStop:function(t){var i=this,s=!1;return e.ui.ddmanager&&!this.options.dropBehaviour&&(s=e.ui.ddmanager.drop(this,t)),this.dropped&&(s=this.dropped,this.dropped=!1),"invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||e.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?e(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",t)!==!1&&i._clear()}):this._trigger("stop",t)!==!1&&this._clear(),!1},_mouseUp:function(t){return this._unblockFrames(),e.ui.ddmanager&&e.ui.ddmanager.dragStop(this,t),this.handleElement.is(t.target)&&this.element.focus(),e.ui.mouse.prototype._mouseUp.call(this,t)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(t){return this.options.handle?!!e(t.target).closest(this.element.find(this.options.handle)).length:!0},_setHandleClassName:function(){this.handleElement=this.options.handle?this.element.find(this.options.handle):this.element,this.handleElement.addClass("ui-draggable-handle")},_removeHandleClassName:function(){this.handleElement.removeClass("ui-draggable-handle")},_createHelper:function(t){var i=this.options,s=e.isFunction(i.helper),n=s?e(i.helper.apply(this.element[0],[t])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return n.parents("body").length||n.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s&&n[0]===this.element[0]&&this._setPositionRelative(),n[0]===this.element[0]||/(fixed|absolute)/.test(n.css("position"))||n.css("position","absolute"),n},_setPositionRelative:function(){/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative")},_adjustOffsetFromHelper:function(t){"string"==typeof t&&(t=t.split(" ")),e.isArray(t)&&(t={left:+t[0],top:+t[1]||0}),"left"in t&&(this.offset.click.left=t.left+this.margins.left),"right"in t&&(this.offset.click.left=this.helperProportions.width-t.right+this.margins.left),"top"in t&&(this.offset.click.top=t.top+this.margins.top),"bottom"in t&&(this.offset.click.top=this.helperProportions.height-t.bottom+this.margins.top)},_isRootNode:function(e){return/(html|body)/i.test(e.tagName)||e===this.document[0]},_getParentOffset:function(){var t=this.offsetParent.offset(),i=this.document[0];return"absolute"===this.cssPosition&&this.scrollParent[0]!==i&&e.contains(this.scrollParent[0],this.offsetParent[0])&&(t.left+=this.scrollParent.scrollLeft(),t.top+=this.scrollParent.scrollTop()),this._isRootNode(this.offsetParent[0])&&(t={top:0,left:0}),{top:t.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:t.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"!==this.cssPosition)return{top:0,left:0};var e=this.element.position(),t=this._isRootNode(this.scrollParent[0]);return{top:e.top-(parseInt(this.helper.css("top"),10)||0)+(t?0:this.scrollParent.scrollTop()),left:e.left-(parseInt(this.helper.css("left"),10)||0)+(t?0:this.scrollParent.scrollLeft())}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var t,i,s,n=this.options,a=this.document[0];return this.relativeContainer=null,n.containment?"window"===n.containment?(this.containment=[e(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,e(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,e(window).scrollLeft()+e(window).width()-this.helperProportions.width-this.margins.left,e(window).scrollTop()+(e(window).height()||a.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):"document"===n.containment?(this.containment=[0,0,e(a).width()-this.helperProportions.width-this.margins.left,(e(a).height()||a.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):n.containment.constructor===Array?(this.containment=n.containment,void 0):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=e(n.containment),s=i[0],s&&(t=/(scroll|auto)/.test(i.css("overflow")),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(t?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(t?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relativeContainer=i),void 0):(this.containment=null,void 0) +},_convertPositionTo:function(e,t){t||(t=this.position);var i="absolute"===e?1:-1,s=this._isRootNode(this.scrollParent[0]);return{top:t.top+this.offset.relative.top*i+this.offset.parent.top*i-("fixed"===this.cssPosition?-this.offset.scroll.top:s?0:this.offset.scroll.top)*i,left:t.left+this.offset.relative.left*i+this.offset.parent.left*i-("fixed"===this.cssPosition?-this.offset.scroll.left:s?0:this.offset.scroll.left)*i}},_generatePosition:function(e,t){var i,s,n,a,o=this.options,r=this._isRootNode(this.scrollParent[0]),h=e.pageX,l=e.pageY;return r&&this.offset.scroll||(this.offset.scroll={top:this.scrollParent.scrollTop(),left:this.scrollParent.scrollLeft()}),t&&(this.containment&&(this.relativeContainer?(s=this.relativeContainer.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,e.pageX-this.offset.click.lefti[2]&&(h=i[2]+this.offset.click.left),e.pageY-this.offset.click.top>i[3]&&(l=i[3]+this.offset.click.top)),o.grid&&(n=o.grid[1]?this.originalPageY+Math.round((l-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,l=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-o.grid[1]:n+o.grid[1]:n,a=o.grid[0]?this.originalPageX+Math.round((h-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,h=i?a-this.offset.click.left>=i[0]||a-this.offset.click.left>i[2]?a:a-this.offset.click.left>=i[0]?a-o.grid[0]:a+o.grid[0]:a),"y"===o.axis&&(h=this.originalPageX),"x"===o.axis&&(l=this.originalPageY)),{top:l-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:r?0:this.offset.scroll.top),left:h-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:r?0:this.offset.scroll.left)}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_normalizeRightBottom:function(){"y"!==this.options.axis&&"auto"!==this.helper.css("right")&&(this.helper.width(this.helper.width()),this.helper.css("right","auto")),"x"!==this.options.axis&&"auto"!==this.helper.css("bottom")&&(this.helper.height(this.helper.height()),this.helper.css("bottom","auto"))},_trigger:function(t,i,s){return s=s||this._uiHash(),e.ui.plugin.call(this,t,[i,s,this],!0),/^(drag|start|stop)/.test(t)&&(this.positionAbs=this._convertPositionTo("absolute"),s.offset=this.positionAbs),e.Widget.prototype._trigger.call(this,t,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),e.ui.plugin.add("draggable","connectToSortable",{start:function(t,i,s){var n=e.extend({},i,{item:s.element});s.sortables=[],e(s.options.connectToSortable).each(function(){var i=e(this).sortable("instance");i&&!i.options.disabled&&(s.sortables.push(i),i.refreshPositions(),i._trigger("activate",t,n))})},stop:function(t,i,s){var n=e.extend({},i,{item:s.element});s.cancelHelperRemoval=!1,e.each(s.sortables,function(){var e=this;e.isOver?(e.isOver=0,s.cancelHelperRemoval=!0,e.cancelHelperRemoval=!1,e._storedCSS={position:e.placeholder.css("position"),top:e.placeholder.css("top"),left:e.placeholder.css("left")},e._mouseStop(t),e.options.helper=e.options._helper):(e.cancelHelperRemoval=!0,e._trigger("deactivate",t,n))})},drag:function(t,i,s){e.each(s.sortables,function(){var n=!1,a=this;a.positionAbs=s.positionAbs,a.helperProportions=s.helperProportions,a.offset.click=s.offset.click,a._intersectsWith(a.containerCache)&&(n=!0,e.each(s.sortables,function(){return this.positionAbs=s.positionAbs,this.helperProportions=s.helperProportions,this.offset.click=s.offset.click,this!==a&&this._intersectsWith(this.containerCache)&&e.contains(a.element[0],this.element[0])&&(n=!1),n})),n?(a.isOver||(a.isOver=1,a.currentItem=i.helper.appendTo(a.element).data("ui-sortable-item",!0),a.options._helper=a.options.helper,a.options.helper=function(){return i.helper[0]},t.target=a.currentItem[0],a._mouseCapture(t,!0),a._mouseStart(t,!0,!0),a.offset.click.top=s.offset.click.top,a.offset.click.left=s.offset.click.left,a.offset.parent.left-=s.offset.parent.left-a.offset.parent.left,a.offset.parent.top-=s.offset.parent.top-a.offset.parent.top,s._trigger("toSortable",t),s.dropped=a.element,e.each(s.sortables,function(){this.refreshPositions()}),s.currentItem=s.element,a.fromOutside=s),a.currentItem&&(a._mouseDrag(t),i.position=a.position)):a.isOver&&(a.isOver=0,a.cancelHelperRemoval=!0,a.options._revert=a.options.revert,a.options.revert=!1,a._trigger("out",t,a._uiHash(a)),a._mouseStop(t,!0),a.options.revert=a.options._revert,a.options.helper=a.options._helper,a.placeholder&&a.placeholder.remove(),s._refreshOffsets(t),i.position=s._generatePosition(t,!0),s._trigger("fromSortable",t),s.dropped=!1,e.each(s.sortables,function(){this.refreshPositions()}))})}}),e.ui.plugin.add("draggable","cursor",{start:function(t,i,s){var n=e("body"),a=s.options;n.css("cursor")&&(a._cursor=n.css("cursor")),n.css("cursor",a.cursor)},stop:function(t,i,s){var n=s.options;n._cursor&&e("body").css("cursor",n._cursor)}}),e.ui.plugin.add("draggable","opacity",{start:function(t,i,s){var n=e(i.helper),a=s.options;n.css("opacity")&&(a._opacity=n.css("opacity")),n.css("opacity",a.opacity)},stop:function(t,i,s){var n=s.options;n._opacity&&e(i.helper).css("opacity",n._opacity)}}),e.ui.plugin.add("draggable","scroll",{start:function(e,t,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(t,i,s){var n=s.options,a=!1,o=s.scrollParentNotHidden[0],r=s.document[0];o!==r&&"HTML"!==o.tagName?(n.axis&&"x"===n.axis||(s.overflowOffset.top+o.offsetHeight-t.pageY=0;c--)h=s.snapElements[c].left-s.margins.left,l=h+s.snapElements[c].width,u=s.snapElements[c].top-s.margins.top,d=u+s.snapElements[c].height,h-m>v||g>l+m||u-m>b||y>d+m||!e.contains(s.snapElements[c].item.ownerDocument,s.snapElements[c].item)?(s.snapElements[c].snapping&&s.options.snap.release&&s.options.snap.release.call(s.element,t,e.extend(s._uiHash(),{snapItem:s.snapElements[c].item})),s.snapElements[c].snapping=!1):("inner"!==f.snapMode&&(n=m>=Math.abs(u-b),a=m>=Math.abs(d-y),o=m>=Math.abs(h-v),r=m>=Math.abs(l-g),n&&(i.position.top=s._convertPositionTo("relative",{top:u-s.helperProportions.height,left:0}).top),a&&(i.position.top=s._convertPositionTo("relative",{top:d,left:0}).top),o&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h-s.helperProportions.width}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l}).left)),p=n||a||o||r,"outer"!==f.snapMode&&(n=m>=Math.abs(u-y),a=m>=Math.abs(d-b),o=m>=Math.abs(h-g),r=m>=Math.abs(l-v),n&&(i.position.top=s._convertPositionTo("relative",{top:u,left:0}).top),a&&(i.position.top=s._convertPositionTo("relative",{top:d-s.helperProportions.height,left:0}).top),o&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l-s.helperProportions.width}).left)),!s.snapElements[c].snapping&&(n||a||o||r||p)&&s.options.snap.snap&&s.options.snap.snap.call(s.element,t,e.extend(s._uiHash(),{snapItem:s.snapElements[c].item})),s.snapElements[c].snapping=n||a||o||r||p)}}),e.ui.plugin.add("draggable","stack",{start:function(t,i,s){var n,a=s.options,o=e.makeArray(e(a.stack)).sort(function(t,i){return(parseInt(e(t).css("zIndex"),10)||0)-(parseInt(e(i).css("zIndex"),10)||0)});o.length&&(n=parseInt(e(o[0]).css("zIndex"),10)||0,e(o).each(function(t){e(this).css("zIndex",n+t)}),this.css("zIndex",n+o.length))}}),e.ui.plugin.add("draggable","zIndex",{start:function(t,i,s){var n=e(i.helper),a=s.options;n.css("zIndex")&&(a._zIndex=n.css("zIndex")),n.css("zIndex",a.zIndex)},stop:function(t,i,s){var n=s.options;n._zIndex&&e(i.helper).css("zIndex",n._zIndex)}}),e.ui.draggable,e.widget("ui.resizable",e.ui.mouse,{version:"1.11.2",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_num:function(e){return parseInt(e,10)||0},_isNumber:function(e){return!isNaN(parseInt(e,10))},_hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",n=!1;return t[s]>0?!0:(t[s]=1,n=t[s]>0,t[s]=0,n)},_create:function(){var t,i,s,n,a,o=this,r=this.options;if(this.element.addClass("ui-resizable"),e.extend(this,{_aspectRatio:!!r.aspectRatio,aspectRatio:r.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:r.helper||r.ghost||r.animate?r.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(e("
").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=r.handles||(e(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),t=this.handles.split(","),this.handles={},i=0;t.length>i;i++)s=e.trim(t[i]),a="ui-resizable-"+s,n=e("
"),n.css({zIndex:r.zIndex}),"se"===s&&n.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(n);this._renderAxis=function(t){var i,s,n,a;t=t||this.element;for(i in this.handles)this.handles[i].constructor===String&&(this.handles[i]=this.element.children(this.handles[i]).first().show()),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)&&(s=e(this.handles[i],this.element),a=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),t.css(n,a),this._proportionallyResize()),e(this.handles[i]).length},this._renderAxis(this.element),this._handles=e(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){o.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),o.axis=n&&n[1]?n[1]:"se")}),r.autoHide&&(this._handles.hide(),e(this.element).addClass("ui-resizable-autohide").mouseenter(function(){r.disabled||(e(this).removeClass("ui-resizable-autohide"),o._handles.show())}).mouseleave(function(){r.disabled||o.resizing||(e(this).addClass("ui-resizable-autohide"),o._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var t,i=function(t){e(t).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),t=this.element,this.originalElement.css({position:t.css("position"),width:t.outerWidth(),height:t.outerHeight(),top:t.css("top"),left:t.css("left")}).insertAfter(t),t.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_mouseCapture:function(t){var i,s,n=!1;for(i in this.handles)s=e(this.handles[i])[0],(s===t.target||e.contains(s,t.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(t){var i,s,n,a=this.options,o=this.element;return this.resizing=!0,this._renderProxy(),i=this._num(this.helper.css("left")),s=this._num(this.helper.css("top")),a.containment&&(i+=e(a.containment).scrollLeft()||0,s+=e(a.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:i,top:s},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:o.width(),height:o.height()},this.originalSize=this._helper?{width:o.outerWidth(),height:o.outerHeight()}:{width:o.width(),height:o.height()},this.sizeDiff={width:o.outerWidth()-o.width(),height:o.outerHeight()-o.height()},this.originalPosition={left:i,top:s},this.originalMousePosition={left:t.pageX,top:t.pageY},this.aspectRatio="number"==typeof a.aspectRatio?a.aspectRatio:this.originalSize.width/this.originalSize.height||1,n=e(".ui-resizable-"+this.axis).css("cursor"),e("body").css("cursor","auto"===n?this.axis+"-resize":n),o.addClass("ui-resizable-resizing"),this._propagate("start",t),!0},_mouseDrag:function(t){var i,s,n=this.originalMousePosition,a=this.axis,o=t.pageX-n.left||0,r=t.pageY-n.top||0,h=this._change[a];return this._updatePrevProperties(),h?(i=h.apply(this,[t,o,r]),this._updateVirtualBoundaries(t.shiftKey),(this._aspectRatio||t.shiftKey)&&(i=this._updateRatio(i,t)),i=this._respectSize(i,t),this._updateCache(i),this._propagate("resize",t),s=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),e.isEmptyObject(s)||(this._updatePrevProperties(),this._trigger("resize",t,this.ui()),this._applyChanges()),!1):!1},_mouseStop:function(t){this.resizing=!1;var i,s,n,a,o,r,h,l=this.options,u=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&this._hasScroll(i[0],"left")?0:u.sizeDiff.height,a=s?0:u.sizeDiff.width,o={width:u.helper.width()-a,height:u.helper.height()-n},r=parseInt(u.element.css("left"),10)+(u.position.left-u.originalPosition.left)||null,h=parseInt(u.element.css("top"),10)+(u.position.top-u.originalPosition.top)||null,l.animate||this.element.css(e.extend(o,{top:h,left:r})),u.helper.height(u.size.height),u.helper.width(u.size.width),this._helper&&!l.animate&&this._proportionallyResize()),e("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",t),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var e={};return this.position.top!==this.prevPosition.top&&(e.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(e.left=this.position.left+"px"),this.size.width!==this.prevSize.width&&(e.width=this.size.width+"px"),this.size.height!==this.prevSize.height&&(e.height=this.size.height+"px"),this.helper.css(e),e},_updateVirtualBoundaries:function(e){var t,i,s,n,a,o=this.options;a={minWidth:this._isNumber(o.minWidth)?o.minWidth:0,maxWidth:this._isNumber(o.maxWidth)?o.maxWidth:1/0,minHeight:this._isNumber(o.minHeight)?o.minHeight:0,maxHeight:this._isNumber(o.maxHeight)?o.maxHeight:1/0},(this._aspectRatio||e)&&(t=a.minHeight*this.aspectRatio,s=a.minWidth/this.aspectRatio,i=a.maxHeight*this.aspectRatio,n=a.maxWidth/this.aspectRatio,t>a.minWidth&&(a.minWidth=t),s>a.minHeight&&(a.minHeight=s),a.maxWidth>i&&(a.maxWidth=i),a.maxHeight>n&&(a.maxHeight=n)),this._vBoundaries=a},_updateCache:function(e){this.offset=this.helper.offset(),this._isNumber(e.left)&&(this.position.left=e.left),this._isNumber(e.top)&&(this.position.top=e.top),this._isNumber(e.height)&&(this.size.height=e.height),this._isNumber(e.width)&&(this.size.width=e.width)},_updateRatio:function(e){var t=this.position,i=this.size,s=this.axis;return this._isNumber(e.height)?e.width=e.height*this.aspectRatio:this._isNumber(e.width)&&(e.height=e.width/this.aspectRatio),"sw"===s&&(e.left=t.left+(i.width-e.width),e.top=null),"nw"===s&&(e.top=t.top+(i.height-e.height),e.left=t.left+(i.width-e.width)),e},_respectSize:function(e){var t=this._vBoundaries,i=this.axis,s=this._isNumber(e.width)&&t.maxWidth&&t.maxWidthe.width,o=this._isNumber(e.height)&&t.minHeight&&t.minHeight>e.height,r=this.originalPosition.left+this.originalSize.width,h=this.position.top+this.size.height,l=/sw|nw|w/.test(i),u=/nw|ne|n/.test(i);return a&&(e.width=t.minWidth),o&&(e.height=t.minHeight),s&&(e.width=t.maxWidth),n&&(e.height=t.maxHeight),a&&l&&(e.left=r-t.minWidth),s&&l&&(e.left=r-t.maxWidth),o&&u&&(e.top=h-t.minHeight),n&&u&&(e.top=h-t.maxHeight),e.width||e.height||e.left||!e.top?e.width||e.height||e.top||!e.left||(e.left=null):e.top=null,e},_getPaddingPlusBorderDimensions:function(e){for(var t=0,i=[],s=[e.css("borderTopWidth"),e.css("borderRightWidth"),e.css("borderBottomWidth"),e.css("borderLeftWidth")],n=[e.css("paddingTop"),e.css("paddingRight"),e.css("paddingBottom"),e.css("paddingLeft")];4>t;t++)i[t]=parseInt(s[t],10)||0,i[t]+=parseInt(n[t],10)||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var e,t=0,i=this.helper||this.element;this._proportionallyResizeElements.length>t;t++)e=this._proportionallyResizeElements[t],this.outerDimensions||(this.outerDimensions=this._getPaddingPlusBorderDimensions(e)),e.css({height:i.height()-this.outerDimensions.height||0,width:i.width()-this.outerDimensions.width||0})},_renderProxy:function(){var t=this.element,i=this.options;this.elementOffset=t.offset(),this._helper?(this.helper=this.helper||e("
"),this.helper.addClass(this._helper).css({width:this.element.outerWidth()-1,height:this.element.outerHeight()-1,position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(e,t){return{width:this.originalSize.width+t}},w:function(e,t){var i=this.originalSize,s=this.originalPosition;return{left:s.left+t,width:i.width-t}},n:function(e,t,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(e,t,i){return{height:this.originalSize.height+i}},se:function(t,i,s){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[t,i,s]))},sw:function(t,i,s){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[t,i,s]))},ne:function(t,i,s){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[t,i,s]))},nw:function(t,i,s){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[t,i,s]))}},_propagate:function(t,i){e.ui.plugin.call(this,t,[i,this.ui()]),"resize"!==t&&this._trigger(t,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),e.ui.plugin.add("resizable","animate",{stop:function(t){var i=e(this).resizable("instance"),s=i.options,n=i._proportionallyResizeElements,a=n.length&&/textarea/i.test(n[0].nodeName),o=a&&i._hasScroll(n[0],"left")?0:i.sizeDiff.height,r=a?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-o},l=parseInt(i.element.css("left"),10)+(i.position.left-i.originalPosition.left)||null,u=parseInt(i.element.css("top"),10)+(i.position.top-i.originalPosition.top)||null;i.element.animate(e.extend(h,u&&l?{top:u,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseInt(i.element.css("width"),10),height:parseInt(i.element.css("height"),10),top:parseInt(i.element.css("top"),10),left:parseInt(i.element.css("left"),10)};n&&n.length&&e(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",t)}})}}),e.ui.plugin.add("resizable","containment",{start:function(){var t,i,s,n,a,o,r,h=e(this).resizable("instance"),l=h.options,u=h.element,d=l.containment,c=d instanceof e?d.get(0):/parent/.test(d)?u.parent().get(0):d;c&&(h.containerElement=e(c),/document/.test(d)||d===document?(h.containerOffset={left:0,top:0},h.containerPosition={left:0,top:0},h.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}):(t=e(c),i=[],e(["Top","Right","Left","Bottom"]).each(function(e,s){i[e]=h._num(t.css("padding"+s))}),h.containerOffset=t.offset(),h.containerPosition=t.position(),h.containerSize={height:t.innerHeight()-i[3],width:t.innerWidth()-i[1]},s=h.containerOffset,n=h.containerSize.height,a=h.containerSize.width,o=h._hasScroll(c,"left")?c.scrollWidth:a,r=h._hasScroll(c)?c.scrollHeight:n,h.parentData={element:c,left:s.left,top:s.top,width:o,height:r}))},resize:function(t){var i,s,n,a,o=e(this).resizable("instance"),r=o.options,h=o.containerOffset,l=o.position,u=o._aspectRatio||t.shiftKey,d={top:0,left:0},c=o.containerElement,p=!0;c[0]!==document&&/static/.test(c.css("position"))&&(d=h),l.left<(o._helper?h.left:0)&&(o.size.width=o.size.width+(o._helper?o.position.left-h.left:o.position.left-d.left),u&&(o.size.height=o.size.width/o.aspectRatio,p=!1),o.position.left=r.helper?h.left:0),l.top<(o._helper?h.top:0)&&(o.size.height=o.size.height+(o._helper?o.position.top-h.top:o.position.top),u&&(o.size.width=o.size.height*o.aspectRatio,p=!1),o.position.top=o._helper?h.top:0),n=o.containerElement.get(0)===o.element.parent().get(0),a=/relative|absolute/.test(o.containerElement.css("position")),n&&a?(o.offset.left=o.parentData.left+o.position.left,o.offset.top=o.parentData.top+o.position.top):(o.offset.left=o.element.offset().left,o.offset.top=o.element.offset().top),i=Math.abs(o.sizeDiff.width+(o._helper?o.offset.left-d.left:o.offset.left-h.left)),s=Math.abs(o.sizeDiff.height+(o._helper?o.offset.top-d.top:o.offset.top-h.top)),i+o.size.width>=o.parentData.width&&(o.size.width=o.parentData.width-i,u&&(o.size.height=o.size.width/o.aspectRatio,p=!1)),s+o.size.height>=o.parentData.height&&(o.size.height=o.parentData.height-s,u&&(o.size.width=o.size.height*o.aspectRatio,p=!1)),p||(o.position.left=o.prevPosition.left,o.position.top=o.prevPosition.top,o.size.width=o.prevSize.width,o.size.height=o.prevSize.height)},stop:function(){var t=e(this).resizable("instance"),i=t.options,s=t.containerOffset,n=t.containerPosition,a=t.containerElement,o=e(t.helper),r=o.offset(),h=o.outerWidth()-t.sizeDiff.width,l=o.outerHeight()-t.sizeDiff.height;t._helper&&!i.animate&&/relative/.test(a.css("position"))&&e(this).css({left:r.left-n.left-s.left,width:h,height:l}),t._helper&&!i.animate&&/static/.test(a.css("position"))&&e(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),e.ui.plugin.add("resizable","alsoResize",{start:function(){var t=e(this).resizable("instance"),i=t.options,s=function(t){e(t).each(function(){var t=e(this);t.data("ui-resizable-alsoresize",{width:parseInt(t.width(),10),height:parseInt(t.height(),10),left:parseInt(t.css("left"),10),top:parseInt(t.css("top"),10)})})};"object"!=typeof i.alsoResize||i.alsoResize.parentNode?s(i.alsoResize):i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):e.each(i.alsoResize,function(e){s(e)})},resize:function(t,i){var s=e(this).resizable("instance"),n=s.options,a=s.originalSize,o=s.originalPosition,r={height:s.size.height-a.height||0,width:s.size.width-a.width||0,top:s.position.top-o.top||0,left:s.position.left-o.left||0},h=function(t,s){e(t).each(function(){var t=e(this),n=e(this).data("ui-resizable-alsoresize"),a={},o=s&&s.length?s:t.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(o,function(e,t){var i=(n[t]||0)+(r[t]||0);i&&i>=0&&(a[t]=i||null)}),t.css(a)})};"object"!=typeof n.alsoResize||n.alsoResize.nodeType?h(n.alsoResize):e.each(n.alsoResize,function(e,t){h(e,t)})},stop:function(){e(this).removeData("resizable-alsoresize")}}),e.ui.plugin.add("resizable","ghost",{start:function(){var t=e(this).resizable("instance"),i=t.options,s=t.size;t.ghost=t.originalElement.clone(),t.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass("string"==typeof i.ghost?i.ghost:""),t.ghost.appendTo(t.helper)},resize:function(){var t=e(this).resizable("instance");t.ghost&&t.ghost.css({position:"relative",height:t.size.height,width:t.size.width})},stop:function(){var t=e(this).resizable("instance");t.ghost&&t.helper&&t.helper.get(0).removeChild(t.ghost.get(0))}}),e.ui.plugin.add("resizable","grid",{resize:function(){var t,i=e(this).resizable("instance"),s=i.options,n=i.size,a=i.originalSize,o=i.originalPosition,r=i.axis,h="number"==typeof s.grid?[s.grid,s.grid]:s.grid,l=h[0]||1,u=h[1]||1,d=Math.round((n.width-a.width)/l)*l,c=Math.round((n.height-a.height)/u)*u,p=a.width+d,f=a.height+c,m=s.maxWidth&&p>s.maxWidth,g=s.maxHeight&&f>s.maxHeight,v=s.minWidth&&s.minWidth>p,y=s.minHeight&&s.minHeight>f;s.grid=h,v&&(p+=l),y&&(f+=u),m&&(p-=l),g&&(f-=u),/^(se|s|e)$/.test(r)?(i.size.width=p,i.size.height=f):/^(ne)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.top=o.top-c):/^(sw)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.left=o.left-d):((0>=f-u||0>=p-l)&&(t=i._getPaddingPlusBorderDimensions(this)),f-u>0?(i.size.height=f,i.position.top=o.top-c):(f=u-t.height,i.size.height=f,i.position.top=o.top+a.height-f),p-l>0?(i.size.width=p,i.position.left=o.left-d):(p=u-t.height,i.size.width=p,i.position.left=o.left+a.width-p))}}),e.ui.resizable,e.widget("ui.dialog",{version:"1.11.2",options:{appendTo:"body",autoOpen:!0,buttons:[],closeOnEscape:!0,closeText:"Close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var i=e(this).css(t).offset().top;0>i&&e(this).css("top",t.top-i)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},sizeRelatedOptions:{buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},resizableRelatedOptions:{maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),this.options.title=this.options.title||this.originalTitle,this._createWrapper(),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(this.uiDialog),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&e.fn.draggable&&this._makeDraggable(),this.options.resizable&&e.fn.resizable&&this._makeResizable(),this._isOpen=!1,this._trackFocus()},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var t=this.options.appendTo;return t&&(t.jquery||t.nodeType)?e(t):this.document.find(t||"body").eq(0)},_destroy:function(){var e,t=this.originalPosition;this._destroyOverlay(),this.element.removeUniqueId().removeClass("ui-dialog-content ui-widget-content").css(this.originalCss).detach(),this.uiDialog.stop(!0,!0).remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.children().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},disable:e.noop,enable:e.noop,close:function(t){var i,s=this;if(this._isOpen&&this._trigger("beforeClose",t)!==!1){if(this._isOpen=!1,this._focusedElement=null,this._destroyOverlay(),this._untrackInstance(),!this.opener.filter(":focusable").focus().length)try{i=this.document[0].activeElement,i&&"body"!==i.nodeName.toLowerCase()&&e(i).blur()}catch(n){}this._hide(this.uiDialog,this.options.hide,function(){s._trigger("close",t)})}},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(t,i){var s=!1,n=this.uiDialog.siblings(".ui-front:visible").map(function(){return+e(this).css("z-index")}).get(),a=Math.max.apply(null,n);return a>=+this.uiDialog.css("z-index")&&(this.uiDialog.css("z-index",a+1),s=!0),s&&!i&&this._trigger("focus",t),s},open:function(){var t=this;return this._isOpen?(this._moveToTop()&&this._focusTabbable(),void 0):(this._isOpen=!0,this.opener=e(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this.overlay&&this.overlay.css("z-index",this.uiDialog.css("z-index")-1),this._show(this.uiDialog,this.options.show,function(){t._focusTabbable(),t._trigger("focus")}),this._makeFocusTarget(),this._trigger("open"),void 0)},_focusTabbable:function(){var e=this._focusedElement;e||(e=this.element.find("[autofocus]")),e.length||(e=this.element.find(":tabbable")),e.length||(e=this.uiDialogButtonPane.find(":tabbable")),e.length||(e=this.uiDialogTitlebarClose.filter(":tabbable")),e.length||(e=this.uiDialog),e.eq(0).focus()},_keepFocus:function(t){function i(){var t=this.document[0].activeElement,i=this.uiDialog[0]===t||e.contains(this.uiDialog[0],t);i||this._focusTabbable()}t.preventDefault(),i.call(this),this._delay(i)},_createWrapper:function(){this.uiDialog=e("
").addClass("ui-dialog ui-widget ui-widget-content ui-corner-all ui-front "+this.options.dialogClass).hide().attr({tabIndex:-1,role:"dialog"}).appendTo(this._appendTo()),this._on(this.uiDialog,{keydown:function(t){if(this.options.closeOnEscape&&!t.isDefaultPrevented()&&t.keyCode&&t.keyCode===e.ui.keyCode.ESCAPE)return t.preventDefault(),this.close(t),void 0; +if(t.keyCode===e.ui.keyCode.TAB&&!t.isDefaultPrevented()){var i=this.uiDialog.find(":tabbable"),s=i.filter(":first"),n=i.filter(":last");t.target!==n[0]&&t.target!==this.uiDialog[0]||t.shiftKey?t.target!==s[0]&&t.target!==this.uiDialog[0]||!t.shiftKey||(this._delay(function(){n.focus()}),t.preventDefault()):(this._delay(function(){s.focus()}),t.preventDefault())}},mousedown:function(e){this._moveToTop(e)&&this._focusTabbable()}}),this.element.find("[aria-describedby]").length||this.uiDialog.attr({"aria-describedby":this.element.uniqueId().attr("id")})},_createTitlebar:function(){var t;this.uiDialogTitlebar=e("
").addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(this.uiDialog),this._on(this.uiDialogTitlebar,{mousedown:function(t){e(t.target).closest(".ui-dialog-titlebar-close")||this.uiDialog.focus()}}),this.uiDialogTitlebarClose=e("").button({label:this.options.closeText,icons:{primary:"ui-icon-closethick"},text:!1}).addClass("ui-dialog-titlebar-close").appendTo(this.uiDialogTitlebar),this._on(this.uiDialogTitlebarClose,{click:function(e){e.preventDefault(),this.close(e)}}),t=e("").uniqueId().addClass("ui-dialog-title").prependTo(this.uiDialogTitlebar),this._title(t),this.uiDialog.attr({"aria-labelledby":t.attr("id")})},_title:function(e){this.options.title||e.html(" "),e.text(this.options.title)},_createButtonPane:function(){this.uiDialogButtonPane=e("
").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),this.uiButtonSet=e("
").addClass("ui-dialog-buttonset").appendTo(this.uiDialogButtonPane),this._createButtons()},_createButtons:function(){var t=this,i=this.options.buttons;return this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),e.isEmptyObject(i)||e.isArray(i)&&!i.length?(this.uiDialog.removeClass("ui-dialog-buttons"),void 0):(e.each(i,function(i,s){var n,a;s=e.isFunction(s)?{click:s,text:i}:s,s=e.extend({type:"button"},s),n=s.click,s.click=function(){n.apply(t.element[0],arguments)},a={icons:s.icons,text:s.showText},delete s.icons,delete s.showText,e("",s).button(a).appendTo(t.uiButtonSet)}),this.uiDialog.addClass("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog),void 0)},_makeDraggable:function(){function t(e){return{position:e.position,offset:e.offset}}var i=this,s=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(s,n){e(this).addClass("ui-dialog-dragging"),i._blockFrames(),i._trigger("dragStart",s,t(n))},drag:function(e,s){i._trigger("drag",e,t(s))},stop:function(n,a){var o=a.offset.left-i.document.scrollLeft(),r=a.offset.top-i.document.scrollTop();s.position={my:"left top",at:"left"+(o>=0?"+":"")+o+" "+"top"+(r>=0?"+":"")+r,of:i.window},e(this).removeClass("ui-dialog-dragging"),i._unblockFrames(),i._trigger("dragStop",n,t(a))}})},_makeResizable:function(){function t(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}var i=this,s=this.options,n=s.resizable,a=this.uiDialog.css("position"),o="string"==typeof n?n:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:s.maxWidth,maxHeight:s.maxHeight,minWidth:s.minWidth,minHeight:this._minHeight(),handles:o,start:function(s,n){e(this).addClass("ui-dialog-resizing"),i._blockFrames(),i._trigger("resizeStart",s,t(n))},resize:function(e,s){i._trigger("resize",e,t(s))},stop:function(n,a){var o=i.uiDialog.offset(),r=o.left-i.document.scrollLeft(),h=o.top-i.document.scrollTop();s.height=i.uiDialog.height(),s.width=i.uiDialog.width(),s.position={my:"left top",at:"left"+(r>=0?"+":"")+r+" "+"top"+(h>=0?"+":"")+h,of:i.window},e(this).removeClass("ui-dialog-resizing"),i._unblockFrames(),i._trigger("resizeStop",n,t(a))}}).css("position",a)},_trackFocus:function(){this._on(this.widget(),{focusin:function(t){this._makeFocusTarget(),this._focusedElement=e(t.target)}})},_makeFocusTarget:function(){this._untrackInstance(),this._trackingInstances().unshift(this)},_untrackInstance:function(){var t=this._trackingInstances(),i=e.inArray(this,t);-1!==i&&t.splice(i,1)},_trackingInstances:function(){var e=this.document.data("ui-dialog-instances");return e||(e=[],this.document.data("ui-dialog-instances",e)),e},_minHeight:function(){var e=this.options;return"auto"===e.height?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(){var e=this.uiDialog.is(":visible");e||this.uiDialog.show(),this.uiDialog.position(this.options.position),e||this.uiDialog.hide()},_setOptions:function(t){var i=this,s=!1,n={};e.each(t,function(e,t){i._setOption(e,t),e in i.sizeRelatedOptions&&(s=!0),e in i.resizableRelatedOptions&&(n[e]=t)}),s&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",n)},_setOption:function(e,t){var i,s,n=this.uiDialog;"dialogClass"===e&&n.removeClass(this.options.dialogClass).addClass(t),"disabled"!==e&&(this._super(e,t),"appendTo"===e&&this.uiDialog.appendTo(this._appendTo()),"buttons"===e&&this._createButtons(),"closeText"===e&&this.uiDialogTitlebarClose.button({label:""+t}),"draggable"===e&&(i=n.is(":data(ui-draggable)"),i&&!t&&n.draggable("destroy"),!i&&t&&this._makeDraggable()),"position"===e&&this._position(),"resizable"===e&&(s=n.is(":data(ui-resizable)"),s&&!t&&n.resizable("destroy"),s&&"string"==typeof t&&n.resizable("option","handles",t),s||t===!1||this._makeResizable()),"title"===e&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")))},_size:function(){var e,t,i,s=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),s.minWidth>s.width&&(s.width=s.minWidth),e=this.uiDialog.css({height:"auto",width:s.width}).outerHeight(),t=Math.max(0,s.minHeight-e),i="number"==typeof s.maxHeight?Math.max(0,s.maxHeight-e):"none","auto"===s.height?this.element.css({minHeight:t,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,s.height-e)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var t=e(this);return e("
").css({position:"absolute",width:t.outerWidth(),height:t.outerHeight()}).appendTo(t.parent()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(t){return e(t.target).closest(".ui-dialog").length?!0:!!e(t.target).closest(".ui-datepicker").length},_createOverlay:function(){if(this.options.modal){var t=!0;this._delay(function(){t=!1}),this.document.data("ui-dialog-overlays")||this._on(this.document,{focusin:function(e){t||this._allowInteraction(e)||(e.preventDefault(),this._trackingInstances()[0]._focusTabbable())}}),this.overlay=e("
").addClass("ui-widget-overlay ui-front").appendTo(this._appendTo()),this._on(this.overlay,{mousedown:"_keepFocus"}),this.document.data("ui-dialog-overlays",(this.document.data("ui-dialog-overlays")||0)+1)}},_destroyOverlay:function(){if(this.options.modal&&this.overlay){var e=this.document.data("ui-dialog-overlays")-1;e?this.document.data("ui-dialog-overlays",e):this.document.unbind("focusin").removeData("ui-dialog-overlays"),this.overlay.remove(),this.overlay=null}}}),e.widget("ui.droppable",{version:"1.11.2",widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var t,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=e.isFunction(s)?s:function(e){return e.is(s)},this.proportions=function(){return arguments.length?(t=arguments[0],void 0):t?t:t={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},this._addToManager(i.scope),i.addClasses&&this.element.addClass("ui-droppable")},_addToManager:function(t){e.ui.ddmanager.droppables[t]=e.ui.ddmanager.droppables[t]||[],e.ui.ddmanager.droppables[t].push(this)},_splice:function(e){for(var t=0;e.length>t;t++)e[t]===this&&e.splice(t,1)},_destroy:function(){var t=e.ui.ddmanager.droppables[this.options.scope];this._splice(t),this.element.removeClass("ui-droppable ui-droppable-disabled")},_setOption:function(t,i){if("accept"===t)this.accept=e.isFunction(i)?i:function(e){return e.is(i)};else if("scope"===t){var s=e.ui.ddmanager.droppables[this.options.scope];this._splice(s),this._addToManager(i)}this._super(t,i)},_activate:function(t){var i=e.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),i&&this._trigger("activate",t,this.ui(i))},_deactivate:function(t){var i=e.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),i&&this._trigger("deactivate",t,this.ui(i))},_over:function(t){var i=e.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",t,this.ui(i)))},_out:function(t){var i=e.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",t,this.ui(i)))},_drop:function(t,i){var s=i||e.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var i=e(this).droppable("instance");return i.options.greedy&&!i.options.disabled&&i.options.scope===s.options.scope&&i.accept.call(i.element[0],s.currentItem||s.element)&&e.ui.intersect(s,e.extend(i,{offset:i.element.offset()}),i.options.tolerance,t)?(n=!0,!1):void 0}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",t,this.ui(s)),this.element):!1):!1},ui:function(e){return{draggable:e.currentItem||e.element,helper:e.helper,position:e.position,offset:e.positionAbs}}}),e.ui.intersect=function(){function e(e,t,i){return e>=t&&t+i>e}return function(t,i,s,n){if(!i.offset)return!1;var a=(t.positionAbs||t.position.absolute).left+t.margins.left,o=(t.positionAbs||t.position.absolute).top+t.margins.top,r=a+t.helperProportions.width,h=o+t.helperProportions.height,l=i.offset.left,u=i.offset.top,d=l+i.proportions().width,c=u+i.proportions().height;switch(s){case"fit":return a>=l&&d>=r&&o>=u&&c>=h;case"intersect":return a+t.helperProportions.width/2>l&&d>r-t.helperProportions.width/2&&o+t.helperProportions.height/2>u&&c>h-t.helperProportions.height/2;case"pointer":return e(n.pageY,u,i.proportions().height)&&e(n.pageX,l,i.proportions().width);case"touch":return(o>=u&&c>=o||h>=u&&c>=h||u>o&&h>c)&&(a>=l&&d>=a||r>=l&&d>=r||l>a&&r>d);default:return!1}}}(),e.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(t,i){var s,n,a=e.ui.ddmanager.droppables[t.options.scope]||[],o=i?i.type:null,r=(t.currentItem||t.element).find(":data(ui-droppable)").addBack();e:for(s=0;a.length>s;s++)if(!(a[s].options.disabled||t&&!a[s].accept.call(a[s].element[0],t.currentItem||t.element))){for(n=0;r.length>n;n++)if(r[n]===a[s].element[0]){a[s].proportions().height=0;continue e}a[s].visible="none"!==a[s].element.css("display"),a[s].visible&&("mousedown"===o&&a[s]._activate.call(a[s],i),a[s].offset=a[s].element.offset(),a[s].proportions({width:a[s].element[0].offsetWidth,height:a[s].element[0].offsetHeight}))}},drop:function(t,i){var s=!1;return e.each((e.ui.ddmanager.droppables[t.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&e.ui.intersect(t,this,this.options.tolerance,i)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],t.currentItem||t.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(t,i){t.element.parentsUntil("body").bind("scroll.droppable",function(){t.options.refreshPositions||e.ui.ddmanager.prepareOffsets(t,i)})},drag:function(t,i){t.options.refreshPositions&&e.ui.ddmanager.prepareOffsets(t,i),e.each(e.ui.ddmanager.droppables[t.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,a,o=e.ui.intersect(t,this,this.options.tolerance,i),r=!o&&this.isover?"isout":o&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,a=this.element.parents(":data(ui-droppable)").filter(function(){return e(this).droppable("instance").options.scope===n}),a.length&&(s=e(a[0]).droppable("instance"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(t,i){t.element.parentsUntil("body").unbind("scroll.droppable"),t.options.refreshPositions||e.ui.ddmanager.prepareOffsets(t,i)}},e.ui.droppable;var y="ui-effects-",b=e;e.effects={effect:{}},function(e,t){function i(e,t,i){var s=d[t.type]||{};return null==e?i||!t.def?null:t.def:(e=s.floor?~~e:parseFloat(e),isNaN(e)?t.def:s.mod?(e+s.mod)%s.mod:0>e?0:e>s.max?s.max:e)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(e,a){var o,r=a.re.exec(i),h=r&&a.parse(r),l=a.space||"rgba";return h?(o=s[l](h),s[u[l].cache]=o[u[l].cache],n=s._rgba=o._rgba,!1):t}),n.length?("0,0,0,0"===n.join()&&e.extend(n,a.transparent),s):a[i]}function n(e,t,i){return i=(i+1)%1,1>6*i?e+6*(t-e)*i:1>2*i?t:2>3*i?e+6*(t-e)*(2/3-i):e}var a,o="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(e){return[e[1],e[2],e[3],e[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(e){return[2.55*e[1],2.55*e[2],2.55*e[3],e[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(e){return[parseInt(e[1],16),parseInt(e[2],16),parseInt(e[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(e){return[parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16),parseInt(e[3]+e[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(e){return[e[1],e[2]/100,e[3]/100,e[4]]}}],l=e.Color=function(t,i,s,n){return new e.Color.fn.parse(t,i,s,n)},u={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},d={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},c=l.support={},p=e("

")[0],f=e.each;p.style.cssText="background-color:rgba(1,1,1,.5)",c.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(u,function(e,t){t.cache="_"+e,t.props.alpha={idx:3,type:"percent",def:1}}),l.fn=e.extend(l.prototype,{parse:function(n,o,r,h){if(n===t)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=e(n).css(o),o=t);var d=this,c=e.type(n),p=this._rgba=[];return o!==t&&(n=[n,o,r,h],c="array"),"string"===c?this.parse(s(n)||a._default):"array"===c?(f(u.rgba.props,function(e,t){p[t.idx]=i(n[t.idx],t)}),this):"object"===c?(n instanceof l?f(u,function(e,t){n[t.cache]&&(d[t.cache]=n[t.cache].slice())}):f(u,function(t,s){var a=s.cache;f(s.props,function(e,t){if(!d[a]&&s.to){if("alpha"===e||null==n[e])return;d[a]=s.to(d._rgba)}d[a][t.idx]=i(n[e],t,!0)}),d[a]&&0>e.inArray(null,d[a].slice(0,3))&&(d[a][3]=1,s.from&&(d._rgba=s.from(d[a])))}),this):t},is:function(e){var i=l(e),s=!0,n=this;return f(u,function(e,a){var o,r=i[a.cache];return r&&(o=n[a.cache]||a.to&&a.to(n._rgba)||[],f(a.props,function(e,i){return null!=r[i.idx]?s=r[i.idx]===o[i.idx]:t})),s}),s},_space:function(){var e=[],t=this;return f(u,function(i,s){t[s.cache]&&e.push(i)}),e.pop()},transition:function(e,t){var s=l(e),n=s._space(),a=u[n],o=0===this.alpha()?l("transparent"):this,r=o[a.cache]||a.to(o._rgba),h=r.slice();return s=s[a.cache],f(a.props,function(e,n){var a=n.idx,o=r[a],l=s[a],u=d[n.type]||{};null!==l&&(null===o?h[a]=l:(u.mod&&(l-o>u.mod/2?o+=u.mod:o-l>u.mod/2&&(o-=u.mod)),h[a]=i((l-o)*t+o,n)))}),this[n](h)},blend:function(t){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(t)._rgba;return l(e.map(i,function(e,t){return(1-s)*n[t]+s*e}))},toRgbaString:function(){var t="rgba(",i=e.map(this._rgba,function(e,t){return null==e?t>2?1:0:e});return 1===i[3]&&(i.pop(),t="rgb("),t+i.join()+")"},toHslaString:function(){var t="hsla(",i=e.map(this.hsla(),function(e,t){return null==e&&(e=t>2?1:0),t&&3>t&&(e=Math.round(100*e)+"%"),e});return 1===i[3]&&(i.pop(),t="hsl("),t+i.join()+")"},toHexString:function(t){var i=this._rgba.slice(),s=i.pop();return t&&i.push(~~(255*s)),"#"+e.map(i,function(e){return e=(e||0).toString(16),1===e.length?"0"+e:e}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,u.hsla.to=function(e){if(null==e[0]||null==e[1]||null==e[2])return[null,null,null,e[3]];var t,i,s=e[0]/255,n=e[1]/255,a=e[2]/255,o=e[3],r=Math.max(s,n,a),h=Math.min(s,n,a),l=r-h,u=r+h,d=.5*u;return t=h===r?0:s===r?60*(n-a)/l+360:n===r?60*(a-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=d?l/u:l/(2-u),[Math.round(t)%360,i,d,null==o?1:o]},u.hsla.from=function(e){if(null==e[0]||null==e[1]||null==e[2])return[null,null,null,e[3]];var t=e[0]/360,i=e[1],s=e[2],a=e[3],o=.5>=s?s*(1+i):s+i-s*i,r=2*s-o;return[Math.round(255*n(r,o,t+1/3)),Math.round(255*n(r,o,t)),Math.round(255*n(r,o,t-1/3)),a]},f(u,function(s,n){var a=n.props,o=n.cache,h=n.to,u=n.from;l.fn[s]=function(s){if(h&&!this[o]&&(this[o]=h(this._rgba)),s===t)return this[o].slice();var n,r=e.type(s),d="array"===r||"object"===r?s:arguments,c=this[o].slice();return f(a,function(e,t){var s=d["object"===r?e:t.idx];null==s&&(s=c[t.idx]),c[t.idx]=i(s,t)}),u?(n=l(u(c)),n[o]=c,n):l(c)},f(a,function(t,i){l.fn[t]||(l.fn[t]=function(n){var a,o=e.type(n),h="alpha"===t?this._hsla?"hsla":"rgba":s,l=this[h](),u=l[i.idx];return"undefined"===o?u:("function"===o&&(n=n.call(this,u),o=e.type(n)),null==n&&i.empty?this:("string"===o&&(a=r.exec(n),a&&(n=u+parseFloat(a[2])*("+"===a[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(t){var i=t.split(" ");f(i,function(t,i){e.cssHooks[i]={set:function(t,n){var a,o,r="";if("transparent"!==n&&("string"!==e.type(n)||(a=s(n)))){if(n=l(a||n),!c.rgba&&1!==n._rgba[3]){for(o="backgroundColor"===i?t.parentNode:t;(""===r||"transparent"===r)&&o&&o.style;)try{r=e.css(o,"backgroundColor"),o=o.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{t.style[i]=n}catch(h){}}},e.fx.step[i]=function(t){t.colorInit||(t.start=l(t.elem,i),t.end=l(t.end),t.colorInit=!0),e.cssHooks[i].set(t.elem,t.start.transition(t.end,t.pos))}})},l.hook(o),e.cssHooks.borderColor={expand:function(e){var t={};return f(["Top","Right","Bottom","Left"],function(i,s){t["border"+s+"Color"]=e}),t}},a=e.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(b),function(){function t(t){var i,s,n=t.ownerDocument.defaultView?t.ownerDocument.defaultView.getComputedStyle(t,null):t.currentStyle,a={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(a[e.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(a[i]=n[i]);return a}function i(t,i){var s,a,o={};for(s in i)a=i[s],t[s]!==a&&(n[s]||(e.fx.step[s]||!isNaN(parseFloat(a)))&&(o[s]=a));return o}var s=["add","remove","toggle"],n={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};e.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(t,i){e.fx.step[i]=function(e){("none"!==e.end&&!e.setAttr||1===e.pos&&!e.setAttr)&&(b.style(e.elem,i,e.end),e.setAttr=!0)}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e.effects.animateClass=function(n,a,o,r){var h=e.speed(a,o,r);return this.queue(function(){var a,o=e(this),r=o.attr("class")||"",l=h.children?o.find("*").addBack():o;l=l.map(function(){var i=e(this);return{el:i,start:t(this)}}),a=function(){e.each(s,function(e,t){n[t]&&o[t+"Class"](n[t])})},a(),l=l.map(function(){return this.end=t(this.el[0]),this.diff=i(this.start,this.end),this}),o.attr("class",r),l=l.map(function(){var t=this,i=e.Deferred(),s=e.extend({},h,{queue:!1,complete:function(){i.resolve(t)}});return this.el.animate(this.diff,s),i.promise()}),e.when.apply(e,l.get()).done(function(){a(),e.each(arguments,function(){var t=this.el;e.each(this.diff,function(e){t.css(e,"")})}),h.complete.call(o[0])})})},e.fn.extend({addClass:function(t){return function(i,s,n,a){return s?e.effects.animateClass.call(this,{add:i},s,n,a):t.apply(this,arguments)}}(e.fn.addClass),removeClass:function(t){return function(i,s,n,a){return arguments.length>1?e.effects.animateClass.call(this,{remove:i},s,n,a):t.apply(this,arguments)}}(e.fn.removeClass),toggleClass:function(t){return function(i,s,n,a,o){return"boolean"==typeof s||void 0===s?n?e.effects.animateClass.call(this,s?{add:i}:{remove:i},n,a,o):t.apply(this,arguments):e.effects.animateClass.call(this,{toggle:i},s,n,a)}}(e.fn.toggleClass),switchClass:function(t,i,s,n,a){return e.effects.animateClass.call(this,{add:i,remove:t},s,n,a)}})}(),function(){function t(t,i,s,n){return e.isPlainObject(t)&&(i=t,t=t.effect),t={effect:t},null==i&&(i={}),e.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||e.fx.speeds[i])&&(n=s,s=i,i={}),e.isFunction(s)&&(n=s,s=null),i&&e.extend(t,i),s=s||i.duration,t.duration=e.fx.off?0:"number"==typeof s?s:s in e.fx.speeds?e.fx.speeds[s]:e.fx.speeds._default,t.complete=n||i.complete,t}function i(t){return!t||"number"==typeof t||e.fx.speeds[t]?!0:"string"!=typeof t||e.effects.effect[t]?e.isFunction(t)?!0:"object"!=typeof t||t.effect?!1:!0:!0}e.extend(e.effects,{version:"1.11.2",save:function(e,t){for(var i=0;t.length>i;i++)null!==t[i]&&e.data(y+t[i],e[0].style[t[i]])},restore:function(e,t){var i,s;for(s=0;t.length>s;s++)null!==t[s]&&(i=e.data(y+t[s]),void 0===i&&(i=""),e.css(t[s],i))},setMode:function(e,t){return"toggle"===t&&(t=e.is(":hidden")?"show":"hide"),t},getBaseline:function(e,t){var i,s;switch(e[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=e[0]/t.height}switch(e[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=e[1]/t.width}return{x:s,y:i}},createWrapper:function(t){if(t.parent().is(".ui-effects-wrapper"))return t.parent();var i={width:t.outerWidth(!0),height:t.outerHeight(!0),"float":t.css("float")},s=e("

").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:t.width(),height:t.height()},a=document.activeElement;try{a.id}catch(o){a=document.body}return t.wrap(s),(t[0]===a||e.contains(t[0],a))&&e(a).focus(),s=t.parent(),"static"===t.css("position")?(s.css({position:"relative"}),t.css({position:"relative"})):(e.extend(i,{position:t.css("position"),zIndex:t.css("z-index")}),e.each(["top","left","bottom","right"],function(e,s){i[s]=t.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),t.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),t.css(n),s.css(i).show()},removeWrapper:function(t){var i=document.activeElement;return t.parent().is(".ui-effects-wrapper")&&(t.parent().replaceWith(t),(t[0]===i||e.contains(t[0],i))&&e(i).focus()),t},setTransition:function(t,i,s,n){return n=n||{},e.each(i,function(e,i){var a=t.cssUnit(i);a[0]>0&&(n[i]=a[0]*s+a[1])}),n}}),e.fn.extend({effect:function(){function i(t){function i(){e.isFunction(a)&&a.call(n[0]),e.isFunction(t)&&t()}var n=e(this),a=s.complete,r=s.mode;(n.is(":hidden")?"hide"===r:"show"===r)?(n[r](),i()):o.call(n[0],s,i)}var s=t.apply(this,arguments),n=s.mode,a=s.queue,o=e.effects.effect[s.effect];return e.fx.off||!o?n?this[n](s.duration,s.complete):this.each(function(){s.complete&&s.complete.call(this)}):a===!1?this.each(i):this.queue(a||"fx",i)},show:function(e){return function(s){if(i(s))return e.apply(this,arguments);var n=t.apply(this,arguments);return n.mode="show",this.effect.call(this,n)}}(e.fn.show),hide:function(e){return function(s){if(i(s))return e.apply(this,arguments);var n=t.apply(this,arguments);return n.mode="hide",this.effect.call(this,n)}}(e.fn.hide),toggle:function(e){return function(s){if(i(s)||"boolean"==typeof s)return e.apply(this,arguments);var n=t.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)}}(e.fn.toggle),cssUnit:function(t){var i=this.css(t),s=[];return e.each(["em","px","%","pt"],function(e,t){i.indexOf(t)>0&&(s=[parseFloat(i),t])}),s}})}(),function(){var t={};e.each(["Quad","Cubic","Quart","Quint","Expo"],function(e,i){t[i]=function(t){return Math.pow(t,e+2)}}),e.extend(t,{Sine:function(e){return 1-Math.cos(e*Math.PI/2)},Circ:function(e){return 1-Math.sqrt(1-e*e)},Elastic:function(e){return 0===e||1===e?e:-Math.pow(2,8*(e-1))*Math.sin((80*(e-1)-7.5)*Math.PI/15)},Back:function(e){return e*e*(3*e-2)},Bounce:function(e){for(var t,i=4;((t=Math.pow(2,--i))-1)/11>e;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*t-2)/22-e,2)}}),e.each(t,function(t,i){e.easing["easeIn"+t]=i,e.easing["easeOut"+t]=function(e){return 1-i(1-e)},e.easing["easeInOut"+t]=function(e){return.5>e?i(2*e)/2:1-i(-2*e+2)/2}})}(),e.effects,e.effects.effect.blind=function(t,i){var s,n,a,o=e(this),r=/up|down|vertical/,h=/up|left|vertical|horizontal/,l=["position","top","bottom","left","right","height","width"],u=e.effects.setMode(o,t.mode||"hide"),d=t.direction||"up",c=r.test(d),p=c?"height":"width",f=c?"top":"left",m=h.test(d),g={},v="show"===u;o.parent().is(".ui-effects-wrapper")?e.effects.save(o.parent(),l):e.effects.save(o,l),o.show(),s=e.effects.createWrapper(o).css({overflow:"hidden"}),n=s[p](),a=parseFloat(s.css(f))||0,g[p]=v?n:0,m||(o.css(c?"bottom":"right",0).css(c?"top":"left","auto").css({position:"absolute"}),g[f]=v?a:n+a),v&&(s.css(p,0),m||s.css(f,a+n)),s.animate(g,{duration:t.duration,easing:t.easing,queue:!1,complete:function(){"hide"===u&&o.hide(),e.effects.restore(o,l),e.effects.removeWrapper(o),i()}})},e.effects.effect.bounce=function(t,i){var s,n,a,o=e(this),r=["position","top","bottom","left","right","height","width"],h=e.effects.setMode(o,t.mode||"effect"),l="hide"===h,u="show"===h,d=t.direction||"up",c=t.distance,p=t.times||5,f=2*p+(u||l?1:0),m=t.duration/f,g=t.easing,v="up"===d||"down"===d?"top":"left",y="up"===d||"left"===d,b=o.queue(),_=b.length;for((u||l)&&r.push("opacity"),e.effects.save(o,r),o.show(),e.effects.createWrapper(o),c||(c=o["top"===v?"outerHeight":"outerWidth"]()/3),u&&(a={opacity:1},a[v]=0,o.css("opacity",0).css(v,y?2*-c:2*c).animate(a,m,g)),l&&(c/=Math.pow(2,p-1)),a={},a[v]=0,s=0;p>s;s++)n={},n[v]=(y?"-=":"+=")+c,o.animate(n,m,g).animate(a,m,g),c=l?2*c:c/2;l&&(n={opacity:0},n[v]=(y?"-=":"+=")+c,o.animate(n,m,g)),o.queue(function(){l&&o.hide(),e.effects.restore(o,r),e.effects.removeWrapper(o),i()}),_>1&&b.splice.apply(b,[1,0].concat(b.splice(_,f+1))),o.dequeue()},e.effects.effect.clip=function(t,i){var s,n,a,o=e(this),r=["position","top","bottom","left","right","height","width"],h=e.effects.setMode(o,t.mode||"hide"),l="show"===h,u=t.direction||"vertical",d="vertical"===u,c=d?"height":"width",p=d?"top":"left",f={};e.effects.save(o,r),o.show(),s=e.effects.createWrapper(o).css({overflow:"hidden"}),n="IMG"===o[0].tagName?s:o,a=n[c](),l&&(n.css(c,0),n.css(p,a/2)),f[c]=l?a:0,f[p]=l?0:a/2,n.animate(f,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){l||o.hide(),e.effects.restore(o,r),e.effects.removeWrapper(o),i()}})},e.effects.effect.drop=function(t,i){var s,n=e(this),a=["position","top","bottom","left","right","opacity","height","width"],o=e.effects.setMode(n,t.mode||"hide"),r="show"===o,h=t.direction||"left",l="up"===h||"down"===h?"top":"left",u="up"===h||"left"===h?"pos":"neg",d={opacity:r?1:0};e.effects.save(n,a),n.show(),e.effects.createWrapper(n),s=t.distance||n["top"===l?"outerHeight":"outerWidth"](!0)/2,r&&n.css("opacity",0).css(l,"pos"===u?-s:s),d[l]=(r?"pos"===u?"+=":"-=":"pos"===u?"-=":"+=")+s,n.animate(d,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){"hide"===o&&n.hide(),e.effects.restore(n,a),e.effects.removeWrapper(n),i()}})},e.effects.effect.explode=function(t,i){function s(){b.push(this),b.length===d*c&&n()}function n(){p.css({visibility:"visible"}),e(b).remove(),m||p.hide(),i()}var a,o,r,h,l,u,d=t.pieces?Math.round(Math.sqrt(t.pieces)):3,c=d,p=e(this),f=e.effects.setMode(p,t.mode||"hide"),m="show"===f,g=p.show().css("visibility","hidden").offset(),v=Math.ceil(p.outerWidth()/c),y=Math.ceil(p.outerHeight()/d),b=[];for(a=0;d>a;a++)for(h=g.top+a*y,u=a-(d-1)/2,o=0;c>o;o++)r=g.left+o*v,l=o-(c-1)/2,p.clone().appendTo("body").wrap("
").css({position:"absolute",visibility:"visible",left:-o*v,top:-a*y}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:v,height:y,left:r+(m?l*v:0),top:h+(m?u*y:0),opacity:m?0:1}).animate({left:r+(m?0:l*v),top:h+(m?0:u*y),opacity:m?1:0},t.duration||500,t.easing,s)},e.effects.effect.fade=function(t,i){var s=e(this),n=e.effects.setMode(s,t.mode||"toggle");s.animate({opacity:n},{queue:!1,duration:t.duration,easing:t.easing,complete:i})},e.effects.effect.fold=function(t,i){var s,n,a=e(this),o=["position","top","bottom","left","right","height","width"],r=e.effects.setMode(a,t.mode||"hide"),h="show"===r,l="hide"===r,u=t.size||15,d=/([0-9]+)%/.exec(u),c=!!t.horizFirst,p=h!==c,f=p?["width","height"]:["height","width"],m=t.duration/2,g={},v={};e.effects.save(a,o),a.show(),s=e.effects.createWrapper(a).css({overflow:"hidden"}),n=p?[s.width(),s.height()]:[s.height(),s.width()],d&&(u=parseInt(d[1],10)/100*n[l?0:1]),h&&s.css(c?{height:0,width:u}:{height:u,width:0}),g[f[0]]=h?n[0]:u,v[f[1]]=h?n[1]:0,s.animate(g,m,t.easing).animate(v,m,t.easing,function(){l&&a.hide(),e.effects.restore(a,o),e.effects.removeWrapper(a),i()})},e.effects.effect.highlight=function(t,i){var s=e(this),n=["backgroundImage","backgroundColor","opacity"],a=e.effects.setMode(s,t.mode||"show"),o={backgroundColor:s.css("backgroundColor")};"hide"===a&&(o.opacity=0),e.effects.save(s,n),s.show().css({backgroundImage:"none",backgroundColor:t.color||"#ffff99"}).animate(o,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){"hide"===a&&s.hide(),e.effects.restore(s,n),i()}})},e.effects.effect.size=function(t,i){var s,n,a,o=e(this),r=["position","top","bottom","left","right","width","height","overflow","opacity"],h=["position","top","bottom","left","right","overflow","opacity"],l=["width","height","overflow"],u=["fontSize"],d=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],c=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=e.effects.setMode(o,t.mode||"effect"),f=t.restore||"effect"!==p,m=t.scale||"both",g=t.origin||["middle","center"],v=o.css("position"),y=f?r:h,b={height:0,width:0,outerHeight:0,outerWidth:0};"show"===p&&o.show(),s={height:o.height(),width:o.width(),outerHeight:o.outerHeight(),outerWidth:o.outerWidth()},"toggle"===t.mode&&"show"===p?(o.from=t.to||b,o.to=t.from||s):(o.from=t.from||("show"===p?b:s),o.to=t.to||("hide"===p?b:s)),a={from:{y:o.from.height/s.height,x:o.from.width/s.width},to:{y:o.to.height/s.height,x:o.to.width/s.width}},("box"===m||"both"===m)&&(a.from.y!==a.to.y&&(y=y.concat(d),o.from=e.effects.setTransition(o,d,a.from.y,o.from),o.to=e.effects.setTransition(o,d,a.to.y,o.to)),a.from.x!==a.to.x&&(y=y.concat(c),o.from=e.effects.setTransition(o,c,a.from.x,o.from),o.to=e.effects.setTransition(o,c,a.to.x,o.to))),("content"===m||"both"===m)&&a.from.y!==a.to.y&&(y=y.concat(u).concat(l),o.from=e.effects.setTransition(o,u,a.from.y,o.from),o.to=e.effects.setTransition(o,u,a.to.y,o.to)),e.effects.save(o,y),o.show(),e.effects.createWrapper(o),o.css("overflow","hidden").css(o.from),g&&(n=e.effects.getBaseline(g,s),o.from.top=(s.outerHeight-o.outerHeight())*n.y,o.from.left=(s.outerWidth-o.outerWidth())*n.x,o.to.top=(s.outerHeight-o.to.outerHeight)*n.y,o.to.left=(s.outerWidth-o.to.outerWidth)*n.x),o.css(o.from),("content"===m||"both"===m)&&(d=d.concat(["marginTop","marginBottom"]).concat(u),c=c.concat(["marginLeft","marginRight"]),l=r.concat(d).concat(c),o.find("*[width]").each(function(){var i=e(this),s={height:i.height(),width:i.width(),outerHeight:i.outerHeight(),outerWidth:i.outerWidth()}; +f&&e.effects.save(i,l),i.from={height:s.height*a.from.y,width:s.width*a.from.x,outerHeight:s.outerHeight*a.from.y,outerWidth:s.outerWidth*a.from.x},i.to={height:s.height*a.to.y,width:s.width*a.to.x,outerHeight:s.height*a.to.y,outerWidth:s.width*a.to.x},a.from.y!==a.to.y&&(i.from=e.effects.setTransition(i,d,a.from.y,i.from),i.to=e.effects.setTransition(i,d,a.to.y,i.to)),a.from.x!==a.to.x&&(i.from=e.effects.setTransition(i,c,a.from.x,i.from),i.to=e.effects.setTransition(i,c,a.to.x,i.to)),i.css(i.from),i.animate(i.to,t.duration,t.easing,function(){f&&e.effects.restore(i,l)})})),o.animate(o.to,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){0===o.to.opacity&&o.css("opacity",o.from.opacity),"hide"===p&&o.hide(),e.effects.restore(o,y),f||("static"===v?o.css({position:"relative",top:o.to.top,left:o.to.left}):e.each(["top","left"],function(e,t){o.css(t,function(t,i){var s=parseInt(i,10),n=e?o.to.left:o.to.top;return"auto"===i?n+"px":s+n+"px"})})),e.effects.removeWrapper(o),i()}})},e.effects.effect.scale=function(t,i){var s=e(this),n=e.extend(!0,{},t),a=e.effects.setMode(s,t.mode||"effect"),o=parseInt(t.percent,10)||(0===parseInt(t.percent,10)?0:"hide"===a?0:100),r=t.direction||"both",h=t.origin,l={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()},u={y:"horizontal"!==r?o/100:1,x:"vertical"!==r?o/100:1};n.effect="size",n.queue=!1,n.complete=i,"effect"!==a&&(n.origin=h||["middle","center"],n.restore=!0),n.from=t.from||("show"===a?{height:0,width:0,outerHeight:0,outerWidth:0}:l),n.to={height:l.height*u.y,width:l.width*u.x,outerHeight:l.outerHeight*u.y,outerWidth:l.outerWidth*u.x},n.fade&&("show"===a&&(n.from.opacity=0,n.to.opacity=1),"hide"===a&&(n.from.opacity=1,n.to.opacity=0)),s.effect(n)},e.effects.effect.puff=function(t,i){var s=e(this),n=e.effects.setMode(s,t.mode||"hide"),a="hide"===n,o=parseInt(t.percent,10)||150,r=o/100,h={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()};e.extend(t,{effect:"scale",queue:!1,fade:!0,mode:n,complete:i,percent:a?o:100,from:a?h:{height:h.height*r,width:h.width*r,outerHeight:h.outerHeight*r,outerWidth:h.outerWidth*r}}),s.effect(t)},e.effects.effect.pulsate=function(t,i){var s,n=e(this),a=e.effects.setMode(n,t.mode||"show"),o="show"===a,r="hide"===a,h=o||"hide"===a,l=2*(t.times||5)+(h?1:0),u=t.duration/l,d=0,c=n.queue(),p=c.length;for((o||!n.is(":visible"))&&(n.css("opacity",0).show(),d=1),s=1;l>s;s++)n.animate({opacity:d},u,t.easing),d=1-d;n.animate({opacity:d},u,t.easing),n.queue(function(){r&&n.hide(),i()}),p>1&&c.splice.apply(c,[1,0].concat(c.splice(p,l+1))),n.dequeue()},e.effects.effect.shake=function(t,i){var s,n=e(this),a=["position","top","bottom","left","right","height","width"],o=e.effects.setMode(n,t.mode||"effect"),r=t.direction||"left",h=t.distance||20,l=t.times||3,u=2*l+1,d=Math.round(t.duration/u),c="up"===r||"down"===r?"top":"left",p="up"===r||"left"===r,f={},m={},g={},v=n.queue(),y=v.length;for(e.effects.save(n,a),n.show(),e.effects.createWrapper(n),f[c]=(p?"-=":"+=")+h,m[c]=(p?"+=":"-=")+2*h,g[c]=(p?"-=":"+=")+2*h,n.animate(f,d,t.easing),s=1;l>s;s++)n.animate(m,d,t.easing).animate(g,d,t.easing);n.animate(m,d,t.easing).animate(f,d/2,t.easing).queue(function(){"hide"===o&&n.hide(),e.effects.restore(n,a),e.effects.removeWrapper(n),i()}),y>1&&v.splice.apply(v,[1,0].concat(v.splice(y,u+1))),n.dequeue()},e.effects.effect.slide=function(t,i){var s,n=e(this),a=["position","top","bottom","left","right","width","height"],o=e.effects.setMode(n,t.mode||"show"),r="show"===o,h=t.direction||"left",l="up"===h||"down"===h?"top":"left",u="up"===h||"left"===h,d={};e.effects.save(n,a),n.show(),s=t.distance||n["top"===l?"outerHeight":"outerWidth"](!0),e.effects.createWrapper(n).css({overflow:"hidden"}),r&&n.css(l,u?isNaN(s)?"-"+s:-s:s),d[l]=(r?u?"+=":"-=":u?"-=":"+=")+s,n.animate(d,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){"hide"===o&&n.hide(),e.effects.restore(n,a),e.effects.removeWrapper(n),i()}})},e.effects.effect.transfer=function(t,i){var s=e(this),n=e(t.to),a="fixed"===n.css("position"),o=e("body"),r=a?o.scrollTop():0,h=a?o.scrollLeft():0,l=n.offset(),u={top:l.top-r,left:l.left-h,height:n.innerHeight(),width:n.innerWidth()},d=s.offset(),c=e("
").appendTo(document.body).addClass(t.className).css({top:d.top-r,left:d.left-h,height:s.innerHeight(),width:s.innerWidth(),position:a?"fixed":"absolute"}).animate(u,t.duration,t.easing,function(){c.remove(),i()})},e.widget("ui.progressbar",{version:"1.11.2",options:{max:100,value:0,change:null,complete:null},min:0,_create:function(){this.oldValue=this.options.value=this._constrainedValue(),this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min}),this.valueDiv=e("
").appendTo(this.element),this._refreshValue()},_destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove()},value:function(e){return void 0===e?this.options.value:(this.options.value=this._constrainedValue(e),this._refreshValue(),void 0)},_constrainedValue:function(e){return void 0===e&&(e=this.options.value),this.indeterminate=e===!1,"number"!=typeof e&&(e=0),this.indeterminate?!1:Math.min(this.options.max,Math.max(this.min,e))},_setOptions:function(e){var t=e.value;delete e.value,this._super(e),this.options.value=this._constrainedValue(t),this._refreshValue()},_setOption:function(e,t){"max"===e&&(t=Math.max(this.min,t)),"disabled"===e&&this.element.toggleClass("ui-state-disabled",!!t).attr("aria-disabled",t),this._super(e,t)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var t=this.options.value,i=this._percentage();this.valueDiv.toggle(this.indeterminate||t>this.min).toggleClass("ui-corner-right",t===this.options.max).width(i.toFixed(0)+"%"),this.element.toggleClass("ui-progressbar-indeterminate",this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=e("
").appendTo(this.valueDiv))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":t}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==t&&(this.oldValue=t,this._trigger("change")),t===this.options.max&&this._trigger("complete")}}),e.widget("ui.selectable",e.ui.mouse,{version:"1.11.2",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var t,i=this;this.element.addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){t=e(i.options.filter,i.element[0]),t.addClass("ui-selectee"),t.each(function(){var t=e(this),i=t.offset();e.data(this,"selectable-item",{element:this,$element:t,left:i.left,top:i.top,right:i.left+t.outerWidth(),bottom:i.top+t.outerHeight(),startselected:!1,selected:t.hasClass("ui-selected"),selecting:t.hasClass("ui-selecting"),unselecting:t.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=t.addClass("ui-selectee"),this._mouseInit(),this.helper=e("
")},_destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled"),this._mouseDestroy()},_mouseStart:function(t){var i=this,s=this.options;this.opos=[t.pageX,t.pageY],this.options.disabled||(this.selectees=e(s.filter,this.element[0]),this._trigger("start",t),e(s.appendTo).append(this.helper),this.helper.css({left:t.pageX,top:t.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=e.data(this,"selectable-item");s.startselected=!0,t.metaKey||t.ctrlKey||(s.$element.removeClass("ui-selected"),s.selected=!1,s.$element.addClass("ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",t,{unselecting:s.element}))}),e(t.target).parents().addBack().each(function(){var s,n=e.data(this,"selectable-item");return n?(s=!t.metaKey&&!t.ctrlKey||!n.$element.hasClass("ui-selected"),n.$element.removeClass(s?"ui-unselecting":"ui-selected").addClass(s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",t,{selecting:n.element}):i._trigger("unselecting",t,{unselecting:n.element}),!1):void 0}))},_mouseDrag:function(t){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,a=this.opos[0],o=this.opos[1],r=t.pageX,h=t.pageY;return a>r&&(i=r,r=a,a=i),o>h&&(i=h,h=o,o=i),this.helper.css({left:a,top:o,width:r-a,height:h-o}),this.selectees.each(function(){var i=e.data(this,"selectable-item"),l=!1;i&&i.element!==s.element[0]&&("touch"===n.tolerance?l=!(i.left>r||a>i.right||i.top>h||o>i.bottom):"fit"===n.tolerance&&(l=i.left>a&&r>i.right&&i.top>o&&h>i.bottom),l?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,s._trigger("selecting",t,{selecting:i.element}))):(i.selecting&&((t.metaKey||t.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",t,{unselecting:i.element}))),i.selected&&(t.metaKey||t.ctrlKey||i.startselected||(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",t,{unselecting:i.element})))))}),!1}},_mouseStop:function(t){var i=this;return this.dragged=!1,e(".ui-unselecting",this.element[0]).each(function(){var s=e.data(this,"selectable-item");s.$element.removeClass("ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",t,{unselected:s.element})}),e(".ui-selecting",this.element[0]).each(function(){var s=e.data(this,"selectable-item");s.$element.removeClass("ui-selecting").addClass("ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",t,{selected:s.element})}),this._trigger("stop",t),this.helper.remove(),!1}}),e.widget("ui.selectmenu",{version:"1.11.2",defaultElement:"",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var t={},i=this.element;return e.each(["min","max","step"],function(e,s){var n=i.attr(s);void 0!==n&&n.length&&(t[s]=n)}),t},_events:{keydown:function(e){this._start(e)&&this._keydown(e)&&e.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",e),void 0)},mousewheel:function(e,t){if(t){if(!this.spinning&&!this._start(e))return!1;this._spin((t>0?1:-1)*this.options.step,e),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(e)},100),e.preventDefault()}},"mousedown .ui-spinner-button":function(t){function i(){var e=this.element[0]===this.document[0].activeElement;e||(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),t.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(t)!==!1&&this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(t){return e(t.currentTarget).hasClass("ui-state-active")?this._start(t)===!1?!1:(this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var e=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=e.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(.5*e.height())&&e.height()>0&&e.height(e.height()),this.options.disabled&&this.disable()},_keydown:function(t){var i=this.options,s=e.ui.keyCode;switch(t.keyCode){case s.UP:return this._repeat(null,1,t),!0;case s.DOWN:return this._repeat(null,-1,t),!0;case s.PAGE_UP:return this._repeat(null,i.page,t),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,t),!0}return!1},_uiSpinnerHtml:function(){return""},_buttonHtml:function(){return""+""+""+""+""},_start:function(e){return this.spinning||this._trigger("start",e)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(e,t,i){e=e||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,t,i)},e),this._spin(t*this.options.step,i)},_spin:function(e,t){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+e*this._increment(this.counter)),this.spinning&&this._trigger("spin",t,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(t){var i=this.options.incremental;return i?e.isFunction(i)?i(t):Math.floor(t*t*t/5e4-t*t/500+17*t/200+1):1},_precision:function(){var e=this._precisionOf(this.options.step);return null!==this.options.min&&(e=Math.max(e,this._precisionOf(this.options.min))),e},_precisionOf:function(e){var t=""+e,i=t.indexOf(".");return-1===i?0:t.length-i-1},_adjustValue:function(e){var t,i,s=this.options;return t=null!==s.min?s.min:0,i=e-t,i=Math.round(i/s.step)*s.step,e=t+i,e=parseFloat(e.toFixed(this._precision())),null!==s.max&&e>s.max?s.max:null!==s.min&&s.min>e?s.min:e},_stop:function(e){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",e))},_setOption:function(e,t){if("culture"===e||"numberFormat"===e){var i=this._parse(this.element.val());return this.options[e]=t,this.element.val(this._format(i)),void 0}("max"===e||"min"===e||"step"===e)&&"string"==typeof t&&(t=this._parse(t)),"icons"===e&&(this.buttons.first().find(".ui-icon").removeClass(this.options.icons.up).addClass(t.up),this.buttons.last().find(".ui-icon").removeClass(this.options.icons.down).addClass(t.down)),this._super(e,t),"disabled"===e&&(this.widget().toggleClass("ui-state-disabled",!!t),this.element.prop("disabled",!!t),this.buttons.button(t?"disable":"enable"))},_setOptions:h(function(e){this._super(e)}),_parse:function(e){return"string"==typeof e&&""!==e&&(e=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(e,10,this.options.culture):+e),""===e||isNaN(e)?null:e},_format:function(e){return""===e?"":window.Globalize&&this.options.numberFormat?Globalize.format(e,this.options.numberFormat,this.options.culture):e},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},isValid:function(){var e=this.value();return null===e?!1:e===this._adjustValue(e)},_value:function(e,t){var i;""!==e&&(i=this._parse(e),null!==i&&(t||(i=this._adjustValue(i)),e=this._format(i))),this.element.val(e),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:h(function(e){this._stepUp(e)}),_stepUp:function(e){this._start()&&(this._spin((e||1)*this.options.step),this._stop())},stepDown:h(function(e){this._stepDown(e)}),_stepDown:function(e){this._start()&&(this._spin((e||1)*-this.options.step),this._stop())},pageUp:h(function(e){this._stepUp((e||1)*this.options.page)}),pageDown:h(function(e){this._stepDown((e||1)*this.options.page)}),value:function(e){return arguments.length?(h(this._value).call(this,e),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}}),e.widget("ui.tabs",{version:"1.11.2",delay:300,options:{active:null,collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_isLocal:function(){var e=/#.*$/;return function(t){var i,s;t=t.cloneNode(!1),i=t.href.replace(e,""),s=location.href.replace(e,"");try{i=decodeURIComponent(i)}catch(n){}try{s=decodeURIComponent(s)}catch(n){}return t.hash.length>1&&i===s}}(),_create:function(){var t=this,i=this.options;this.running=!1,this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all").toggleClass("ui-tabs-collapsible",i.collapsible),this._processTabs(),i.active=this._initialActive(),e.isArray(i.disabled)&&(i.disabled=e.unique(i.disabled.concat(e.map(this.tabs.filter(".ui-state-disabled"),function(e){return t.tabs.index(e)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(i.active):e(),this._refresh(),this.active.length&&this.load(i.active)},_initialActive:function(){var t=this.options.active,i=this.options.collapsible,s=location.hash.substring(1);return null===t&&(s&&this.tabs.each(function(i,n){return e(n).attr("aria-controls")===s?(t=i,!1):void 0}),null===t&&(t=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===t||-1===t)&&(t=this.tabs.length?0:!1)),t!==!1&&(t=this.tabs.index(this.tabs.eq(t)),-1===t&&(t=i?!1:0)),!i&&t===!1&&this.anchors.length&&(t=0),t},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):e()}},_tabKeydown:function(t){var i=e(this.document[0].activeElement).closest("li"),s=this.tabs.index(i),n=!0;if(!this._handlePageNav(t)){switch(t.keyCode){case e.ui.keyCode.RIGHT:case e.ui.keyCode.DOWN:s++;break;case e.ui.keyCode.UP:case e.ui.keyCode.LEFT:n=!1,s--;break;case e.ui.keyCode.END:s=this.anchors.length-1;break;case e.ui.keyCode.HOME:s=0;break;case e.ui.keyCode.SPACE:return t.preventDefault(),clearTimeout(this.activating),this._activate(s),void 0;case e.ui.keyCode.ENTER:return t.preventDefault(),clearTimeout(this.activating),this._activate(s===this.options.active?!1:s),void 0;default:return}t.preventDefault(),clearTimeout(this.activating),s=this._focusNextTab(s,n),t.ctrlKey||(i.attr("aria-selected","false"),this.tabs.eq(s).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",s)},this.delay))}},_panelKeydown:function(t){this._handlePageNav(t)||t.ctrlKey&&t.keyCode===e.ui.keyCode.UP&&(t.preventDefault(),this.active.focus())},_handlePageNav:function(t){return t.altKey&&t.keyCode===e.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):t.altKey&&t.keyCode===e.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):void 0},_findNextTab:function(t,i){function s(){return t>n&&(t=0),0>t&&(t=n),t}for(var n=this.tabs.length-1;-1!==e.inArray(s(),this.options.disabled);)t=i?t+1:t-1;return t},_focusNextTab:function(e,t){return e=this._findNextTab(e,t),this.tabs.eq(e).focus(),e},_setOption:function(e,t){return"active"===e?(this._activate(t),void 0):"disabled"===e?(this._setupDisabled(t),void 0):(this._super(e,t),"collapsible"===e&&(this.element.toggleClass("ui-tabs-collapsible",t),t||this.options.active!==!1||this._activate(0)),"event"===e&&this._setupEvents(t),"heightStyle"===e&&this._setupHeightStyle(t),void 0)},_sanitizeSelector:function(e){return e?e.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var t=this.options,i=this.tablist.children(":has(a[href])");t.disabled=e.map(i.filter(".ui-state-disabled"),function(e){return i.index(e)}),this._processTabs(),t.active!==!1&&this.anchors.length?this.active.length&&!e.contains(this.tablist[0],this.active[0])?this.tabs.length===t.disabled.length?(t.active=!1,this.active=e()):this._activate(this._findNextTab(Math.max(0,t.active-1),!1)):t.active=this.tabs.index(this.active):(t.active=!1,this.active=e()),this._refresh()},_refresh:function(){this._setupDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-hidden":"true"}),this.active.length?(this.active.addClass("ui-tabs-active ui-state-active").attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}),this._getPanelForTab(this.active).show().attr({"aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var t=this,i=this.tabs,s=this.anchors,n=this.panels;this.tablist=this._getList().addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").attr("role","tablist").delegate("> li","mousedown"+this.eventNamespace,function(t){e(this).is(".ui-state-disabled")&&t.preventDefault()}).delegate(".ui-tabs-anchor","focus"+this.eventNamespace,function(){e(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this.tabs=this.tablist.find("> li:has(a[href])").addClass("ui-state-default ui-corner-top").attr({role:"tab",tabIndex:-1}),this.anchors=this.tabs.map(function(){return e("a",this)[0] +}).addClass("ui-tabs-anchor").attr({role:"presentation",tabIndex:-1}),this.panels=e(),this.anchors.each(function(i,s){var n,a,o,r=e(s).uniqueId().attr("id"),h=e(s).closest("li"),l=h.attr("aria-controls");t._isLocal(s)?(n=s.hash,o=n.substring(1),a=t.element.find(t._sanitizeSelector(n))):(o=h.attr("aria-controls")||e({}).uniqueId()[0].id,n="#"+o,a=t.element.find(n),a.length||(a=t._createPanel(o),a.insertAfter(t.panels[i-1]||t.tablist)),a.attr("aria-live","polite")),a.length&&(t.panels=t.panels.add(a)),l&&h.data("ui-tabs-aria-controls",l),h.attr({"aria-controls":o,"aria-labelledby":r}),a.attr("aria-labelledby",r)}),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").attr("role","tabpanel"),i&&(this._off(i.not(this.tabs)),this._off(s.not(this.anchors)),this._off(n.not(this.panels)))},_getList:function(){return this.tablist||this.element.find("ol,ul").eq(0)},_createPanel:function(t){return e("
").attr("id",t).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)},_setupDisabled:function(t){e.isArray(t)&&(t.length?t.length===this.anchors.length&&(t=!0):t=!1);for(var i,s=0;i=this.tabs[s];s++)t===!0||-1!==e.inArray(s,t)?e(i).addClass("ui-state-disabled").attr("aria-disabled","true"):e(i).removeClass("ui-state-disabled").removeAttr("aria-disabled");this.options.disabled=t},_setupEvents:function(t){var i={};t&&e.each(t.split(" "),function(e,t){i[t]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(!0,this.anchors,{click:function(e){e.preventDefault()}}),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(t){var i,s=this.element.parent();"fill"===t?(i=s.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var t=e(this),s=t.css("position");"absolute"!==s&&"fixed"!==s&&(i-=t.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=e(this).outerHeight(!0)}),this.panels.each(function(){e(this).height(Math.max(0,i-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):"auto"===t&&(i=0,this.panels.each(function(){i=Math.max(i,e(this).height("").height())}).height(i))},_eventHandler:function(t){var i=this.options,s=this.active,n=e(t.currentTarget),a=n.closest("li"),o=a[0]===s[0],r=o&&i.collapsible,h=r?e():this._getPanelForTab(a),l=s.length?this._getPanelForTab(s):e(),u={oldTab:s,oldPanel:l,newTab:r?e():a,newPanel:h};t.preventDefault(),a.hasClass("ui-state-disabled")||a.hasClass("ui-tabs-loading")||this.running||o&&!i.collapsible||this._trigger("beforeActivate",t,u)===!1||(i.active=r?!1:this.tabs.index(a),this.active=o?e():a,this.xhr&&this.xhr.abort(),l.length||h.length||e.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(a),t),this._toggle(t,u))},_toggle:function(t,i){function s(){a.running=!1,a._trigger("activate",t,i)}function n(){i.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),o.length&&a.options.show?a._show(o,a.options.show,s):(o.show(),s())}var a=this,o=i.newPanel,r=i.oldPanel;this.running=!0,r.length&&this.options.hide?this._hide(r,this.options.hide,function(){i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),n()}):(i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),r.hide(),n()),r.attr("aria-hidden","true"),i.oldTab.attr({"aria-selected":"false","aria-expanded":"false"}),o.length&&r.length?i.oldTab.attr("tabIndex",-1):o.length&&this.tabs.filter(function(){return 0===e(this).attr("tabIndex")}).attr("tabIndex",-1),o.attr("aria-hidden","false"),i.newTab.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_activate:function(t){var i,s=this._findActive(t);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:e.noop}))},_findActive:function(t){return t===!1?e():this.tabs.eq(t)},_getIndex:function(e){return"string"==typeof e&&(e=this.anchors.index(this.anchors.filter("[href$='"+e+"']"))),e},_destroy:function(){this.xhr&&this.xhr.abort(),this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"),this.tablist.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").removeAttr("role"),this.anchors.removeClass("ui-tabs-anchor").removeAttr("role").removeAttr("tabIndex").removeUniqueId(),this.tablist.unbind(this.eventNamespace),this.tabs.add(this.panels).each(function(){e.data(this,"ui-tabs-destroy")?e(this).remove():e(this).removeClass("ui-state-default ui-state-active ui-state-disabled ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel").removeAttr("tabIndex").removeAttr("aria-live").removeAttr("aria-busy").removeAttr("aria-selected").removeAttr("aria-labelledby").removeAttr("aria-hidden").removeAttr("aria-expanded").removeAttr("role")}),this.tabs.each(function(){var t=e(this),i=t.data("ui-tabs-aria-controls");i?t.attr("aria-controls",i).removeData("ui-tabs-aria-controls"):t.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(t){var i=this.options.disabled;i!==!1&&(void 0===t?i=!1:(t=this._getIndex(t),i=e.isArray(i)?e.map(i,function(e){return e!==t?e:null}):e.map(this.tabs,function(e,i){return i!==t?i:null})),this._setupDisabled(i))},disable:function(t){var i=this.options.disabled;if(i!==!0){if(void 0===t)i=!0;else{if(t=this._getIndex(t),-1!==e.inArray(t,i))return;i=e.isArray(i)?e.merge([t],i).sort():[t]}this._setupDisabled(i)}},load:function(t,i){t=this._getIndex(t);var s=this,n=this.tabs.eq(t),a=n.find(".ui-tabs-anchor"),o=this._getPanelForTab(n),r={tab:n,panel:o};this._isLocal(a[0])||(this.xhr=e.ajax(this._ajaxSettings(a,i,r)),this.xhr&&"canceled"!==this.xhr.statusText&&(n.addClass("ui-tabs-loading"),o.attr("aria-busy","true"),this.xhr.success(function(e){setTimeout(function(){o.html(e),s._trigger("load",i,r)},1)}).complete(function(e,t){setTimeout(function(){"abort"===t&&s.panels.stop(!1,!0),n.removeClass("ui-tabs-loading"),o.removeAttr("aria-busy"),e===s.xhr&&delete s.xhr},1)})))},_ajaxSettings:function(t,i,s){var n=this;return{url:t.attr("href"),beforeSend:function(t,a){return n._trigger("beforeLoad",i,e.extend({jqXHR:t,ajaxSettings:a},s))}}},_getPanelForTab:function(t){var i=e(t).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}}),e.widget("ui.tooltip",{version:"1.11.2",options:{content:function(){var t=e(this).attr("title")||"";return e("").text(t).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,tooltipClass:null,track:!1,close:null,open:null},_addDescribedBy:function(t,i){var s=(t.attr("aria-describedby")||"").split(/\s+/);s.push(i),t.data("ui-tooltip-id",i).attr("aria-describedby",e.trim(s.join(" ")))},_removeDescribedBy:function(t){var i=t.data("ui-tooltip-id"),s=(t.attr("aria-describedby")||"").split(/\s+/),n=e.inArray(i,s);-1!==n&&s.splice(n,1),t.removeData("ui-tooltip-id"),s=e.trim(s.join(" ")),s?t.attr("aria-describedby",s):t.removeAttr("aria-describedby")},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.options.disabled&&this._disable(),this.liveRegion=e("
").attr({role:"log","aria-live":"assertive","aria-relevant":"additions"}).addClass("ui-helper-hidden-accessible").appendTo(this.document[0].body)},_setOption:function(t,i){var s=this;return"disabled"===t?(this[i?"_disable":"_enable"](),this.options[t]=i,void 0):(this._super(t,i),"content"===t&&e.each(this.tooltips,function(e,t){s._updateContent(t.element)}),void 0)},_disable:function(){var t=this;e.each(this.tooltips,function(i,s){var n=e.Event("blur");n.target=n.currentTarget=s.element[0],t.close(n,!0)}),this.element.find(this.options.items).addBack().each(function(){var t=e(this);t.is("[title]")&&t.data("ui-tooltip-title",t.attr("title")).removeAttr("title")})},_enable:function(){this.element.find(this.options.items).addBack().each(function(){var t=e(this);t.data("ui-tooltip-title")&&t.attr("title",t.data("ui-tooltip-title"))})},open:function(t){var i=this,s=e(t?t.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),t&&"mouseover"===t.type&&s.parents().each(function(){var t,s=e(this);s.data("ui-tooltip-open")&&(t=e.Event("blur"),t.target=t.currentTarget=this,i.close(t,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._updateContent(s,t))},_updateContent:function(e,t){var i,s=this.options.content,n=this,a=t?t.type:null;return"string"==typeof s?this._open(t,e,s):(i=s.call(e[0],function(i){e.data("ui-tooltip-open")&&n._delay(function(){t&&(t.type=a),this._open(t,e,i)})}),i&&this._open(t,e,i),void 0)},_open:function(t,i,s){function n(e){u.of=e,o.is(":hidden")||o.position(u)}var a,o,r,h,l,u=e.extend({},this.options.position);if(s){if(a=this._find(i))return a.tooltip.find(".ui-tooltip-content").html(s),void 0;i.is("[title]")&&(t&&"mouseover"===t.type?i.attr("title",""):i.removeAttr("title")),a=this._tooltip(i),o=a.tooltip,this._addDescribedBy(i,o.attr("id")),o.find(".ui-tooltip-content").html(s),this.liveRegion.children().hide(),s.clone?(l=s.clone(),l.removeAttr("id").find("[id]").removeAttr("id")):l=s,e("
").html(l).appendTo(this.liveRegion),this.options.track&&t&&/^mouse/.test(t.type)?(this._on(this.document,{mousemove:n}),n(t)):o.position(e.extend({of:i},this.options.position)),o.hide(),this._show(o,this.options.show),this.options.show&&this.options.show.delay&&(h=this.delayedShow=setInterval(function(){o.is(":visible")&&(n(u.of),clearInterval(h))},e.fx.interval)),this._trigger("open",t,{tooltip:o}),r={keyup:function(t){if(t.keyCode===e.ui.keyCode.ESCAPE){var s=e.Event(t);s.currentTarget=i[0],this.close(s,!0)}}},i[0]!==this.element[0]&&(r.remove=function(){this._removeTooltip(o)}),t&&"mouseover"!==t.type||(r.mouseleave="close"),t&&"focusin"!==t.type||(r.focusout="close"),this._on(!0,i,r)}},close:function(t){var i,s=this,n=e(t?t.currentTarget:this.element),a=this._find(n);a&&(i=a.tooltip,a.closing||(clearInterval(this.delayedShow),n.data("ui-tooltip-title")&&!n.attr("title")&&n.attr("title",n.data("ui-tooltip-title")),this._removeDescribedBy(n),a.hiding=!0,i.stop(!0),this._hide(i,this.options.hide,function(){s._removeTooltip(e(this))}),n.removeData("ui-tooltip-open"),this._off(n,"mouseleave focusout keyup"),n[0]!==this.element[0]&&this._off(n,"remove"),this._off(this.document,"mousemove"),t&&"mouseleave"===t.type&&e.each(this.parents,function(t,i){e(i.element).attr("title",i.title),delete s.parents[t]}),a.closing=!0,this._trigger("close",t,{tooltip:i}),a.hiding||(a.closing=!1)))},_tooltip:function(t){var i=e("
").attr("role","tooltip").addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content "+(this.options.tooltipClass||"")),s=i.uniqueId().attr("id");return e("
").addClass("ui-tooltip-content").appendTo(i),i.appendTo(this.document[0].body),this.tooltips[s]={element:t,tooltip:i}},_find:function(e){var t=e.data("ui-tooltip-id");return t?this.tooltips[t]:null},_removeTooltip:function(e){e.remove(),delete this.tooltips[e.attr("id")]},_destroy:function(){var t=this;e.each(this.tooltips,function(i,s){var n=e.Event("blur"),a=s.element;n.target=n.currentTarget=a[0],t.close(n,!0),e("#"+i).remove(),a.data("ui-tooltip-title")&&(a.attr("title")||a.attr("title",a.data("ui-tooltip-title")),a.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}})}); \ No newline at end of file diff --git a/ext/autocomplete/lib/jquery-ui.theme.min.css b/ext/autocomplete/lib/jquery-ui.theme.min.css new file mode 100644 index 00000000..1b9f9bfa --- /dev/null +++ b/ext/autocomplete/lib/jquery-ui.theme.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.11.2 - 2015-01-31 +* http://jqueryui.com +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-widget{font-family:Helvetica,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Helvetica,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#fff url("images/ui-bg_flat_75_ffffff_40x100.png") 50% 50% repeat-x;color:#444}.ui-widget-content a{color:#444}.ui-widget-header{border:1px solid #ddd;background:#ddd url("images/ui-bg_highlight-soft_50_dddddd_1x100.png") 50% 50% repeat-x;color:#444;font-weight:bold}.ui-widget-header a{color:#444}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ddd;background:#f6f6f6 url("images/ui-bg_highlight-soft_100_f6f6f6_1x100.png") 50% 50% repeat-x;font-weight:bold;color:#0073ea}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#0073ea;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #0073ea;background:#0073ea url("images/ui-bg_highlight-soft_25_0073ea_1x100.png") 50% 50% repeat-x;font-weight:bold;color:#fff}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#fff;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #ddd;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#ff0084}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#ff0084;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #ccc;background:#fff url("images/ui-bg_flat_55_ffffff_40x100.png") 50% 50% repeat-x;color:#444}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#444}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #ff0084;background:#fff url("images/ui-bg_flat_55_ffffff_40x100.png") 50% 50% repeat-x;color:#222}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#222}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#222}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_ff0084_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_0073ea_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_666666_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_0073ea_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_ff0084_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:2px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:2px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:2px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:2px}.ui-widget-overlay{background:#eee url("images/ui-bg_flat_0_eeeeee_40x100.png") 50% 50% repeat-x;opacity:.8;filter:Alpha(Opacity=80)}.ui-widget-shadow{margin:-4px 0 0 -4px;padding:4px;background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.6;filter:Alpha(Opacity=60);border-radius:0} \ No newline at end of file diff --git a/ext/autocomplete/lib/jquery.tagit.css b/ext/autocomplete/lib/jquery.tagit.css new file mode 100644 index 00000000..f18650d9 --- /dev/null +++ b/ext/autocomplete/lib/jquery.tagit.css @@ -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; +} diff --git a/ext/autocomplete/lib/tag-it.min.js b/ext/autocomplete/lib/tag-it.min.js new file mode 100644 index 00000000..af48aee1 --- /dev/null +++ b/ext/autocomplete/lib/tag-it.min.js @@ -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("
    ").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('').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('
  • ').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(''),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=this.options.tagLimit)return this._trigger("onTagLimitExceeded",null,{duringInitialization:d}),!1;var g=b(this.options.onTagClicked?'
    ':'').text(a),e=b("
  • ").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("").addClass("ui-icon ui-icon-close"),c=b('\u00d7').addClass("tagit-close").append(c).click(function(a){f.removeTag(e)}),e.append(c));this.options.singleField||(g=g.html(),e.append(''));!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); diff --git a/ext/autocomplete/lib/tagit.ui-zendesk.css b/ext/autocomplete/lib/tagit.ui-zendesk.css new file mode 100644 index 00000000..18982864 --- /dev/null +++ b/ext/autocomplete/lib/tagit.ui-zendesk.css @@ -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); } + + diff --git a/ext/autocomplete/main.php b/ext/autocomplete/main.php index 506be6fa..35b379c8 100644 --- a/ext/autocomplete/main.php +++ b/ext/autocomplete/main.php @@ -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 - */ 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; } } diff --git a/ext/autocomplete/script.js b/ext/autocomplete/script.js index 6c26fa36..d437bbf9 100644 --- a/ext/autocomplete/script.js +++ b/ext/autocomplete/script.js @@ -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. + } + } }); }); diff --git a/ext/autocomplete/style.css b/ext/autocomplete/style.css index fe24acbd..7ff0f69b 100644 --- a/ext/autocomplete/style.css +++ b/ext/autocomplete/style.css @@ -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; } diff --git a/ext/autocomplete/test.php b/ext/autocomplete/test.php index bc6dc462..8378e584 100644 --- a/ext/autocomplete/test.php +++ b/ext/autocomplete/test.php @@ -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); } } diff --git a/ext/autocomplete/theme.php b/ext/autocomplete/theme.php new file mode 100644 index 00000000..d2d17047 --- /dev/null +++ b/ext/autocomplete/theme.php @@ -0,0 +1,19 @@ +add_html_header(""); + $page->add_html_header(""); + $page->add_html_header(''); + $page->add_html_header(""); + } +} diff --git a/ext/ban_words/main.php b/ext/ban_words/main.php index d51b479e..4e57b7b4 100644 --- a/ext/ban_words/main.php +++ b/ext/ban_words/main.php @@ -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
    "); @@ -97,9 +97,6 @@ xanax } } - /** - * @return string[] - */ private function get_words(): array { global $config; diff --git a/ext/ban_words/test.php b/ext/ban_words/test.php index cb78fa63..a70f6a06 100644 --- a/ext/ban_words/test.php +++ b/ext/ban_words/test.php @@ -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\//"); diff --git a/ext/bbcode/main.php b/ext/bbcode/main.php index 8746a050..f2739441 100644 --- a/ext/bbcode/main.php +++ b/ext/bbcode/main.php @@ -7,12 +7,6 @@ namespace Shimmie2; class BBCode extends FormatterExtension { public function format(string $text): string - { - $text = $this->_format($text); - return "$text"; - } - - 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 . "
    " . $middle . "
    " . $ending; + $text = $beginning . "
    " . $middle . "
    " . $ending; } return $text; } diff --git a/ext/bbcode/script.js b/ext/bbcode/script.js index acedba74..96c6ea7d 100644 --- a/ext/bbcode/script.js +++ b/ext/bbcode/script.js @@ -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()); } }); }); diff --git a/ext/bbcode/style.css b/ext/bbcode/style.css index 525ff5c5..200221cc 100644 --- a/ext/bbcode/style.css +++ b/ext/bbcode/style.css @@ -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; } diff --git a/ext/bbcode/test.php b/ext/bbcode/test.php index 79094841..359ee3e0 100644 --- a/ext/bbcode/test.php +++ b/ext/bbcode/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class BBCodeTest extends ShimmiePHPUnitTestCase { - public function testBasics(): void + public function testBasics() { $this->assertEquals( "bolditalic", @@ -14,7 +14,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase ); } - public function testStacking(): void + public function testStacking() { $this->assertEquals( "BIB", @@ -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( - "
    [b]bold[/b]
    ", + "
    [b]bold[/b]
    ", $this->filter("[code][b]bold[/b][/code]") ); } - public function testNestedList(): void + public function testNestedList() { $this->assertEquals( "
    • a
      • a
      • b
    • b
    ", @@ -54,7 +54,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase ); } - public function testSpoiler(): void + public function testSpoiler() { $this->assertEquals( "ShishNet", @@ -69,7 +69,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase # "[spoiler]ShishNet"); } - public function testURL(): void + public function testURL() { $this->assertEquals( "https://shishnet.org", @@ -85,7 +85,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase ); } - public function testEmailURL(): void + public function testEmailURL() { $this->assertEquals( "spam@shishnet.org", @@ -93,7 +93,7 @@ class BBCodeTest extends ShimmiePHPUnitTestCase ); } - public function testAnchor(): void + public function testAnchor() { $this->assertEquals( 'Rules ', @@ -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( '>>123', diff --git a/ext/biography/main.php b/ext/biography/main.php index d645cd93..fac41f5f 100644 --- a/ext/biography/main.php +++ b/ext/biography/main.php @@ -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")) { diff --git a/ext/biography/test.php b/ext/biography/test.php index 746d092f..d410df72 100644 --- a/ext/biography/test.php +++ b/ext/biography/test.php @@ -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"); diff --git a/ext/biography/theme.php b/ext/biography/theme.php index 37e91e86..f555e932 100644 --- a/ext/biography/theme.php +++ b/ext/biography/theme.php @@ -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") ); diff --git a/ext/blocks/main.php b/ext/blocks/main.php index e06eccaa..e864b311 100644 --- a/ext/blocks/main.php +++ b/ext/blocks/main.php @@ -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"); diff --git a/ext/blocks/test.php b/ext/blocks/test.php index 03d3ff1f..15677c4f 100644 --- a/ext/blocks/test.php +++ b/ext/blocks/test.php @@ -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"); diff --git a/ext/blocks/theme.php b/ext/blocks/theme.php index fc225d53..4f296140 100644 --- a/ext/blocks/theme.php +++ b/ext/blocks/theme.php @@ -16,38 +16,35 @@ use function MicroHTML\OPTION; class BlocksTheme extends Themelet { - /** - * @param array $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"])) ), )); diff --git a/ext/blotter/info.php b/ext/blotter/info.php index 90d68bae..844b3aa2 100644 --- a/ext/blotter/info.php +++ b/ext/blotter/info.php @@ -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.

    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"; } diff --git a/ext/blotter/main.php b/ext/blotter/main.php index 0a4dc1bb..7c7d5451 100644 --- a/ext/blotter/main.php +++ b/ext/blotter/main.php @@ -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", "
    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"], "
    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); diff --git a/ext/blotter/script.js b/ext/blotter/script.js index 664d8588..2623f3e4 100644 --- a/ext/blotter/script.js +++ b/ext/blotter/script.js @@ -4,14 +4,14 @@ document.addEventListener('DOMContentLoaded', () => { $(".shm-blotter2-toggle").click(function() { $(".shm-blotter2").slideToggle("slow", function() { if($(".shm-blotter2").is(":hidden")) { - shm_cookie_set("ui-blotter2-hidden", 'true'); + Cookies.set("ui-blotter2-hidden", 'true'); } else { - shm_cookie_set("ui-blotter2-hidden", 'false'); + Cookies.set("ui-blotter2-hidden", 'false'); } }); }); - if(shm_cookie_get("ui-blotter2-hidden") === 'true') { + if(Cookies.get("ui-blotter2-hidden") === 'true') { $(".shm-blotter2").hide(); } }); diff --git a/ext/blotter/test.php b/ext/blotter/test.php index 26797a2a..4bd3cfdb 100644 --- a/ext/blotter/test.php +++ b/ext/blotter/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class BlotterTest extends ShimmiePHPUnitTestCase { - public function testDenial(): void + public function testDenial() { $this->get_page("blotter/editor"); $this->assert_response(403); @@ -16,7 +16,7 @@ class BlotterTest extends ShimmiePHPUnitTestCase $this->assert_response(403); } - public function testAddViewRemove(): void + public function testAddViewRemove() { $this->log_in_as_admin(); diff --git a/ext/blotter/theme.php b/ext/blotter/theme.php index 6a9750e6..67096a94 100644 --- a/ext/blotter/theme.php +++ b/ext/blotter/theme.php @@ -4,15 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -/** - * @phpstan-type BlotterEntry array{id:int,entry_date:string,entry_text:string,important:bool} - */ class BlotterTheme extends Themelet { - /** - * @param BlotterEntry[] $entries - */ - public function display_editor(array $entries): void + public function display_editor($entries) { global $page; $html = $this->get_html_for_blotter_editor($entries); @@ -22,10 +16,7 @@ class BlotterTheme extends Themelet $page->add_block(new Block("Navigation", "Index", "left", 0)); } - /** - * @param BlotterEntry[] $entries - */ - public function display_blotter_page(array $entries): void + public function display_blotter_page($entries) { global $page; $html = $this->get_html_for_blotter_page($entries); @@ -34,9 +25,6 @@ class BlotterTheme extends Themelet $page->add_block(new Block("Blotter Entries", $html, "main", 10)); } - /** - * @param BlotterEntry[] $entries - */ public function display_blotter(array $entries): void { global $page, $config; @@ -45,9 +33,6 @@ class BlotterTheme extends Themelet $page->add_block(new Block(null, $html, $position, 20)); } - /** - * @param BlotterEntry[] $entries - */ private function get_html_for_blotter_editor(array $entries): string { global $user; @@ -119,9 +104,6 @@ class BlotterTheme extends Themelet return $html; } - /** - * @param BlotterEntry[] $entries - */ private function get_html_for_blotter_page(array $entries): string { /** @@ -141,11 +123,11 @@ class BlotterTheme extends Themelet $i_close = ""; //$id = $entries[$i]['id']; $messy_date = $entries[$i]['entry_date']; - $clean_date = date("y/m/d", strtotime_ex($messy_date)); + $clean_date = date("y/m/d", strtotime($messy_date)); $entry_text = $entries[$i]['entry_text']; if ($entries[$i]['important'] == 'Y') { $i_open = ""; - $i_close = ""; + $i_close=""; } $html .= "{$i_open}{$clean_date} - {$entry_text}{$i_close}

    "; } @@ -153,29 +135,27 @@ class BlotterTheme extends Themelet return $html; } - /** - * @param BlotterEntry[] $entries - */ private function get_html_for_blotter(array $entries): string { global $config; $i_color = $config->get_string("blotter_color", "#FF0000"); $position = $config->get_string("blotter_position", "subheading"); $entries_list = ""; - foreach($entries as $entry) { + $num_entries = count($entries); + for ($i = 0 ; $i < $num_entries ; $i++) { /** * Blotter entries */ // Reset variables: $i_open = ""; $i_close = ""; - //$id = $entry['id']; - $messy_date = $entry['entry_date']; - $clean_date = date("m/d/y", strtotime_ex($messy_date)); - $entry_text = $entry['entry_text']; - if ($entry['important'] == 'Y') { + //$id = $entries[$i]['id']; + $messy_date = $entries[$i]['entry_date']; + $clean_date = date("m/d/y", strtotime($messy_date)); + $entry_text = $entries[$i]['entry_text']; + if ($entries[$i]['important'] == 'Y') { $i_open = ""; - $i_close = ""; + $i_close=""; } $entries_list .= "

  • {$i_open}{$clean_date} - {$entry_text}{$i_close}
  • "; } @@ -192,7 +172,7 @@ class BlotterTheme extends Themelet $out_text = "No blotter entries yet."; $in_text = "Empty."; } else { - $clean_date = date("m/d/y", strtotime_ex($entries[0]['entry_date'])); + $clean_date = date("m/d/y", strtotime($entries[0]['entry_date'])); $out_text = "Blotter updated: {$clean_date}"; $in_text = "
      $entries_list
    "; } diff --git a/ext/browser_search/info.php b/ext/browser_search/info.php index b5fa37c4..0644f9bc 100644 --- a/ext/browser_search/info.php +++ b/ext/browser_search/info.php @@ -11,7 +11,7 @@ class BrowserSearchInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Browser Search"; public string $url = "http://atravelinggeek.com/"; - public array $authors = ["ATravelingGeek" => "atg@atravelinggeek.com"]; + public array $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"]; public string $license = self::LICENSE_GPLV2; public ?string $version = "0.1c, October 26, 2007"; public string $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions"; diff --git a/ext/browser_search/main.php b/ext/browser_search/main.php index 890e8b41..eb71905a 100644 --- a/ext/browser_search/main.php +++ b/ext/browser_search/main.php @@ -6,13 +6,13 @@ namespace Shimmie2; class BrowserSearch extends Extension { - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string("search_suggestions_results_order", 'a'); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $database, $page; @@ -26,9 +26,9 @@ class BrowserSearch extends Extension if ($event->page_matches("browser_search.xml")) { // First, we need to build all the variables we'll need $search_title = $config->get_string(SetupConfig::TITLE); - $search_form_url = search_link(['{searchTerms}']); + $search_form_url = make_link('post/list/{searchTerms}'); $suggenton_url = make_link('browser_search/')."{searchTerms}"; - $icon_b64 = base64_encode(file_get_contents_ex("ext/static_files/static/favicon.ico")); + $icon_b64 = base64_encode(file_get_contents("ext/static_files/static/favicon.ico")); // Now for the XML $xml = " @@ -65,17 +65,17 @@ class BrowserSearch extends Extension } $tags = $database->get_col( "SELECT tag FROM tags WHERE tag LIKE :tag AND count > 0 ORDER BY $order LIMIT 30", - ['tag' => $tag_search."%"] + ['tag'=>$tag_search."%"] ); // And to do stuff with it. We want our output to look like: // ["shimmie",["shimmies","shimmy","shimmie","21 shimmies","hip shimmies","skea shimmies"],[],[]] $page->set_mode(PageMode::DATA); - $page->set_data(json_encode_ex([$tag_search, $tags, [], []])); + $page->set_data(json_encode([$tag_search, $tags, [], []])); } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sort_by = []; $sort_by['Alphabetical'] = 'a'; diff --git a/ext/browser_search/test.php b/ext/browser_search/test.php index c50f0d81..3ccd045f 100644 --- a/ext/browser_search/test.php +++ b/ext/browser_search/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class BrowserSearchTest extends ShimmiePHPUnitTestCase { - public function testBasic(): void + public function testBasic() { $page = $this->get_page("browser_search.xml"); $this->assertEquals(200, $page->code); diff --git a/ext/bulk_actions/info.php b/ext/bulk_actions/info.php index 1c4dd33f..6d2f8fe0 100644 --- a/ext/bulk_actions/info.php +++ b/ext/bulk_actions/info.php @@ -10,7 +10,7 @@ class BulkActionsInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Bulk Actions"; - 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 query and selection-based bulk action support"; public ?string $documentation = "Provides bulk action section in list view. Allows performing actions against a set of posts based on query or manual selection. Based on Mass Tagger by Christian Walde, contributions by Shish and Agasa. diff --git a/ext/bulk_actions/main.php b/ext/bulk_actions/main.php index 096cc00d..19ce61ad 100644 --- a/ext/bulk_actions/main.php +++ b/ext/bulk_actions/main.php @@ -4,41 +4,33 @@ 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; - class BulkActionException extends SCoreException { } class BulkActionBlockBuildingEvent extends Event { - /** - * @var array - */ public array $actions = []; - /** @var string[] */ public array $search_terms = []; - public function add_action(string $action, string $button_text, string $access_key = null, string $confirmation_message = "", string $block = "", int $position = 40): void + public function add_action(String $action, string $button_text, string $access_key = null, string $confirmation_message = "", string $block = "", int $position = 40) { if (!empty($access_key)) { - assert(strlen($access_key) == 1); + assert(strlen($access_key)==1); foreach ($this->actions as $existing) { - if ($existing["access_key"] == $access_key) { + if ($existing["access_key"]==$access_key) { throw new SCoreException("Access key $access_key is already in use"); } } } - $this->actions[] = [ - "block" => $block, - "access_key" => $access_key, - "confirmation_message" => $confirmation_message, - "action" => $action, - "button_text" => $button_text, - "position" => $position - ]; + $this->actions[] =[ + "block" => $block, + "access_key" => $access_key, + "confirmation_message" => $confirmation_message, + "action" => $action, + "button_text" => $button_text, + "position" => $position + ]; } } @@ -48,7 +40,7 @@ class BulkActionEvent extends Event public \Generator $items; public bool $redirect = true; - public function __construct(string $action, \Generator $items) + public function __construct(String $action, \Generator $items) { parent::__construct(); $this->action = $action; @@ -61,7 +53,7 @@ class BulkActions extends Extension /** @var BulkActionsTheme */ protected Themelet $theme; - public function onPostListBuilding(PostListBuildingEvent $event): void + public function onPostListBuilding(PostListBuildingEvent $event) { global $page, $user; @@ -81,7 +73,7 @@ class BulkActions extends Extension } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -105,23 +97,25 @@ class BulkActions extends Extension } } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('bulk-action') - ->addArgument('action', InputArgument::REQUIRED) - ->addArgument('query', InputArgument::REQUIRED) - ->setDescription('Perform a bulk action on a given query') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $action = $input->getArgument('action'); - $query = $input->getArgument('query'); - $items = $this->yield_search_results($query); - log_info("bulk_actions", "Performing $action on $query"); - send_event(new BulkActionEvent($action, $items)); - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + print "\tbulk-action \n"; + print "\t\tperform an action on all query results\n\n"; + } + if ($event->cmd == "bulk-action") { + if (count($event->args) < 2) { + return; + } + $action = $event->args[0]; + $query = $event->args[1]; + $items = $this->yield_search_results($query); + log_info("bulk_actions", "Performing $action on {$event->args[1]}"); + send_event(new BulkActionEvent($event->args[0], $items)); + } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $page, $user; @@ -143,7 +137,7 @@ class BulkActions extends Extension $replace = true; } - $i = $this->tag_items($event->items, $tags, $replace); + $i= $this->tag_items($event->items, $tags, $replace); $page->flash("Tagged $i items"); } break; @@ -160,7 +154,7 @@ class BulkActions extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("bulk_action") && $user->can(Permissions::PERFORM_BULK_ACTIONS)) { @@ -174,19 +168,26 @@ class BulkActions extends Extension $items = null; if (isset($_POST['bulk_selected_ids']) && !empty($_POST['bulk_selected_ids'])) { $data = json_decode($_POST['bulk_selected_ids']); - if (!is_array($data) || empty($data)) { + if (empty($data)) { throw new BulkActionException("No ids specified in bulk_selected_ids"); } - $items = $this->yield_items($data); - } elseif (isset($_POST['bulk_query']) && !empty($_POST['bulk_query'])) { + if (is_array($data)) { + $items = $this->yield_items($data); + } + } elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") { $query = $_POST['bulk_query']; - $items = $this->yield_search_results($query); + if (!empty($query)) { + $items = $this->yield_search_results($query); + } } else { throw new BulkActionException("No ids selected and no query present, cannot perform bulk operation on entire collection"); } - shm_set_timeout(null); - $bae = send_event(new BulkActionEvent($action, $items)); + $bae = new BulkActionEvent($action, $items); + + if (is_iterable($items)) { + send_event($bae); + } if ($bae->redirect) { $page->set_mode(PageMode::REDIRECT); @@ -198,42 +199,29 @@ class BulkActions extends Extension } } - /** - * @param int[] $data - * @return \Generator - */ private function yield_items(array $data): \Generator { foreach ($data as $id) { - $image = Image::by_id($id); - if ($image != null) { - yield $image; + if (is_numeric($id)) { + $image = Image::by_id($id); + if ($image!=null) { + yield $image; + } } } } - /** - * @return \Generator - */ private function yield_search_results(string $query): \Generator { $tags = Tag::explode($query); - return Search::find_images_iterable(0, null, $tags); + return Image::find_images_iterable(0, null, $tags); } - /** - * @param array{position: int} $a - * @param array{position: int} $b - */ - private function sort_blocks(array $a, array $b): int + private function sort_blocks($a, $b) { return $a["position"] - $b["position"]; } - /** - * @param iterable $posts - * @return array{0: int, 1: int} - */ private function delete_posts(iterable $posts): array { global $page; @@ -241,7 +229,7 @@ class BulkActions extends Extension $size = 0; foreach ($posts as $post) { try { - if (Extension::is_enabled(ImageBanInfo::KEY) && isset($_POST['bulk_ban_reason'])) { + if (class_exists("Shimmie2\ImageBan") && isset($_POST['bulk_ban_reason'])) { $reason = $_POST['bulk_ban_reason']; if ($reason) { send_event(new AddImageHashBanEvent($post->hash, $reason)); @@ -257,9 +245,6 @@ class BulkActions extends Extension return [$total, $size]; } - /** - * @param iterable $items - */ private function tag_items(iterable $items, string $tags, bool $replace): int { $tags = Tag::explode($tags); @@ -300,10 +285,7 @@ class BulkActions extends Extension return $total; } - /** - * @param iterable $items - */ - private function set_source(iterable $items, string $source): int + private function set_source(iterable $items, String $source): int { global $page; $total = 0; diff --git a/ext/bulk_actions/script.js b/ext/bulk_actions/script.js index 184a4b8c..831c0087 100644 --- a/ext/bulk_actions/script.js +++ b/ext/bulk_actions/script.js @@ -68,11 +68,11 @@ function get_selected_items() { } function set_selected_items(items) { - $(".shm-thumb").removeClass('bulk_selected'); + $(".shm-thumb").removeClass('selected'); $(items).each( function(index,item) { - $('.shm-thumb[data-post-id="' + item + '"]').addClass('bulk_selected'); + $('.shm-thumb[data-post-id="' + item + '"]').addClass('selected'); } ); diff --git a/ext/bulk_actions/style.css b/ext/bulk_actions/style.css index 117a5ce8..4e7449fc 100644 --- a/ext/bulk_actions/style.css +++ b/ext/bulk_actions/style.css @@ -1,4 +1,4 @@ -.bulk_selected { +.selected { outline: 3px solid blue; } diff --git a/ext/bulk_actions/theme.php b/ext/bulk_actions/theme.php index 1059b4a0..608b7778 100644 --- a/ext/bulk_actions/theme.php +++ b/ext/bulk_actions/theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class BulkActionsTheme extends Themelet { - /** - * @param array $actions - */ - public function display_selector(Page $page, array $actions, string $query): void + public function display_selector(Page $page, array $actions, string $query) { $body = " @@ -54,7 +51,7 @@ class BulkActionsTheme extends Themelet public function render_ban_reason_input(): string { - if (Extension::is_enabled(ImageBanInfo::KEY)) { + if (class_exists("Shimmie2\ImageBan")) { return ""; } else { return ""; diff --git a/ext/bulk_add/main.php b/ext/bulk_add/main.php index 88e44ce8..83d84d9c 100644 --- a/ext/bulk_add/main.php +++ b/ext/bulk_add/main.php @@ -4,14 +4,9 @@ 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; - class BulkAddEvent extends Event { public string $dir; - /** @var UploadResult[] */ public array $results; public function __construct(string $dir) @@ -27,48 +22,47 @@ class BulkAdd extends Extension /** @var BulkAddTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("bulk_add")) { if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['dir'])) { shm_set_timeout(null); $bae = send_event(new BulkAddEvent($_POST['dir'])); - $this->theme->display_upload_results($page, $bae->results); + foreach ($bae->results as $result) { + $this->theme->add_status("Adding files", $result); + } + $this->theme->display_upload_results($page); } } } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('bulk-add') - ->addArgument('directory', InputArgument::REQUIRED) - ->setDescription('Import a directory of images') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $dir = $input->getArgument('directory'); - $bae = send_event(new BulkAddEvent($dir)); - foreach ($bae->results as $r) { - if(is_a($r, UploadError::class)) { - $output->writeln($r->name." failed: ".$r->error); - } else { - $output->writeln($r->name." ok"); - } - } - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + print "\tbulk-add [directory]\n"; + print "\t\tImport this directory\n\n"; + } + if ($event->cmd == "bulk-add") { + if (count($event->args) == 1) { + $bae = send_event(new BulkAddEvent($event->args[0])); + print(implode("\n", $bae->results)); + } + } } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_admin_block(); } - public function onBulkAdd(BulkAddEvent $event): void + public function onBulkAdd(BulkAddEvent $event) { if (is_dir($event->dir) && is_readable($event->dir)) { $event->results = add_dir($event->dir); } else { - $event->results = [new UploadError($event->dir, "is not a readable directory")]; + $h_dir = html_escape($event->dir); + $event->results[] = "Error, $h_dir is not a readable directory"; } } } diff --git a/ext/bulk_add/test.php b/ext/bulk_add/test.php index 81425c03..78c6e74c 100644 --- a/ext/bulk_add/test.php +++ b/ext/bulk_add/test.php @@ -6,14 +6,18 @@ namespace Shimmie2; class BulkAddTest extends ShimmiePHPUnitTestCase { - public function testInvalidDir(): void + public function testInvalidDir() { send_event(new UserLoginEvent(User::by_name(self::$admin_name))); $bae = send_event(new BulkAddEvent('asdf')); - $this->assertTrue(is_a($bae->results[0], UploadError::class)); + $this->assertContainsEquals( + "Error, asdf is not a readable directory", + $bae->results, + implode("\n", $bae->results) + ); } - public function testValidDir(): void + public function testValidDir() { send_event(new UserLoginEvent(User::by_name(self::$admin_name))); send_event(new BulkAddEvent('tests')); diff --git a/ext/bulk_add/theme.php b/ext/bulk_add/theme.php index 339bfd5b..6a74b3ef 100644 --- a/ext/bulk_add/theme.php +++ b/ext/bulk_add/theme.php @@ -4,27 +4,21 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\{UL, LI}; - class BulkAddTheme extends Themelet { - /** + private array $messages = []; + + /* * Show a standard page for results to be put into - * - * @param UploadResult[] $results */ - public function display_upload_results(Page $page, array $results): void + public function display_upload_results(Page $page) { $page->set_title("Adding folder"); $page->set_heading("Adding folder"); $page->add_block(new NavBlock()); - $html = UL(); - foreach ($results as $r) { - if (is_a($r, UploadError::class)) { - $html->appendChild(LI("{$r->name} failed: {$r->error}")); - } else { - $html->appendChild(LI("{$r->name} ok")); - } + $html = ""; + foreach ($this->messages as $block) { + $html .= "
    " . $block->body; } $page->add_block(new Block("Results", $html)); } @@ -34,7 +28,7 @@ class BulkAddTheme extends Themelet * links to bulk_add with POST[dir] set to the name of a server-side * directory full of images */ - public function display_admin_block(): void + public function display_admin_block() { global $page; $html = " @@ -52,4 +46,9 @@ class BulkAddTheme extends Themelet "; $page->add_block(new Block("Bulk Add", $html)); } + + public function add_status($title, $body) + { + $this->messages[] = new Block($title, $body); + } } diff --git a/ext/bulk_add_csv/info.php b/ext/bulk_add_csv/info.php index 772e3f7f..46d13192 100644 --- a/ext/bulk_add_csv/info.php +++ b/ext/bulk_add_csv/info.php @@ -11,20 +11,18 @@ class BulkAddCSVInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Bulk Add CSV"; public string $url = self::SHIMMIE_URL; - public array $authors = ["velocity37" => "velocity37@gmail.com"]; + public array $authors = ["velocity37"=>"velocity37@gmail.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Bulk add server-side posts with metadata from CSV file"; public ?string $documentation = -"Adds posts from a CSV with the five following values: -
    \"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\"
    - -e.g. -
    \"/tmp/cat.png\",\"shish oekaki\",\"http://shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\"
    - -Any value but the first may be omitted, but there must be five values per line. -e.g.
    \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"
    - -Useful for importing tagged posts without having to do database manipulation. - +"Modification of \"Bulk Add\" by Shish.

    +Adds posts from a CSV with the five following values:
    +\"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\"
    +e.g. \"/tmp/cat.png\",\"shish oekaki\",\"shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\"

    +Any value but the first may be omitted, but there must be five values per line.
    +e.g. \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"

    +Post thumbnails will be displayed at the AR of the full post. Thumbnails that are +normally static (e.g. SWF) will be displayed at the board's max thumbnail size

    +Useful for importing tagged posts without having to do database manipulation.

    Note: requires \"Admin Controls\" and optionally \"Post Ratings\" to be enabled

    "; } diff --git a/ext/bulk_add_csv/main.php b/ext/bulk_add_csv/main.php index 94e93fcf..868ae5ac 100644 --- a/ext/bulk_add_csv/main.php +++ b/ext/bulk_add_csv/main.php @@ -4,16 +4,12 @@ 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; - class BulkAddCSV extends Extension { /** @var BulkAddCSVTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("bulk_add_csv")) { @@ -25,56 +21,49 @@ class BulkAddCSV extends Extension } } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('bulk-add-csv') - ->addArgument('path-to-csv', InputArgument::REQUIRED) - ->setDescription('Import posts from a given CSV file') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - global $user; - if (!$user->can(Permissions::BULK_ADD)) { - $output->writeln("Not running as an admin, which can cause problems."); - $output->writeln("Please add the parameter: -u admin_username"); - return Command::FAILURE; - } + if ($event->cmd == "help") { + print " bulk-add-csv [/path/to.csv]\n"; + print " Import this .csv file (refer to documentation)\n\n"; + } + if ($event->cmd == "bulk-add-csv") { + global $user; - $this->add_csv($input->getArgument('path-to-csv')); - return Command::SUCCESS; - }); + //Nag until CLI is admin by default + if (!$user->can(Permissions::BULK_ADD)) { + print "Not running as an admin, which can cause problems.\n"; + print "Please add the parameter: -u admin_username"; + } elseif (count($event->args) == 1) { + $this->add_csv($event->args[0]); + } + } } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_admin_block(); } /** * Generate the necessary DataUploadEvent for a given image and tags. - * - * @param string[] $tags */ - private function add_image(string $tmpname, string $filename, array $tags, string $source, string $rating, string $thumbfile): void + private function add_image(string $tmpname, string $filename, string $tags, string $source, string $rating, string $thumbfile) { - global $database; - $database->with_savepoint(function () use ($tmpname, $filename, $tags, $source, $rating, $thumbfile) { - $event = send_event(new DataUploadEvent($tmpname, [ - 'filename' => pathinfo($filename, PATHINFO_BASENAME), - 'tags' => $tags, - 'source' => $source, - 'rating' => $rating, - ])); - - if (count($event->images) == 0) { - throw new UploadException("File type not recognised"); - } else { - if (file_exists($thumbfile)) { - copy($thumbfile, warehouse_path(Image::THUMBNAIL_DIR, $event->hash)); - } + $event = add_image($tmpname, $filename, $tags, $source); + if ($event->image_id == -1) { + throw new UploadException("File type not recognised"); + } else { + if (class_exists("Shimmie2\RatingSetEvent") && in_array($rating, ["s", "q", "e"])) { + send_event(new RatingSetEvent(Image::by_id($event->image_id), $rating)); } - }); + if (file_exists($thumbfile)) { + copy($thumbfile, warehouse_path(Image::THUMBNAIL_DIR, $event->hash)); + } + } } - private function add_csv(string $csvfile): void + private function add_csv(string $csvfile) { if (!file_exists($csvfile)) { $this->theme->add_status("Error", "$csvfile not found"); @@ -87,7 +76,7 @@ class BulkAddCSV extends Extension $linenum = 1; $list = ""; - $csvhandle = false_throws(fopen($csvfile, "r")); + $csvhandle = fopen($csvfile, "r"); while (($csvdata = fgetcsv($csvhandle, 0, ",")) !== false) { if (count($csvdata) != 5) { @@ -102,12 +91,12 @@ class BulkAddCSV extends Extension } } $fullpath = $csvdata[0]; - $tags = Tag::explode(trim($csvdata[1])); + $tags = trim($csvdata[1]); $source = $csvdata[2]; $rating = $csvdata[3]; $thumbfile = $csvdata[4]; $shortpath = pathinfo($fullpath, PATHINFO_BASENAME); - $list .= "
    ".html_escape("$shortpath (".implode(", ", $tags).")... "); + $list .= "
    ".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... "); if (file_exists($csvdata[0]) && is_file($csvdata[0])) { try { $this->add_image($fullpath, $shortpath, $tags, $source, $rating, $thumbfile); diff --git a/ext/bulk_add_csv/theme.php b/ext/bulk_add_csv/theme.php index a29777f9..2128e5ae 100644 --- a/ext/bulk_add_csv/theme.php +++ b/ext/bulk_add_csv/theme.php @@ -6,13 +6,12 @@ namespace Shimmie2; class BulkAddCSVTheme extends Themelet { - /** @var Block[] */ private array $messages = []; /* * Show a standard page for results to be put into */ - public function display_upload_results(Page $page): void + public function display_upload_results(Page $page) { $page->set_title("Adding posts from csv"); $page->set_heading("Adding posts from csv"); @@ -27,7 +26,7 @@ class BulkAddCSVTheme extends Themelet * links to bulk_add_csv with POST[csv] set to the name of a server-side * csv file */ - public function display_admin_block(): void + public function display_admin_block() { global $page; $html = " @@ -45,7 +44,7 @@ class BulkAddCSVTheme extends Themelet $page->add_block(new Block("Bulk Add CSV", $html)); } - public function add_status(string $title, string $body): void + public function add_status($title, $body) { $this->messages[] = new Block($title, $body); } diff --git a/ext/bulk_download/info.php b/ext/bulk_download/info.php index 14e1b731..d5260459 100644 --- a/ext/bulk_download/info.php +++ b/ext/bulk_download/info.php @@ -10,7 +10,7 @@ class BulkDownloadInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Bulk Download"; - 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 = "Allows bulk downloading images."; public array $dependencies = [BulkActionsInfo::KEY]; diff --git a/ext/bulk_download/main.php b/ext/bulk_download/main.php index 6d8210e2..64e38025 100644 --- a/ext/bulk_download/main.php +++ b/ext/bulk_download/main.php @@ -17,13 +17,13 @@ class BulkDownload extends Extension { private const DOWNLOAD_ACTION_NAME = "bulk_download"; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int(BulkDownloadConfig::SIZE_LIMIT, parse_shorthand_int('100MB')); } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -32,7 +32,7 @@ class BulkDownload extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Bulk Download"); @@ -41,14 +41,14 @@ class BulkDownload extends Extension $sb->end_table(); } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $user, $page, $config; - if ($user->can(Permissions::BULK_DOWNLOAD) && + if ($user->can(Permissions::BULK_DOWNLOAD)&& ($event->action == BulkDownload::DOWNLOAD_ACTION_NAME)) { $download_filename = $user->name . '-' . date('YmdHis') . '.zip'; - $zip_filename = shm_tempnam("bulk_download"); + $zip_filename = tempnam(sys_get_temp_dir(), "shimmie_bulk_download"); $zip = new \ZipArchive(); $size_total = 0; $max_size = $config->get_int(BulkDownloadConfig::SIZE_LIMIT); @@ -57,10 +57,11 @@ class BulkDownload extends Extension foreach ($event->items as $image) { $img_loc = warehouse_path(Image::IMAGE_DIR, $image->hash, false); $size_total += filesize($img_loc); - if ($size_total > $max_size) { + if ($size_total>$max_size) { throw new BulkDownloadException("Bulk download limited to ".human_filesize($max_size)); } + $filename = urldecode($image->get_nice_image_name()); $filename = str_replace(":", ";", $filename); $zip->addFile($img_loc, $filename); diff --git a/ext/bulk_import_export/events.php b/ext/bulk_import_export/events.php index 3cbb1b91..89b4b00c 100644 --- a/ext/bulk_import_export/events.php +++ b/ext/bulk_import_export/events.php @@ -7,7 +7,6 @@ namespace Shimmie2; class BulkExportEvent extends Event { public Image $image; - /** @var array */ public array $fields = []; public function __construct(Image $image) @@ -21,13 +20,9 @@ class BulkExportEvent extends Event class BulkImportEvent extends Event { public Image $image; - /** @var array */ public array $fields = []; - /** - * @param array $fields - */ - public function __construct(Image $image, array $fields) + public function __construct(Image $image, $fields) { parent::__construct(); $this->image = $image; diff --git a/ext/bulk_import_export/info.php b/ext/bulk_import_export/info.php index c36a416e..6ad951b0 100644 --- a/ext/bulk_import_export/info.php +++ b/ext/bulk_import_export/info.php @@ -12,7 +12,7 @@ class BulkImportExportInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Bulk Import/Export"; - 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 = "Allows bulk exporting/importing of images and associated data."; public array $dependencies = [BulkActionsInfo::KEY]; diff --git a/ext/bulk_import_export/main.php b/ext/bulk_import_export/main.php index cd42422a..5f635784 100644 --- a/ext/bulk_import_export/main.php +++ b/ext/bulk_import_export/main.php @@ -11,7 +11,7 @@ class BulkImportExport extends DataHandlerExtension protected array $SUPPORTED_MIME = [MimeType::ZIP]; - public function onDataUpload(DataUploadEvent $event): void + public function onDataUpload(DataUploadEvent $event) { global $user, $database; @@ -30,53 +30,63 @@ class BulkImportExport extends DataHandlerExtension $skipped = 0; $failed = 0; + $database->commit(); + while (!empty($json_data)) { $item = array_pop($json_data); + $database->begin_transaction(); try { $image = Image::by_hash($item->hash); - if ($image != null) { + if ($image!=null) { $skipped++; log_info(BulkImportExportInfo::KEY, "Post $item->hash already present, skipping"); + $database->commit(); continue; } - $tmpfile = shm_tempnam("bulk_import"); + $tmpfile = tempnam(sys_get_temp_dir(), "shimmie_bulk_import"); $stream = $zip->getStream($item->hash); - if ($stream === false) { + if ($zip === false) { throw new SCoreException("Could not import " . $item->hash . ": File not in zip"); } file_put_contents($tmpfile, $stream); - $database->with_savepoint(function () use ($item, $tmpfile, $event) { - $images = send_event(new DataUploadEvent($tmpfile, [ - 'filename' => pathinfo($item->filename, PATHINFO_BASENAME), - 'tags' => $item->new_tags, - 'source' => null, - ]))->images; + $id = add_image($tmpfile, $item->filename, Tag::implode($item->tags))->image_id; - if (count($images) == 0) { - throw new SCoreException("Unable to import file $item->hash"); - } - foreach ($images as $image) { - $event->images[] = $image; - if ($item->source != null) { - $image->set_source($item->source); - } - send_event(new BulkImportEvent($image, $item)); - } - }); + if ($id==-1) { + throw new SCoreException("Unable to import file $item->hash"); + } + $image = Image::by_id($id); + + if ($image==null) { + throw new SCoreException("Unable to import file $item->hash"); + } + + if ($item->source!=null) { + $image->set_source($item->source); + } + send_event(new BulkImportEvent($image, $item)); + + $database->commit(); $total++; } catch (\Exception $ex) { $failed++; + try { + $database->rollBack(); + } catch (\Exception $ex2) { + log_error(BulkImportExportInfo::KEY, "Could not roll back transaction: " . $ex2->getMessage(), "Could not import " . $item->hash . ": " . $ex->getMessage()); + } log_error(BulkImportExportInfo::KEY, "Could not import " . $item->hash . ": " . $ex->getMessage(), "Could not import " . $item->hash . ": " . $ex->getMessage()); + continue; } finally { if (!empty($tmpfile) && is_file($tmpfile)) { unlink($tmpfile); } } } + $event->image_id = -2; // default -1 = upload wasn't handled log_info( BulkImportExportInfo::KEY, @@ -89,7 +99,9 @@ class BulkImportExport extends DataHandlerExtension } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + + + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -98,14 +110,14 @@ class BulkImportExport extends DataHandlerExtension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $user, $page; if ($user->can(Permissions::BULK_EXPORT) && ($event->action == self::EXPORT_ACTION_NAME)) { $download_filename = $user->name . '-' . date('YmdHis') . '.zip'; - $zip_filename = shm_tempnam("bulk_export"); + $zip_filename = tempnam(sys_get_temp_dir(), "shimmie_bulk_export"); $zip = new \ZipArchive(); $json_data = []; @@ -126,7 +138,7 @@ class BulkImportExport extends DataHandlerExtension $zip->addFile($img_loc, $image->hash); } - $json_data = json_encode_ex($json_data, JSON_PRETTY_PRINT); + $json_data = json_encode($json_data, JSON_PRETTY_PRINT); $zip->addFromString(self::EXPORT_INFO_FILE_NAME, $json_data); $zip->close(); @@ -139,7 +151,6 @@ class BulkImportExport extends DataHandlerExtension } } } - // we don't actually do anything, just accept one upload and spawn several protected function media_check_properties(MediaCheckPropertiesEvent $event): void { @@ -150,20 +161,17 @@ class BulkImportExport extends DataHandlerExtension return false; } - protected function create_thumb(Image $image): bool + protected function create_thumb(string $hash, string $mime): bool { return false; } - /** - * @return array - */ private function get_export_data(\ZipArchive $zip): ?array { $info = $zip->getStream(self::EXPORT_INFO_FILE_NAME); if ($info !== false) { try { - $json_string = false_throws(stream_get_contents($info)); + $json_string = stream_get_contents($info); $json_data = json_decode($json_string); return $json_data; } finally { diff --git a/ext/bulk_parent_child/info.php b/ext/bulk_parent_child/info.php index b01ea6e7..4f30a9d8 100644 --- a/ext/bulk_parent_child/info.php +++ b/ext/bulk_parent_child/info.php @@ -10,7 +10,7 @@ class BulkParentChildInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Bulk Parent Child"; - public array $authors = ["Flatty" => ""]; + public array $authors = ["Flatty"=>""]; public string $license = self::LICENSE_WTFPL; public string $description = "Allows bulk setting of parent-child relationships, in order of manual selection"; public array $dependencies = [BulkActionsInfo::KEY]; diff --git a/ext/bulk_parent_child/main.php b/ext/bulk_parent_child/main.php index c318776e..3e3c58bd 100644 --- a/ext/bulk_parent_child/main.php +++ b/ext/bulk_parent_child/main.php @@ -15,7 +15,7 @@ class BulkParentChild extends Extension { private const PARENT_CHILD_ACTION_NAME = "bulk_parent_child"; - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -24,10 +24,10 @@ class BulkParentChild extends Extension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $user, $page, $config; - if ($user->can(Permissions::BULK_PARENT_CHILD) && + if ($user->can(Permissions::BULK_PARENT_CHILD)&& ($event->action == BulkParentChild::PARENT_CHILD_ACTION_NAME)) { $prev_id = null; foreach ($event->items as $image) { diff --git a/ext/comment/main.php b/ext/comment/main.php index 7ad5f1c1..113161fb 100644 --- a/ext/comment/main.php +++ b/ext/comment/main.php @@ -63,10 +63,7 @@ class Comment #[Field] public string $posted; - /** - * @param array $row - */ - public function __construct(array $row) + public function __construct($row) { $this->owner = null; $this->owner_id = (int)$row['user_id']; @@ -87,7 +84,7 @@ class Comment SELECT COUNT(*) AS count FROM comments WHERE owner_id=:owner_id - ", ["owner_id" => $user->id]); + ", ["owner_id"=>$user->id]); } #[Field(name: "owner")] @@ -99,9 +96,6 @@ class Comment return $this->owner; } - /** - * @return Comment[] - */ #[Field(extends: "Post", name: "comments", type: "[Comment!]!")] public static function get_comments(Image $post): array { @@ -122,7 +116,7 @@ class CommentList extends Extension /** @var CommentListTheme $theme */ public Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int('comment_window', 5); @@ -132,7 +126,7 @@ class CommentList extends Extension $config->set_default_bool('comment_captcha', false); } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; if ($this->get_version("ext_comments_version") < 3) { @@ -185,21 +179,21 @@ class CommentList extends Extension } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("comment", new Link('comment/list'), "Comments"); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "comment") { + if ($event->parent=="comment") { $event->add_nav_link("comment_list", new Link('comment/list'), "All"); $event->add_nav_link("comment_help", new Link('ext_doc/comment'), "Help"); } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { if ($event->page_matches("comment")) { switch ($event->get_arg(0)) { @@ -222,14 +216,14 @@ class CommentList extends Extension } } - public function onRobotsBuilding(RobotsBuildingEvent $event): void + public function onRobotsBuilding(RobotsBuildingEvent $event) { // comment lists change all the time, crawlers should // index individual image's comments $event->add_disallow("comment"); } - private function onPageRequest_add(): void + private function onPageRequest_add() { global $user, $page; if (isset($_POST['image_id']) && isset($_POST['comment'])) { @@ -244,7 +238,7 @@ class CommentList extends Extension } } - private function onPageRequest_delete(PageRequestEvent $event): void + private function onPageRequest_delete(PageRequestEvent $event) { global $user, $page; if ($user->can(Permissions::DELETE_COMMENT)) { @@ -260,7 +254,7 @@ class CommentList extends Extension } } - private function onPageRequest_bulk_delete(): void + private function onPageRequest_bulk_delete() { global $user, $database, $page; if ($user->can(Permissions::DELETE_COMMENT) && !empty($_POST["ip"])) { @@ -285,21 +279,24 @@ class CommentList extends Extension } } - private function onPageRequest_list(PageRequestEvent $event): void + private function onPageRequest_list(PageRequestEvent $event) { global $cache, $config, $database, $user; - $threads_per_page = 10; - $where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : ""; - $total_pages = cache_get_or_set("comment_pages", fn () => (int)ceil($database->get_one(" - SELECT COUNT(c1) - FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1 - ") / $threads_per_page), 600); + $total_pages = $cache->get("comment_pages"); + if (is_null($total_pages)) { + $total_pages = (int)ceil($database->get_one(" + SELECT COUNT(c1) + FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1 + ") / 10); + $cache->set("comment_pages", $total_pages, 600); + } $total_pages = max($total_pages, 1); $current_page = $event->try_page_num(1, $total_pages); + $threads_per_page = 10; $start = $threads_per_page * $current_page; $result = $database->execute(" @@ -309,23 +306,23 @@ class CommentList extends Extension GROUP BY image_id ORDER BY latest DESC LIMIT :limit OFFSET :offset - ", ["limit" => $threads_per_page, "offset" => $start]); + ", ["limit"=>$threads_per_page, "offset"=>$start]); - $user_ratings = Extension::is_enabled(RatingsInfo::KEY) ? Ratings::get_user_class_privs($user) : []; + $user_ratings = Extension::is_enabled(RatingsInfo::KEY) ? Ratings::get_user_class_privs($user) : ""; $images = []; while ($row = $result->fetch()) { $image = Image::by_id((int)$row["image_id"]); if ( Extension::is_enabled(RatingsInfo::KEY) && !is_null($image) && - !in_array($image['rating'], $user_ratings) + !in_array($image->rating, $user_ratings) ) { $image = null; // this is "clever", I may live to regret it } if ( Extension::is_enabled(ApprovalInfo::KEY) && !is_null($image) && $config->get_bool(ApprovalConfig::IMAGES) && - $image['approved'] !== true + $image->approved!==true ) { $image = null; } @@ -335,10 +332,10 @@ class CommentList extends Extension } } - $this->theme->display_comment_list($images, $current_page + 1, $total_pages, $user->can(Permissions::CREATE_COMMENT)); + $this->theme->display_comment_list($images, $current_page+1, $total_pages, $user->can(Permissions::CREATE_COMMENT)); } - private function onPageRequest_beta_search(PageRequestEvent $event): void + private function onPageRequest_beta_search(PageRequestEvent $event) { $search = $event->get_arg(1); $page_num = $event->try_page_num(2); @@ -347,29 +344,33 @@ class CommentList extends Extension $com_per_page = 50; $total_pages = (int)ceil($i_comment_count / $com_per_page); $comments = $this->get_user_comments($duser->id, $com_per_page, $page_num * $com_per_page); - $this->theme->display_all_user_comments($comments, $page_num + 1, $total_pages, $duser); + $this->theme->display_all_user_comments($comments, $page_num+1, $total_pages, $duser); } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_admin_block(); } - public function onPostListBuilding(PostListBuildingEvent $event): void + public function onPostListBuilding(PostListBuildingEvent $event) { global $cache, $config; $cc = $config->get_int("comment_count"); if ($cc > 0) { - $recent = cache_get_or_set("recent_comments", fn () => $this->get_recent_comments($cc), 60); + $recent = $cache->get("recent_comments"); + if (is_null($recent)) { + $recent = $this->get_recent_comments($cc); + $cache->set("recent_comments", $recent, 60); + } if (count($recent) > 0) { $this->theme->display_recent_comments($recent); } } } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onUserPageBuilding(UserPageBuildingEvent $event) { - $i_days_old = ((time() - strtotime_ex($event->display_user->join_date)) / 86400) + 1; + $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; $i_comment_count = Comment::count_comments_by_user($event->display_user); $h_comment_rate = sprintf("%.1f", ($i_comment_count / $i_days_old)); $event->add_stats("Comments made: $i_comment_count, $h_comment_rate per day"); @@ -378,7 +379,7 @@ class CommentList extends Extension $this->theme->display_recent_user_comments($recent, $event->display_user); } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $user; $this->theme->display_image_comments( @@ -389,22 +390,22 @@ class CommentList extends Extension } // TODO: split akismet into a separate class, which can veto the event - public function onCommentPosting(CommentPostingEvent $event): void + public function onCommentPosting(CommentPostingEvent $event) { $this->add_comment_wrapper($event->image_id, $event->user, $event->comment); } - public function onCommentDeletion(CommentDeletionEvent $event): void + public function onCommentDeletion(CommentDeletionEvent $event) { global $database; $database->execute(" DELETE FROM comments WHERE id=:comment_id - ", ["comment_id" => $event->comment_id]); + ", ["comment_id"=>$event->comment_id]); log_info("comment", "Deleting Comment #{$event->comment_id}"); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Comment Options"); $sb->add_bool_option("comment_captcha", "Require CAPTCHA for anonymous comments: "); @@ -423,7 +424,7 @@ class CommentList extends Extension $sb->add_bool_option("comment_samefags_public"); } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -443,9 +444,9 @@ class CommentList extends Extension } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Comments"; $block->body = $this->theme->get_help_html(); @@ -454,8 +455,7 @@ class CommentList extends Extension } /** - * @param array $args - * @return Comment[] + * #return Comment[] */ private static function get_generic_comments(string $query, array $args): array { @@ -469,7 +469,7 @@ class CommentList extends Extension } /** - * @return Comment[] + * #return Comment[] */ private static function get_recent_comments(int $count): array { @@ -483,13 +483,13 @@ class CommentList extends Extension LEFT JOIN users ON comments.owner_id=users.id ORDER BY comments.id DESC LIMIT :limit - ", ["limit" => $count]); + ", ["limit"=>$count]); } /** - * @return Comment[] + * #return Comment[] */ - private static function get_user_comments(int $user_id, int $count, int $offset = 0): array + private static function get_user_comments(int $user_id, int $count, int $offset=0): array { return CommentList::get_generic_comments(" SELECT @@ -502,12 +502,12 @@ class CommentList extends Extension WHERE users.id = :user_id ORDER BY comments.id DESC LIMIT :limit OFFSET :offset - ", ["user_id" => $user_id, "offset" => $offset, "limit" => $count]); + ", ["user_id"=>$user_id, "offset"=>$offset, "limit"=>$count]); } /** * public just for Image::get_comments() - * @return Comment[] + * #return Comment[] */ public static function get_comments(int $image_id): array { @@ -521,7 +521,7 @@ class CommentList extends Extension LEFT JOIN users ON comments.owner_id=users.id WHERE comments.image_id=:image_id ORDER BY comments.id ASC - ", ["image_id" => $image_id]); + ", ["image_id"=>$image_id]); } private function is_comment_limit_hit(): bool @@ -547,7 +547,7 @@ class CommentList extends Extension SELECT * FROM comments WHERE owner_ip = :remote_ip AND posted > now() - $window_sql - ", ["remote_ip" => get_real_ip()]); + ", ["remote_ip"=>get_real_ip()]); return (count($result) >= $max); } @@ -572,8 +572,7 @@ class CommentList extends Extension private function is_spam_akismet(string $text): bool { global $config, $user; - $key = $config->get_string('comment_wordpress_key'); - if (!is_null($key) && strlen($key) > 0) { + if (strlen($config->get_string('comment_wordpress_key')) > 0) { $comment = [ 'author' => $user->name, 'email' => $user->email, @@ -585,7 +584,11 @@ class CommentList extends Extension ]; // @phpstan-ignore-next-line - $akismet = new \Akismet($_SERVER['SERVER_NAME'], $key, $comment); + $akismet = new \Akismet( + $_SERVER['SERVER_NAME'], + $config->get_string('comment_wordpress_key'), + $comment + ); // @phpstan-ignore-next-line if ($akismet->errorsExist()) { @@ -606,11 +609,11 @@ class CommentList extends Extension SELECT * FROM comments WHERE image_id=:image_id AND comment=:comment - ", ["image_id" => $image_id, "comment" => $comment]); + ", ["image_id"=>$image_id, "comment"=>$comment]); } // do some checks - private function add_comment_wrapper(int $image_id, User $user, string $comment): void + private function add_comment_wrapper(int $image_id, User $user, string $comment) { global $database, $page; @@ -621,12 +624,12 @@ class CommentList extends Extension // all checks passed if ($user->is_anonymous()) { - $page->add_cookie("nocache", "Anonymous Commenter", time() + 60 * 60 * 24, "/"); + $page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/"); } $database->execute( "INSERT INTO comments(image_id, owner_id, owner_ip, posted, comment) ". "VALUES(:image_id, :user_id, :remote_addr, now(), :comment)", - ["image_id" => $image_id, "user_id" => $user->id, "remote_addr" => get_real_ip(), "comment" => $comment] + ["image_id"=>$image_id, "user_id"=>$user->id, "remote_addr"=>get_real_ip(), "comment"=>$comment] ); $cid = $database->get_last_insert_id('comments_id_seq'); $snippet = substr($comment, 0, 100); @@ -635,7 +638,7 @@ class CommentList extends Extension log_info("comment", "Comment #$cid added to >>$image_id: $snippet"); } - private function comment_checks(int $image_id, User $user, string $comment): void + private function comment_checks(int $image_id, User $user, string $comment) { global $config, $page; @@ -651,10 +654,10 @@ class CommentList extends Extension } // advanced sanity checks - elseif (strlen($comment) / strlen(false_throws(gzcompress($comment))) > 10) { + elseif (strlen($comment)/strlen(gzcompress($comment)) > 10) { throw new CommentPostingException("Comment too repetitive~"); } elseif ($user->is_anonymous() && !$this->hash_match()) { - $page->add_cookie("nocache", "Anonymous Commenter", time() + 60 * 60 * 24, "/"); + $page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/"); throw new CommentPostingException( "Comment submission form is out of date; refresh the ". "comment form to show you aren't a spammer~" diff --git a/ext/comment/script.js b/ext/comment/script.js index a6f15345..47023be7 100644 --- a/ext/comment/script.js +++ b/ext/comment/script.js @@ -1,8 +1,8 @@ function replyTo(imageId, commentId, userId) { - var box = document.getElementById("comment_on_"+imageId); + var box = $("#comment_on_"+imageId); var text = "[url=site://post/view/"+imageId+"#c"+commentId+"]@"+userId+"[/url]: "; box.focus(); - box.value += text; + box.val(box.val() + text); $("#c"+commentId).highlight(); } diff --git a/ext/comment/test.php b/ext/comment/test.php index 9d7e9fdb..4bafa098 100644 --- a/ext/comment/test.php +++ b/ext/comment/test.php @@ -21,7 +21,7 @@ class CommentListTest extends ShimmiePHPUnitTestCase parent::tearDown(); } - public function testCommentsPage(): void + public function testCommentsPage() { global $user; @@ -90,7 +90,7 @@ class CommentListTest extends ShimmiePHPUnitTestCase $this->assert_no_text('ASDFASDF'); } - public function testSingleDel(): void + public function testSingleDel() { global $database, $user; diff --git a/ext/comment/theme.php b/ext/comment/theme.php index b5bf49cf..6f92e7ef 100644 --- a/ext/comment/theme.php +++ b/ext/comment/theme.php @@ -8,15 +8,12 @@ class CommentListTheme extends Themelet { private bool $show_anon_id = false; private int $anon_id = 1; - /** @var array */ private array $anon_map = []; /** * Display a page with a list of images, and for each image, the image's comments. - * - * @param array $images */ - public function display_comment_list(array $images, int $page_number, int $total_pages, bool $can_post): void + public function display_comment_list(array $images, int $page_number, int $total_pages, bool $can_post) { global $config, $page, $user; @@ -32,7 +29,7 @@ class CommentListTheme extends Themelet $h_prev = ($page_number <= 1) ? "Prev" : 'Prev'; - $h_index = "Index"; + $h_index = "Index"; $h_next = ($page_number >= $total_pages) ? "Next" : 'Next'; @@ -59,7 +56,7 @@ class CommentListTheme extends Themelet $comment_count = count($comments); if ($comment_limit > 0 && $comment_count > $comment_limit) { $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; - $comments = array_slice($comments, negative_int($comment_limit)); + $comments = array_slice($comments, -$comment_limit); $this->show_anon_id = false; } else { $this->show_anon_id = true; @@ -94,7 +91,7 @@ class CommentListTheme extends Themelet } } - public function display_admin_block(): void + public function display_admin_block() { global $page; @@ -114,9 +111,9 @@ class CommentListTheme extends Themelet /** * Add some comments to the page, probably in a sidebar. * - * @param Comment[] $comments An array of Comment objects to be shown + * #param Comment[] $comments An array of Comment objects to be shown */ - public function display_recent_comments(array $comments): void + public function display_recent_comments(array $comments) { global $page; $this->show_anon_id = false; @@ -131,9 +128,9 @@ class CommentListTheme extends Themelet /** * Show comments for an image. * - * @param Comment[] $comments + * #param Comment[] $comments */ - public function display_image_comments(Image $image, array $comments, bool $postbox): void + public function display_image_comments(Image $image, array $comments, bool $postbox) { global $page; $this->show_anon_id = true; @@ -150,9 +147,9 @@ class CommentListTheme extends Themelet /** * Show comments made by a user. * - * @param Comment[] $comments + * #param Comment[] $comments */ - public function display_recent_user_comments(array $comments, User $user): void + public function display_recent_user_comments(array $comments, User $user) { global $page; $html = ""; @@ -167,10 +164,7 @@ class CommentListTheme extends Themelet $page->add_block(new Block("Comments", $html, "left", 70, "comment-list-user")); } - /** - * @param Comment[] $comments - */ - public function display_all_user_comments(array $comments, int $page_number, int $total_pages, User $user): void + public function display_all_user_comments(array $comments, int $page_number, int $total_pages, User $user) { global $page; @@ -192,7 +186,7 @@ class CommentListTheme extends Themelet //$query = empty($u_tags) ? "" : '/'.$u_tags; $h_prev = ($page_number <= 1) ? "Prev" : "Prev"; - $h_index = "Index"; + $h_index = "Index"; $h_next = ($page_number >= $total_pages) ? "Next" : "Next"; $page->set_title(html_escape($user->name)."'s comments"); @@ -200,7 +194,7 @@ class CommentListTheme extends Themelet $this->display_paginator($page, "comment/beta-search/{$user->name}", null, $page_number, $total_pages); } - protected function comment_to_html(Comment $comment, bool $trim = false): string + protected function comment_to_html(Comment $comment, bool $trim=false): string { global $config, $user; @@ -255,7 +249,7 @@ class CommentListTheme extends Themelet $h_del = ""; if ($user->can(Permissions::DELETE_COMMENT)) { $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); - $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview") ?: "Delete "; + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); $h_del = " - Del"; diff --git a/ext/cron_uploader/info.php b/ext/cron_uploader/info.php index 3fdefddb..2113f504 100644 --- a/ext/cron_uploader/info.php +++ b/ext/cron_uploader/info.php @@ -20,7 +20,7 @@ class CronUploaderInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Cron Uploader"; public string $url = self::SHIMMIE_URL; - public array $authors = ["YaoiFox" => "admin@yaoifox.com", "Matthew Barbour" => "matthew@darkholme.net"]; + public array $authors = ["YaoiFox"=>"admin@yaoifox.com", "Matthew Barbour"=>"matthew@darkholme.net"]; public string $license = self::LICENSE_GPLV2; public string $description = "Uploads images automatically using Cron Jobs"; diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php index e2da4392..8690ce48 100644 --- a/ext/cron_uploader/main.php +++ b/ext/cron_uploader/main.php @@ -21,7 +21,7 @@ class CronUploader extends Extension private static bool $IMPORT_RUNNING = false; - public function onInitUserConfig(InitUserConfigEvent $event): void + public function onInitUserConfig(InitUserConfigEvent $event) { $event->user_config->set_default_string( CronUploaderConfig::DIR, @@ -32,7 +32,7 @@ class CronUploader extends Extension $event->user_config->set_default_int(CronUploaderConfig::LOG_LEVEL, SCORE_LOG_INFO); } - public function onUserOptionsBuilding(UserOptionsBuildingEvent $event): void + public function onUserOptionsBuilding(UserOptionsBuildingEvent $event) { if ($event->user->can(Permissions::CRON_ADMIN)) { $documentation_link = make_http(make_link("cron_upload")); @@ -55,9 +55,9 @@ class CronUploader extends Extension } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "system") { + if ($event->parent=="system") { $event->add_nav_link("cron_docs", new Link('cron_upload'), "Cron Upload"); } } @@ -66,12 +66,12 @@ class CronUploader extends Extension * Checks if the cron upload page has been accessed * and initializes the upload. */ - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $user; if ($event->page_matches("cron_upload")) { - if ($event->count_args() == 1 && $event->get_arg(0) == "run") { + if ($event->count_args() == 1 && $event->get_arg(0) =="run") { $this->process_upload(); // Start upload } elseif ($user->can(Permissions::CRON_RUN)) { $this->display_documentation(); @@ -79,7 +79,7 @@ class CronUploader extends Extension } } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $failed_dir = $this->get_failed_dir(); $results = get_dir_contents($failed_dir); @@ -95,7 +95,7 @@ class CronUploader extends Extension $this->theme->display_form($failed_dirs); } - public function onAdminAction(AdminActionEvent $event): void + public function onAdminAction(AdminActionEvent $event) { $action = $event->action; switch ($action) { @@ -120,14 +120,14 @@ class CronUploader extends Extension } } - public function onLog(LogEvent $event): void + public function onLog(LogEvent $event) { global $user_config; if (self::$IMPORT_RUNNING) { $all = $user_config->get_bool(CronUploaderConfig::INCLUDE_ALL_LOGS); if ($event->priority >= $user_config->get_int(CronUploaderConfig::LOG_LEVEL) && - ($event->section == self::NAME || $all)) { + ($event->section==self::NAME || $all)) { $output = "[" . date('Y-m-d H:i:s') . "] " . ($all ? '[' . $event->section . '] ' : '') . "[" . LOGGING_LEVEL_NAMES[$event->priority] . "] " . $event->message; echo $output . "\r\n"; @@ -139,7 +139,7 @@ class CronUploader extends Extension } } - private function restage_folder(string $folder): void + private function restage_folder(string $folder) { global $page; if (empty($folder)) { @@ -157,7 +157,7 @@ class CronUploader extends Extension $results = get_files_recursively($stage_dir); if (count($results) == 0) { - if (remove_empty_dirs($stage_dir) === false) { + if (remove_empty_dirs($stage_dir)===false) { $page->flash("Nothing to stage from $folder, cannot remove folder"); } else { $page->flash("Nothing to stage from $folder, removing folder"); @@ -182,21 +182,21 @@ class CronUploader extends Extension mkdir($dir, 0775, true); } - if (rename($result, $new_path) === false) { + if (rename($result, $new_path)===false) { $page->flash("Could not move file: " .$result); $success = false; } } - if ($success === true) { + if ($success===true) { $page->flash("Re-staged $folder to queue"); - if (remove_empty_dirs($stage_dir) === false) { + if (remove_empty_dirs($stage_dir)===false) { $page->flash("Could not remove $folder"); } } } - private function clear_folder(string $folder): void + private function clear_folder($folder) { global $page, $user_config; $path = join_path($user_config->get_string(CronUploaderConfig::DIR), $folder); @@ -235,7 +235,7 @@ class CronUploader extends Extension $running = false; - $lockfile = false_throws(fopen($this->get_lock_file(), "w")); + $lockfile = fopen($this->get_lock_file(), "w"); try { if (!flock($lockfile, LOCK_EX | LOCK_NB)) { $running = true; @@ -325,7 +325,7 @@ class CronUploader extends Extension { global $database, $user, $user_config, $config, $_shm_load_start; - $max_time = intval(ini_get('max_execution_time')) * .8; + $max_time = intval(ini_get('max_execution_time'))*.8; $this->set_headers(); @@ -343,7 +343,7 @@ class CronUploader extends Extension throw new SCoreException("User does not have permission to run cron upload"); } - $lockfile = false_throws(fopen($this->get_lock_file(), "w")); + $lockfile = fopen($this->get_lock_file(), "w"); if (!flock($lockfile, LOCK_EX | LOCK_NB)) { throw new SCoreException("Cron upload process is already running"); } @@ -365,25 +365,34 @@ class CronUploader extends Extension // Upload the file(s) foreach ($image_queue as $img) { $execution_time = ftime() - $_shm_load_start; - if ($execution_time > $max_time) { + if ($execution_time>$max_time) { break; } else { $remaining = $max_time - $execution_time; $this->log_message(SCORE_LOG_DEBUG, "Max run time remaining: $remaining"); } try { - $result = $database->with_savepoint(function () use ($img, $output_subdir) { - $this->log_message(SCORE_LOG_INFO, "Adding file: {$img[0]} - tags: {$img[2]}"); - $result = $this->add_image($img[0], $img[1], $img[2]); - $this->move_uploaded($img[0], $img[1], $output_subdir, false); - return $result; - }); + $database->begin_transaction(); + $this->log_message(SCORE_LOG_INFO, "Adding file: {$img[0]} - tags: {$img[2]}"); + $result = $this->add_image($img[0], $img[1], $img[2]); + if ($database->is_transaction_open()) { + $database->commit(); + } + $this->move_uploaded($img[0], $img[1], $output_subdir, false); if ($result->merged) { $merged++; } else { $added++; } } catch (\Exception $e) { + try { + if ($database->is_transaction_open()) { + $database->rollback(); + } + } catch (\Exception $e) { + // rollback failed, let's just log things and die + } + $failed++; $this->log_message(SCORE_LOG_ERROR, "(" . gettype($e) . ") " . $e->getMessage()); $this->log_message(SCORE_LOG_ERROR, $e->getTraceAsString()); @@ -396,7 +405,7 @@ class CronUploader extends Extension } // Throw exception if there's nothing in the queue - if ($merged + $failed + $added === 0) { + if ($merged+$failed+$added === 0) { $this->log_message(SCORE_LOG_WARNING, "Your queue is empty so nothing could be uploaded."); return false; } @@ -414,19 +423,19 @@ class CronUploader extends Extension } } - private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false): void + private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false) { global $user_config; $rootDir = $user_config->get_string(CronUploaderConfig::DIR); $rootLength = strlen($rootDir); - if ($rootDir[$rootLength - 1] == "/" || $rootDir[$rootLength - 1] == "\\") { + if ($rootDir[$rootLength-1]=="/"||$rootDir[$rootLength-1]=="\\") { $rootLength--; } $relativeDir = dirname(substr($path, $rootLength + 7)); - if ($relativeDir == ".") { + if ($relativeDir==".") { $relativeDir = ""; } @@ -454,24 +463,18 @@ class CronUploader extends Extension /** * Generate the necessary DataUploadEvent for a given image and tags. - * - * @param string[] $tags */ - private function add_image(string $tmpname, string $filename, array $tags): DataUploadEvent + private function add_image(string $tmpname, string $filename, string $tags): DataUploadEvent { - $event = send_event(new DataUploadEvent($tmpname, [ - 'filename' => pathinfo($filename, PATHINFO_BASENAME), - 'tags' => $tags, - 'source' => null, - ])); + $event = add_image($tmpname, $filename, $tags, null); // Generate info message - if (count($event->images) == 0) { + if ($event->image_id == -1) { throw new UploadException("File type not recognised (".$event->mime."). Filename: {$filename}"); } elseif ($event->merged === true) { - $infomsg = "Post merged. ID: {$event->images[0]->id} - Filename: {$filename}"; + $infomsg = "Post merged. ID: {$event->image_id} - Filename: {$filename}"; } else { - $infomsg = "Post uploaded. ID: {$event->images[0]->id} - Filename: {$filename}"; + $infomsg = "Post uploaded. ID: {$event->image_id} - Filename: {$filename}"; } $this->log_message(SCORE_LOG_INFO, $infomsg); @@ -479,7 +482,7 @@ class CronUploader extends Extension } private const PARTIAL_DOWNLOAD_EXTENSIONS = ['crdownload','part']; - private const SKIPPABLE_FILES = ['.ds_store', 'thumbs.db', 'desktop.ini', '.listing']; + private const SKIPPABLE_FILES = ['.ds_store','thumbs.db']; private function is_skippable_file(string $path): bool { diff --git a/ext/cron_uploader/style.css b/ext/cron_uploader/style.css index 770152bd..2643a6a1 100644 --- a/ext/cron_uploader/style.css +++ b/ext/cron_uploader/style.css @@ -1,3 +1,3 @@ -table.cron_uploader_log th { +table.log th { width: 200px; } \ No newline at end of file diff --git a/ext/cron_uploader/theme.php b/ext/cron_uploader/theme.php index 36aec0e1..cc293f59 100644 --- a/ext/cron_uploader/theme.php +++ b/ext/cron_uploader/theme.php @@ -17,12 +17,6 @@ use function MicroHTML\emptyHTML; class CronUploaderTheme extends Themelet { - /** - * @param array{path:string,total_files:int,total_mb:string} $queue_dirinfo - * @param array{path:string,total_files:int,total_mb:string} $uploaded_dirinfo - * @param array{path:string,total_files:int,total_mb:string} $failed_dirinfo - * @param array|null $log_entries - */ public function display_documentation( bool $running, array $queue_dirinfo, @@ -31,7 +25,7 @@ class CronUploaderTheme extends Themelet string $cron_cmd, string $cron_url, ?array $log_entries - ): void { + ) { global $page, $config, $user_config; $info_html = ""; @@ -40,7 +34,7 @@ class CronUploaderTheme extends Themelet $page->set_heading("Cron Uploader"); if (!$config->get_bool(UserConfig::ENABLE_API_KEYS)) { - $info_html .= "THIS EXTENSION REQUIRES USER API KEYS TO BE ENABLED IN BOARD ADMIN
    "; + $info_html .= "THIS EXTENSION REQUIRES USER API KEYS TO BE ENABLED IN BOARD ADMIN"; } $info_html .= "Information @@ -90,7 +84,7 @@ class CronUploaderTheme extends Themelet "; - $max_time = intval(ini_get('max_execution_time')) * .8; + $max_time = intval(ini_get('max_execution_time'))*.8; $usage_html = "Upload your images you want to be uploaded to the queue directory using your FTP client or other means.
    ({$queue_dirinfo['path']}) @@ -115,13 +109,13 @@ class CronUploaderTheme extends Themelet $block = new Block("Cron Uploader", $info_html, "main", 10); $block_install = new Block("Setup Guide", $install_html, "main", 30); - $block_usage = new Block("Usage Guide", $usage_html, "main", 20); + $block_usage= new Block("Usage Guide", $usage_html, "main", 20); $page->add_block($block); $page->add_block($block_install); $page->add_block($block_usage); if (!empty($log_entries)) { - $log_html = ""; + $log_html = "
    "; foreach ($log_entries as $entry) { $log_html .= ""; } @@ -136,28 +130,28 @@ class CronUploaderTheme extends Themelet $form = SHM_SIMPLE_FORM( "user_admin/cron_uploader", TABLE( - ["class" => "form"], + ["class"=>"form"], TBODY( TR( TH("Cron Uploader") ), TR( TH("Root dir"), - TD(INPUT(["type" => 'text', "name" => 'name', "required" => true])) + TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) ), TR( TH(), TD( - LABEL(INPUT(["type" => 'checkbox', "name" => 'stop_on_error']), "Stop On Error") + LABEL(INPUT(["type"=>'checkbox', "name"=>'stop_on_error']), "Stop On Error") ) ), TR( TH(rawHTML("Repeat Password")), - TD(INPUT(["type" => 'password', "name" => 'pass2', "required" => true])) + TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) ) ), TFOOT( - TR(TD(["colspan" => "2"], INPUT(["type" => "submit", "value" => "Save Settings"]))) + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Save Settings"]))) ) ) ); @@ -165,10 +159,7 @@ class CronUploaderTheme extends Themelet return (string)$html; } - /** - * @param string[] $failed_dirs - */ - public function display_form(array $failed_dirs): void + public function display_form(array $failed_dirs) { global $page; diff --git a/ext/custom_html_headers/info.php b/ext/custom_html_headers/info.php index df49d48b..6cdee477 100644 --- a/ext/custom_html_headers/info.php +++ b/ext/custom_html_headers/info.php @@ -11,7 +11,7 @@ class CustomHtmlHeadersInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Custom HTML Headers"; public string $url = "http://www.drudexsoftware.com"; - public array $authors = ["Drudex Software" => "support@drudexsoftware.com"]; + public array $authors = ["Drudex Software"=>"support@drudexsoftware.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Allows admins to modify & set custom <head> content"; public ?string $documentation = diff --git a/ext/custom_html_headers/main.php b/ext/custom_html_headers/main.php index 87f90033..0cabae53 100644 --- a/ext/custom_html_headers/main.php +++ b/ext/custom_html_headers/main.php @@ -7,7 +7,7 @@ namespace Shimmie2; class CustomHtmlHeaders extends Extension { # Adds setup block for custom content - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Custom HTML Headers"); @@ -25,30 +25,30 @@ class CustomHtmlHeaders extends Extension ], "
    Add website name in title"); } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string("sitename_in_title", "none"); } # Load Analytics tracking code on page request - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { $this->handle_custom_html_headers(); $this->handle_modified_page_title(); } - private function handle_custom_html_headers(): void + private function handle_custom_html_headers() { global $config, $page; $header = $config->get_string('custom_html_headers', ''); - if ($header != '') { + if ($header!='') { $page->add_html_header($header); } } - private function handle_modified_page_title(): void + private function handle_modified_page_title() { global $config, $page; diff --git a/ext/danbooru_api/info.php b/ext/danbooru_api/info.php index 2c2f9e71..06312b0f 100644 --- a/ext/danbooru_api/info.php +++ b/ext/danbooru_api/info.php @@ -10,7 +10,7 @@ class DanbooruApiInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Danbooru Client API"; - public array $authors = ["JJS" => "jsutinen@gmail.com"]; + public array $authors = ["JJS"=>"jsutinen@gmail.com"]; public string $description = "Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie"; public ?string $documentation = "Notes: diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php index 6d515ce4..95b56e77 100644 --- a/ext/danbooru_api/main.php +++ b/ext/danbooru_api/main.php @@ -6,30 +6,18 @@ namespace Shimmie2; use MicroHTML\HTMLElement; -/** - * @param mixed[] ...$args - */ function TAGS(...$args): HTMLElement { return new HTMLElement("tags", $args); } -/** - * @param mixed[] ...$args - */ function TAG(...$args): HTMLElement { return new HTMLElement("tag", $args); } -/** - * @param mixed[] ...$args - */ function POSTS(...$args): HTMLElement { return new HTMLElement("posts", $args); } -/** - * @param mixed[] ...$args - */ function POST(...$args): HTMLElement { return new HTMLElement("post", $args); @@ -38,7 +26,7 @@ function POST(...$args): HTMLElement class DanbooruApi extends Extension { - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { if ($event->page_matches("api/danbooru")) { global $page; @@ -87,7 +75,6 @@ class DanbooruApi extends Extension $user = $duser; } else { $user = User::by_id($config->get_int("anon_id", 0)); - assert(!is_null($user)); } send_event(new UserLoginEvent($user)); } @@ -112,7 +99,7 @@ class DanbooruApi extends Extension foreach ($idlist as $id) { $sqlresult = $database->get_all( "SELECT id,tag,count FROM tags WHERE id = :id", - ['id' => $id] + ['id'=>$id] ); foreach ($sqlresult as $row) { $results[] = [$row['count'], $row['tag'], $row['id']]; @@ -123,7 +110,7 @@ class DanbooruApi extends Extension foreach ($namelist as $name) { $sqlresult = $database->get_all( "SELECT id,tag,count FROM tags WHERE LOWER(tag) = LOWER(:tag)", - ['tag' => $name] + ['tag'=>$name] ); foreach ($sqlresult as $row) { $results[] = [$row['count'], $row['tag'], $row['id']]; @@ -142,7 +129,7 @@ class DanbooruApi extends Extension $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0; $sqlresult = $database->get_all( "SELECT id,tag,count FROM tags WHERE count > 0 AND id >= :id ORDER BY id DESC", - ['id' => $start] + ['id'=>$start] ); foreach ($sqlresult as $row) { $results[] = [$row['count'], $row['tag'], $row['id']]; @@ -173,6 +160,8 @@ class DanbooruApi extends Extension * - limit: limit * - page: page number * - after_id: limit results to posts added after this id + * + * #return string */ private function api_find_posts(): HTMLElement { @@ -210,14 +199,13 @@ class DanbooruApi extends Extension $tags = array_filter($tags, static function ($element) { return $element !== "*"; }); - $tags = array_values($tags); // reindex array because count_images() expects a 0-based array - $count = Search::count_images($tags); - $results = Search::find_images(max($start, 0), min($limit, 100), $tags); + $count = Image::count_images($tags); + $results = Image::find_images(max($start, 0), min($limit, 100), $tags); } // Now we have the array $results filled with Image objects // Let's display them - $xml = POSTS(["count" => $count, "offset" => $start]); + $xml = POSTS(["count"=>$count, "offset"=>$start]); foreach ($results as $img) { // Sanity check to see if $img is really an image object // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this @@ -278,7 +266,8 @@ class DanbooruApi extends Extension */ private function api_add_post(): void { - global $database, $user, $page; + global $user, $page; + $danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path // Check first if a login was supplied, if it wasn't check if the user is logged in via cookie // If all that fails, it's an anonymous upload @@ -311,13 +300,11 @@ class DanbooruApi extends Extension } } elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source']; - $file = shm_tempnam("transload"); - assert($file !== false); - try { - fetch_url($source, $file); - } catch(FetchException $e) { + $file = tempnam(sys_get_temp_dir(), "shimmie_transload"); + $ok = fetch_url($source, $file); + if (!$ok) { $page->set_code(409); - $page->add_http_header("X-Danbooru-Errors: $e"); + $page->add_http_header("X-Danbooru-Errors: fopen read error"); return; } $filename = basename($source); @@ -332,7 +319,6 @@ class DanbooruApi extends Extension // Was an md5 supplied? Does it match the file hash? $hash = md5_file($file); - assert($hash !== false); if (isset($_REQUEST['md5']) && strtolower($_REQUEST['md5']) != $hash) { $page->set_code(409); $page->add_http_header("X-Danbooru-Errors: md5 mismatch"); @@ -347,7 +333,9 @@ class DanbooruApi extends Extension $page->set_code(409); $page->add_http_header("X-Danbooru-Errors: duplicate"); $existinglink = make_link("post/view/" . $existing->id); - $existinglink = make_http($existinglink); + if ($danboorup_kludge) { + $existinglink = make_http($existinglink); + } $page->add_http_header("X-Danbooru-Location: $existinglink"); return; } @@ -356,21 +344,15 @@ class DanbooruApi extends Extension //log_debug("danbooru_api", "upload($filename): fileinfo(".var_export($fileinfo,TRUE)."), metadata(".var_export($metadata,TRUE).")..."); try { - $newimg = $database->with_savepoint(function () use ($file, $filename, $posttags, $source) { - // Fire off an event which should process the new file and add it to the db - $dae = send_event(new DataUploadEvent($file, [ - 'filename' => pathinfo($filename, PATHINFO_BASENAME), - 'tags' => $posttags, - 'source' => $source, - ])); - - //log_debug("danbooru_api", "send_event(".var_export($nevent,TRUE).")"); - // If it went ok, grab the id for the newly uploaded image and pass it in the header - return $dae->images[0]; - }); - + // Fire off an event which should process the new file and add it to the db + $nevent = add_image($file, $filename, $posttags, $source); + //log_debug("danbooru_api", "send_event(".var_export($nevent,TRUE).")"); + // If it went ok, grab the id for the newly uploaded image and pass it in the header + $newimg = Image::by_hash($hash); // FIXME: Unsupported file doesn't throw an error? $newid = make_link("post/view/" . $newimg->id); - $newid = make_http($newid); + if ($danboorup_kludge) { + $newid = make_http($newid); + } // Did we POST or GET this call? if ($_SERVER['REQUEST_METHOD'] == 'POST') { @@ -379,6 +361,7 @@ class DanbooruApi extends Extension $page->add_http_header("Location: $newid"); } } catch (UploadException $ex) { + // Did something screw up? $page->set_code(409); $page->add_http_header("X-Danbooru-Errors: exception - " . $ex->getMessage()); } diff --git a/ext/danbooru_api/test.php b/ext/danbooru_api/test.php index bf72e247..258ad855 100644 --- a/ext/danbooru_api/test.php +++ b/ext/danbooru_api/test.php @@ -6,20 +6,20 @@ namespace Shimmie2; class DanbooruApiTest extends ShimmiePHPUnitTestCase { - public function testSearch(): void + public function testSearch() { $this->log_in_as_admin(); $image_id = $this->post_image("tests/bedroom_workshop.jpg", "data"); $this->get_page("api/danbooru/find_posts"); - $this->get_page("api/danbooru/find_posts", ["id" => $image_id]); - $this->get_page("api/danbooru/find_posts", ["md5" => "17fc89f372ed3636e28bd25cc7f3bac1"]); - $this->get_page("api/danbooru/find_posts", ["tags" => "*"]); + $this->get_page("api/danbooru/find_posts", ["id"=>$image_id]); + $this->get_page("api/danbooru/find_posts", ["md5"=>"17fc89f372ed3636e28bd25cc7f3bac1"]); + $this->get_page("api/danbooru/find_posts", ["tags"=>"*"]); $this->get_page("api/danbooru/find_tags"); - $this->get_page("api/danbooru/find_tags", ["id" => 1]); - $this->get_page("api/danbooru/find_tags", ["name" => "data"]); + $this->get_page("api/danbooru/find_tags", ["id"=>1]); + $this->get_page("api/danbooru/find_tags", ["name"=>"data"]); $page = $this->get_page("api/danbooru/post/show/$image_id"); $this->assertEquals(302, $page->code); diff --git a/ext/download/info.php b/ext/download/info.php index 51d5b1f8..4eb22416 100644 --- a/ext/download/info.php +++ b/ext/download/info.php @@ -10,7 +10,7 @@ class DownloadInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Download"; - 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 = "System-wide download functions"; public bool $core = true; diff --git a/ext/download/main.php b/ext/download/main.php index d46a3d45..52f42888 100644 --- a/ext/download/main.php +++ b/ext/download/main.php @@ -14,13 +14,17 @@ class Download extends Extension return 99; } - public function onImageDownloading(ImageDownloadingEvent $event): void + + public function onImageDownloading(ImageDownloadingEvent $event) { global $page; $page->set_mime($event->mime); + $page->set_mode(PageMode::FILE); + $page->set_file($event->path, $event->file_modified); + $event->stop_processing = true; } } diff --git a/ext/download/test.php b/ext/download/test.php deleted file mode 100644 index 26c79c4e..00000000 --- a/ext/download/test.php +++ /dev/null @@ -1,16 +0,0 @@ -post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - $this->get_page("image/$image_id"); - $this->assertEquals(PageMode::FILE, $page->mode); - } -} diff --git a/ext/downtime/main.php b/ext/downtime/main.php index d931a6b8..28d6aeef 100644 --- a/ext/downtime/main.php +++ b/ext/downtime/main.php @@ -14,14 +14,14 @@ class Downtime extends Extension return 10; } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Downtime"); $sb->add_bool_option("downtime", "Disable non-admin access: "); $sb->add_longtext_option("downtime_message", "
    "); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page, $user; diff --git a/ext/downtime/test.php b/ext/downtime/test.php index 77dfa261..d27638b9 100644 --- a/ext/downtime/test.php +++ b/ext/downtime/test.php @@ -10,10 +10,9 @@ class DowntimeTest extends ShimmiePHPUnitTestCase { global $config; $config->set_bool("downtime", false); - parent::tearDown(); } - public function testDowntime(): void + public function testDowntime() { global $config; diff --git a/ext/downtime/theme.php b/ext/downtime/theme.php index 495c7cd6..6876eaf6 100644 --- a/ext/downtime/theme.php +++ b/ext/downtime/theme.php @@ -9,11 +9,11 @@ class DowntimeTheme extends Themelet /** * Show the admin that downtime mode is enabled */ - public function display_notification(Page $page): void + public function display_notification(Page $page) { $page->add_block(new Block( "Downtime", - "DOWNTIME MODE IS ON!", + "DOWNTIME MODE IS ON!", "left", 0 )); @@ -22,7 +22,7 @@ class DowntimeTheme extends Themelet /** * Display $message and exit */ - public function display_message(string $message): void + public function display_message(string $message) { global $config, $user, $page; $theme_name = $config->get_string(SetupConfig::THEME); diff --git a/ext/emoticons/test.php b/ext/emoticons/test.php index 9606d9e2..0bcd20ee 100644 --- a/ext/emoticons/test.php +++ b/ext/emoticons/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class EmoticonsTest extends ShimmiePHPUnitTestCase { - public function testEmoticons(): void + public function testEmoticons() { global $user; diff --git a/ext/emoticons_list/main.php b/ext/emoticons_list/main.php index a0dd7271..07bd6f80 100644 --- a/ext/emoticons_list/main.php +++ b/ext/emoticons_list/main.php @@ -12,10 +12,10 @@ class EmoticonList extends Extension /** @var EmoticonListTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { if ($event->page_matches("emote/list")) { - $this->theme->display_emotes(glob_ex("ext/emoticons/default/*")); + $this->theme->display_emotes(glob("ext/emoticons/default/*")); } } } diff --git a/ext/emoticons_list/theme.php b/ext/emoticons_list/theme.php index 5d770d70..1f01a34b 100644 --- a/ext/emoticons_list/theme.php +++ b/ext/emoticons_list/theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class EmoticonListTheme extends Themelet { - /** - * @param string[] $list - */ - public function display_emotes(array $list): void + public function display_emotes(array $list) { global $page; $data_href = get_base_href(); diff --git a/ext/eokm/main.php b/ext/eokm/main.php index dd1e9f52..83b98f32 100644 --- a/ext/eokm/main.php +++ b/ext/eokm/main.php @@ -11,14 +11,14 @@ class Eokm extends Extension return 40; } // early, to veto ImageUploadEvent - public function onImageAddition(ImageAdditionEvent $event): void + public function onImageAddition(ImageAdditionEvent $event) { global $config; $username = $config->get_string("eokm_username"); $password = $config->get_string("eokm_password"); if ($username && $password) { - $ch = false_throws(curl_init("https://api.eokmhashdb.nl/v1/check/md5")); + $ch = curl_init("https://api.eokmhashdb.nl/v1/check/md5"); // curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/xml', $additionalHeaders)); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password); @@ -41,7 +41,7 @@ class Eokm extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("EOKM Filter"); diff --git a/ext/eokm/test.php b/ext/eokm/test.php index dd44fba8..b71e45e1 100644 --- a/ext/eokm/test.php +++ b/ext/eokm/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class EokmTest extends ShimmiePHPUnitTestCase { - public function testPass(): void + public function testPass() { // no EOKM login details set, so be a no-op $this->log_in_as_user(); @@ -15,4 +15,17 @@ class EokmTest extends ShimmiePHPUnitTestCase $this->assert_no_text("Image too small"); $this->assert_no_text("ratio"); } + + /* + public function testFail() + { + $this->log_in_as_user(); + try { + $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->assertTrue(false, "Invalid-size image was allowed"); + } catch (UploadException $e) { + $this->assertEquals("Image too small", $e->getMessage()); + } + } + */ } diff --git a/ext/et/main.php b/ext/et/main.php index d56bfbb6..0d919f92 100644 --- a/ext/et/main.php +++ b/ext/et/main.php @@ -4,16 +4,12 @@ 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; - class ET extends Extension { /** @var ETTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $user; if ($event->page_matches("system_info")) { @@ -23,17 +19,17 @@ class ET 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::VIEW_SYSINTO)) { $event->add_nav_link("system_info", new Link('system_info'), "System Info", null, 10); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::VIEW_SYSINTO)) { @@ -41,20 +37,19 @@ class ET extends Extension } } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('info') - ->setDescription('List a bunch of info') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - print($this->to_yaml($this->get_info())); - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + print "\tinfo\n"; + print "\t\tList a bunch of info\n\n"; + } + if ($event->cmd == "info") { + print($this->to_yaml($this->get_info())); + } } /** * Collect the information and return it in a keyed array. - * - * @return array */ private function get_info(): array { @@ -68,17 +63,6 @@ class ET extends Extension } } - $ver = VERSION; - if(defined("BUILD_TIME")) { - $ver .= "-" . substr(str_replace("-", "", constant("BUILD_TIME")), 0, 8); - } - if(defined("BUILD_HASH")) { - $ver .= "-" . substr(constant("BUILD_HASH"), 0, 7); - } - if(file_exists(".git")) { - $ver .= "+"; - } - $info = [ "about" => [ 'title' => $config->get_string(SetupConfig::TITLE), @@ -86,7 +70,7 @@ class ET extends Extension 'url' => make_http(make_link("/")), ], "versions" => [ - 'shimmie' => $ver, + 'shimmie' => VERSION, 'schema' => $config->get_int("db_version"), 'php' => phpversion(), 'db' => $database->get_driver_id()->value . " " . $database->get_version(), @@ -120,10 +104,10 @@ class ET extends Extension if (file_exists(".git")) { try { - $commitHash = trim(exec_ex('git log --pretty="%h" -n1 HEAD')); - $commitBranch = trim(exec_ex('git rev-parse --abbrev-ref HEAD')); - $commitOrigin = trim(exec_ex('git config --get remote.origin.url')); - $commitOrigin = preg_replace("#//.*@#", "//xxx@", $commitOrigin); + $commitHash = trim(exec('git log --pretty="%h" -n1 HEAD')); + $commitBranch= trim(exec('git rev-parse --abbrev-ref HEAD')); + $commitOrigin= trim(exec('git config --get remote.origin.url')); + $commitOrigin= preg_replace("#//.*@#", "//xxx@", $commitOrigin); $info['git'] = [ 'commit' => $commitHash, 'branch' => $commitBranch, @@ -137,16 +121,13 @@ class ET extends Extension return $info; } - /** - * @param array $info - */ private function to_yaml(array $info): string { $data = ""; foreach ($info as $title => $section) { $data .= "$title:\n"; foreach ($section as $k => $v) { - $data .= " $k: " . json_encode_ex($v, JSON_UNESCAPED_SLASHES) . "\n"; + $data .= " $k: " . json_encode($v, JSON_UNESCAPED_SLASHES) . "\n"; } $data .= "\n"; } diff --git a/ext/et/test.php b/ext/et/test.php index d5ce1a75..8e94f525 100644 --- a/ext/et/test.php +++ b/ext/et/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class ETTest extends ShimmiePHPUnitTestCase { - public function testET(): void + public function testET() { $this->log_in_as_admin(); $this->get_page("system_info"); diff --git a/ext/et/theme.php b/ext/et/theme.php index f877ecb5..089175b4 100644 --- a/ext/et/theme.php +++ b/ext/et/theme.php @@ -13,8 +13,10 @@ class ETTheme extends Themelet { /* * Create a page showing info + * + * $info = an array of ($name => $value) */ - public function display_info_page(string $yaml): void + public function display_info_page($yaml) { global $page; @@ -24,21 +26,21 @@ class ETTheme extends Themelet $page->add_block(new Block("Information:", $this->build_data_form($yaml))); } - protected function build_data_form(string $yaml): \MicroHTML\HTMLElement + protected function build_data_form($yaml): \MicroHTML\HTMLElement { return FORM( - ["action" => "https://shimmie.shishnet.org/register.php", "method" => "POST"], - INPUT(["type" => "hidden", "name" => "registration_api", "value" => "2"]), + ["action"=>"https://shimmie.shishnet.org/register.php", "method"=>"POST"], + INPUT(["type"=>"hidden", "name"=>"registration_api", "value"=>"2"]), P( "Your stats are useful so that I know which combinations of ". "web servers / databases / etc I need to support :)" ), P(TEXTAREA( - ["name" => 'data', "style" => "width: 100%; height: 20em;"], + ["name"=>'data', "style"=>"width: 100%; height: 20em;"], $yaml )), P(INPUT( - ["type" => 'submit', "value" => 'Click to send to Shish', "style" => "width: 100%; padding: 1em;"] + ["type"=>'submit', "value"=>'Click to send to Shish', "style"=>"width: 100%; padding: 1em;"] )), ); } diff --git a/ext/et_server/main.php b/ext/et_server/main.php index c78c30b2..2c3f2762 100644 --- a/ext/et_server/main.php +++ b/ext/et_server/main.php @@ -8,14 +8,15 @@ use function MicroHTML\{PRE}; class ETServer extends Extension { - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $database, $page, $user; if ($event->page_matches("register.php")) { + error_log("register.php"); if (isset($_POST["data"])) { $database->execute( "INSERT INTO registration(data) VALUES(:data)", - ["data" => $_POST["data"]] + ["data"=>$_POST["data"]] ); $page->set_title("Thanks!"); $page->set_heading("Thanks!"); @@ -27,7 +28,7 @@ class ETServer extends Extension foreach ($database->get_all("SELECT responded, data FROM registration ORDER BY responded DESC") as $row) { $page->add_block(new Block( $row["responded"], - PRE(["style" => "text-align: left; overflow: scroll;"], $row["data"]), + PRE(["style"=>"text-align: left; overflow: scroll;"], $row["data"]), "main", $n++ )); @@ -36,7 +37,7 @@ class ETServer extends Extension } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; diff --git a/ext/et_server/test.php b/ext/et_server/test.php deleted file mode 100644 index 6d643d56..00000000 --- a/ext/et_server/test.php +++ /dev/null @@ -1,21 +0,0 @@ -post_page("register.php", ["data" => "test entry"]); - - $this->log_in_as_user(); - $this->get_page("register.php"); - $this->assert_no_text("test entry"); - - $this->log_in_as_admin(); - $this->get_page("register.php"); - $this->assert_text("test entry"); - } -} diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php index 55f21024..0eeee676 100644 --- a/ext/ext_manager/main.php +++ b/ext/ext_manager/main.php @@ -4,9 +4,23 @@ 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; +function __extman_extcmp(ExtensionInfo $a, ExtensionInfo $b): int +{ + if ($a->beta===true&&$b->beta===false) { + return 1; + } + if ($a->beta===false&&$b->beta===true) { + return -1; + } + + return strcmp($a->name, $b->name); +} + +function __extman_extactive(ExtensionInfo $a): bool +{ + return Extension::is_enabled($a->key); +} + class ExtensionAuthor { @@ -25,7 +39,7 @@ class ExtManager extends Extension /** @var ExtManagerTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("ext_manager")) { @@ -64,20 +78,21 @@ class ExtManager extends Extension } } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('disable-all-ext') - ->setDescription('Disable all extensions') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $this->write_config([]); - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + print "\tdisable-all-ext\n"; + print "\t\tdisable all extensions\n\n"; + } + if ($event->cmd == "disable-all-ext") { + $this->write_config([]); + } } - 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_EXTENSION_LIST)) { $event->add_nav_link("ext_manager", new Link('ext_manager'), "Extension Manager"); } else { @@ -86,7 +101,7 @@ class ExtManager extends Extension } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::MANAGE_EXTENSION_LIST)) { @@ -95,40 +110,25 @@ class ExtManager extends Extension } /** - * @return ExtensionInfo[] + * #return ExtensionInfo[] */ private function get_extensions(bool $all): array { $extensions = ExtensionInfo::get_all(); if (!$all) { - $extensions = array_filter($extensions, fn ($x) => Extension::is_enabled($x->key)); + $extensions = array_filter($extensions, "Shimmie2\__extman_extactive"); } - usort($extensions, function ($a, $b) { - if ($a->beta === true && $b->beta === false) { - return 1; - } - if ($a->beta === false && $b->beta === true) { - return -1; - } - - return strcmp($a->name, $b->name); - }); + usort($extensions, "Shimmie2\__extman_extcmp"); return $extensions; } - /** - * @param array $settings - */ - private function set_things(array $settings): void + private function set_things($settings) { $core = ExtensionInfo::get_core_extensions(); $extras = []; foreach (ExtensionInfo::get_all_keys() as $key) { - if (in_array($key, $core)) { - continue; // core extensions are always enabled - } - if (isset($settings["ext_$key"]) && $settings["ext_$key"] === "on") { + if (!in_array($key, $core) && isset($settings["ext_$key"])) { $extras[] = $key; } } @@ -137,14 +137,19 @@ class ExtManager extends Extension } /** - * @param string[] $extras + * #param string[] $extras */ - private function write_config(array $extras): void + private function write_config(array $extras) { file_put_contents( "data/config/extensions.conf.php", '<' . '?php' . "\n" . 'define("EXTRA_EXTS", "' . implode(",", $extras) . '");' . "\n" ); + + // when the list of active extensions changes, we can be + // pretty sure that the list of who reacts to what will + // change too + _clear_cached_event_listeners(); } } diff --git a/ext/ext_manager/test.php b/ext/ext_manager/test.php index e1da9dc5..acb56b77 100644 --- a/ext/ext_manager/test.php +++ b/ext/ext_manager/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class ExtManagerTest extends ShimmiePHPUnitTestCase { - public function testAuth(): void + public function testAuth() { $this->get_page('ext_manager'); $this->assert_title("Extensions"); diff --git a/ext/ext_manager/theme.php b/ext/ext_manager/theme.php index 8d368390..ddc01bc5 100644 --- a/ext/ext_manager/theme.php +++ b/ext/ext_manager/theme.php @@ -25,16 +25,16 @@ use function MicroHTML\rawHTML; class ExtManagerTheme extends Themelet { /** - * @param ExtensionInfo[] $extensions + * #param ExtensionInfo[] $extensions */ - public function display_table(Page $page, array $extensions, bool $editable): void + public function display_table(Page $page, array $extensions, bool $editable) { $tbody = TBODY(); $form = SHM_SIMPLE_FORM( "ext_manager/set", TABLE( - ["id" => 'extensions', "class" => 'zebra'], + ["id"=>'extensions', "class"=>'zebra'], THEAD(TR( $editable ? TH("Enabled") : null, TH("Name"), @@ -42,7 +42,7 @@ class ExtManagerTheme extends Themelet TH("Description") )), $tbody, - $editable ? TFOOT(TR(TD(["colspan" => '5'], INPUT(["type" => 'submit', "value" => 'Set Extensions'])))) : null + $editable ? TFOOT(TR(TD(["colspan"=>'5'], INPUT(["type"=>'submit', "value"=>'Set Extensions'])))) : null ) ); @@ -53,18 +53,18 @@ class ExtManagerTheme extends Themelet } $tbody->appendChild(TR( - ["data-ext" => $extension->name], + ["data-ext"=>$extension->name], $editable ? TD(INPUT([ - "type" => 'checkbox', - "name" => "ext_{$extension->key}", - "id" => "ext_{$extension->key}", - "checked" => ($extension->is_enabled() === true), - "disabled" => ($extension->is_supported() === false || $extension->core === true) + "type"=>'checkbox', + "name"=>"ext_{$extension->key}", + "id"=>"ext_{$extension->key}", + "checked"=>($extension->is_enabled() === true), + "disabled"=>($extension->is_supported()===false || $extension->core===true) ])) : null, TD(LABEL( - ["for" => "ext_{$extension->key}"], + ["for"=>"ext_{$extension->key}"], ( - ($extension->beta === true ? "[BETA] " : ""). + ($extension->beta===true ? "[BETA] " : ""). (empty($extension->name) ? $extension->key : $extension->name) ) )), @@ -72,47 +72,35 @@ class ExtManagerTheme extends Themelet // TODO: A proper "docs" symbol would be preferred here. $extension->documentation ? A( - ["href" => make_link("ext_doc/" . url_escape($extension->key))], - IMG(["src" => 'ext/ext_manager/baseline_open_in_new_black_18dp.png']) + ["href"=>make_link("ext_doc/" . url_escape($extension->key))], + IMG(["src"=>'ext/ext_manager/baseline_open_in_new_black_18dp.png']) ) : null ), TD( - ["style" => 'text-align: left;'], + ["style"=>'text-align: left;'], $extension->description, " ", - B(["style" => 'color:red'], $extension->get_support_info()) + B(["style"=>'color:red'], $extension->get_support_info()) ), )); } - if($editable) { - foreach ($extensions as $extension) { - if ($extension->visibility === ExtensionVisibility::HIDDEN && !$extension->core) { - $form->appendChild(INPUT([ - "type" => 'hidden', - "name" => "ext_{$extension->key}", - "value" => ($extension->is_enabled() === true) ? "on" : "off" - ])); - } - } - } - $page->set_title("Extensions"); $page->set_heading("Extensions"); $page->add_block(new NavBlock()); $page->add_block(new Block("Extension Manager", $form)); } - public function display_doc(Page $page, ExtensionInfo $info): void + public function display_doc(Page $page, ExtensionInfo $info) { $author = emptyHTML(); if (count($info->authors) > 0) { $author->appendChild(BR()); $author->appendChild(B(count($info->authors) > 1 ? "Authors: " : "Author: ")); - foreach ($info->authors as $auth => $email) { + foreach ($info->authors as $auth=>$email) { if (!empty($email)) { - $author->appendChild(A(["href" => "mailto:$email"], $auth)); + $author->appendChild(A(["href"=>"mailto:$email"], $auth)); } else { $author->appendChild($auth); } @@ -121,13 +109,13 @@ class ExtManagerTheme extends Themelet } $html = DIV( - ["style" => 'margin: auto; text-align: left; width: 512px;'], + ["style"=>'margin: auto; text-align: left; width: 512px;'], $author, ($info->version ? emptyHTML(BR(), B("Version: "), $info->version) : null), - ($info->link ? emptyHTML(BR(), B("Home Page"), A(["href" => $info->link], "Link")) : null), + ($info->link ? emptyHTML(BR(), B("Home Page"), A(["href"=>$info->link], "Link")) : null), P(rawHTML($info->documentation ?? "(This extension has no documentation)")), //
    , - P(A(["href" => make_link("ext_manager")], "Back to the list")) + P(A(["href"=>make_link("ext_manager")], "Back to the list")) ); $page->set_title("Documentation for " . html_escape($info->name)); diff --git a/ext/favorites/info.php b/ext/favorites/info.php index e13358df..d0db6ae5 100644 --- a/ext/favorites/info.php +++ b/ext/favorites/info.php @@ -10,7 +10,7 @@ class FavoritesInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Favorites"; - public array $authors = ["Daniel Marschall" => "info@daniel-marschall.de"]; + public array $authors = ["Daniel Marschall"=>"info@daniel-marschall.de"]; public string $license = self::LICENSE_GPLV2; public string $description = "Allow users to favorite images"; public ?string $documentation = diff --git a/ext/favorites/main.php b/ext/favorites/main.php index d5ad9306..eb94d62c 100644 --- a/ext/favorites/main.php +++ b/ext/favorites/main.php @@ -27,12 +27,7 @@ class Favorites extends Extension /** @var FavoritesTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void - { - Image::$prop_types["favorites"] = ImagePropType::INT; - } - - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $database, $user; if (!$user->is_anonymous()) { @@ -41,14 +36,14 @@ class Favorites extends Extension $is_favorited = $database->get_one( "SELECT COUNT(*) AS ct FROM user_favorites WHERE user_id = :user_id AND image_id = :image_id", - ["user_id" => $user_id, "image_id" => $image_id] + ["user_id"=>$user_id, "image_id"=>$image_id] ) > 0; - $event->add_part($this->theme->get_voter_html($event->image, $is_favorited)); + $event->add_part((string)$this->theme->get_voter_html($event->image, $is_favorited)); } } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { $people = $this->list_persons_who_have_favorited($event->image); if (count($people) > 0) { @@ -56,7 +51,7 @@ class Favorites extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("change_favorite") && !$user->is_anonymous() && $user->check_auth_token()) { @@ -75,16 +70,16 @@ class Favorites extends Extension } } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onUserPageBuilding(UserPageBuildingEvent $event) { - $i_favorites_count = Search::count_images(["favorited_by={$event->display_user->name}"]); - $i_days_old = ((time() - strtotime_ex($event->display_user->join_date)) / 86400) + 1; + $i_favorites_count = Image::count_images(["favorited_by={$event->display_user->name}"]); + $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; $h_favorites_rate = sprintf("%.1f", ($i_favorites_count / $i_days_old)); - $favorites_link = search_link(["favorited_by={$event->display_user->name}"]); + $favorites_link = make_link("post/list/favorited_by={$event->display_user->name}/1"); $event->add_stats("Posts favorited: $i_favorites_count, $h_favorites_rate per day"); } - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { global $user; if ( @@ -96,7 +91,7 @@ class Favorites extends Extension } } - public function onFavoriteSet(FavoriteSetEvent $event): void + public function onFavoriteSet(FavoriteSetEvent $event) { global $user; $this->add_vote($event->image_id, $user->id, $event->do_set); @@ -104,26 +99,26 @@ class Favorites extends Extension // FIXME: this should be handled by the foreign key. Check that it // is, and then remove this - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { global $database; - $database->execute("DELETE FROM user_favorites WHERE image_id=:image_id", ["image_id" => $event->image->id]); + $database->execute("DELETE FROM user_favorites WHERE image_id=:image_id", ["image_id"=>$event->image->id]); } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { - $event->replace('$favorites', (string)$event->image['favorites']); + $event->replace('$favorites', (string)$event->image->favorites); } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; $username = url_escape($user->name); - $event->add_link("My Favorites", search_link(["favorited_by=$username"]), 20); + $event->add_link("My Favorites", make_link("post/list/favorited_by=$username/1"), 20); } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -147,21 +142,21 @@ class Favorites extends Extension } } - 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("Favorites", $this->theme->get_help_html())); } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { global $user; - if ($event->parent == "posts") { + if ($event->parent=="posts") { $event->add_nav_link("posts_favorites", new Link("post/list/favorited_by={$user->name}/1"), "My Favorites"); } - if ($event->parent === "user") { + if ($event->parent==="user") { if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { $username = url_escape($user->name); $event->add_nav_link("favorites", new Link("post/list/favorited_by=$username/1"), "My Favorites"); @@ -169,7 +164,7 @@ class Favorites extends Extension } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -179,7 +174,7 @@ class Favorites extends Extension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $page, $user; @@ -207,7 +202,7 @@ class Favorites extends Extension } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -238,30 +233,30 @@ class Favorites extends Extension } } - private function add_vote(int $image_id, int $user_id, bool $do_set): void + private function add_vote(int $image_id, int $user_id, bool $do_set) { global $database; if ($do_set) { - if (!$database->get_row("select 1 from user_favorites where image_id=:image_id and user_id=:user_id", ["image_id" => $image_id, "user_id" => $user_id])) { + if (!$database->get_row("select 1 from user_favorites where image_id=:image_id and user_id=:user_id", ["image_id"=>$image_id, "user_id"=>$user_id])) { $database->execute( "INSERT INTO user_favorites(image_id, user_id, created_at) VALUES(:image_id, :user_id, NOW())", - ["image_id" => $image_id, "user_id" => $user_id] + ["image_id"=>$image_id, "user_id"=>$user_id] ); } } else { $database->execute( "DELETE FROM user_favorites WHERE image_id = :image_id AND user_id = :user_id", - ["image_id" => $image_id, "user_id" => $user_id] + ["image_id"=>$image_id, "user_id"=>$user_id] ); } $database->execute( "UPDATE images SET favorites=(SELECT COUNT(*) FROM user_favorites WHERE image_id=:image_id) WHERE id=:user_id", - ["image_id" => $image_id, "user_id" => $user_id] + ["image_id"=>$image_id, "user_id"=>$user_id] ); } /** - * @return string[] + * #return string[] */ private function list_persons_who_have_favorited(Image $image): array { @@ -269,7 +264,7 @@ class Favorites extends Extension return $database->get_col( "SELECT name FROM users WHERE id IN (SELECT user_id FROM user_favorites WHERE image_id = :image_id) ORDER BY name", - ["image_id" => $image->id] + ["image_id"=>$image->id] ); } } diff --git a/ext/favorites/test.php b/ext/favorites/test.php index aa24efcc..ed1247bc 100644 --- a/ext/favorites/test.php +++ b/ext/favorites/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class FavoritesTest extends ShimmiePHPUnitTestCase { - public function testFavorites(): void + public function testFavorites() { global $user; $this->log_in_as_user(); diff --git a/ext/favorites/theme.php b/ext/favorites/theme.php index e9fff045..c8fb5216 100644 --- a/ext/favorites/theme.php +++ b/ext/favorites/theme.php @@ -16,15 +16,12 @@ class FavoritesTheme extends Themelet $label = $is_favorited ? "Un-Favorite" : "Favorite"; return SHM_SIMPLE_FORM( "change_favorite", - INPUT(["type" => "hidden", "name" => "image_id", "value" => $image->id]), - INPUT(["type" => "hidden", "name" => "favorite_action", "value" => $name]), - INPUT(["type" => "submit", "value" => $label]), + INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image->id]), + INPUT(["type"=>"hidden", "name"=>"favorite_action", "value"=>$name]), + INPUT(["type"=>"submit", "value"=>$label]), ); } - /** - * @param string[] $username_array - */ public function display_people(array $username_array): void { global $page; diff --git a/ext/featured/main.php b/ext/featured/main.php index e0d05f0b..11cba6ec 100644 --- a/ext/featured/main.php +++ b/ext/featured/main.php @@ -9,13 +9,13 @@ class Featured extends Extension /** @var FeaturedTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int('featured_id', 0); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page, $user; if ($event->page_matches("featured_image")) { @@ -35,7 +35,7 @@ class Featured extends Extension if (!is_null($image)) { $page->set_mode(PageMode::DATA); $page->set_mime($image->get_mime()); - $page->set_data(file_get_contents_ex($image->get_image_filename())); + $page->set_data(file_get_contents($image->get_image_filename())); } } if ($event->get_arg(0) == "view") { @@ -47,25 +47,22 @@ class Featured extends Extension } } - public function onPostListBuilding(PostListBuildingEvent $event): void + public function onPostListBuilding(PostListBuildingEvent $event) { global $cache, $config, $page, $user; $fid = $config->get_int("featured_id"); if ($fid > 0) { - $image = cache_get_or_set( - "featured_image_object:$fid", - function () use ($fid) { - $image = Image::by_id($fid); - if ($image) { // make sure the object is fully populated before saving - $image->get_tag_array(); - } - return $image; - }, - 600 - ); + $image = $cache->get("featured_image_object:$fid"); + if (is_null($image)) { + $image = Image::by_id($fid); + if ($image) { // make sure the object is fully populated before saving + $image->get_tag_array(); + } + $cache->set("featured_image_object:$fid", $image, 600); + } if (!is_null($image)) { if (Extension::is_enabled(RatingsInfo::KEY)) { - if (!in_array($image['rating'], Ratings::get_user_class_privs($user))) { + if (!in_array($image->rating, Ratings::get_user_class_privs($user))) { return; } } @@ -74,7 +71,7 @@ class Featured extends Extension } } - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { global $config; if ($event->image->id == $config->get_int("featured_id")) { @@ -83,7 +80,7 @@ class Featured extends Extension } } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::EDIT_FEATURE) && $event->context == "view") { diff --git a/ext/featured/test.php b/ext/featured/test.php index c0c82eb3..ef0c4119 100644 --- a/ext/featured/test.php +++ b/ext/featured/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class FeaturedTest extends ShimmiePHPUnitTestCase { - public function testFeatured(): void + public function testFeatured() { global $config; diff --git a/ext/featured/theme.php b/ext/featured/theme.php index d3c5f58d..dcca6283 100644 --- a/ext/featured/theme.php +++ b/ext/featured/theme.php @@ -16,30 +16,30 @@ class FeaturedTheme extends Themelet $page->add_block(new Block("Featured Post", $this->build_featured_html($image), "left", 3)); } - public function get_buttons_html(int $image_id): \MicroHTML\HTMLElement + public function get_buttons_html(int $image_id): string { - return SHM_SIMPLE_FORM( + return (string)SHM_SIMPLE_FORM( "featured_image/set", - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image_id]), - INPUT(["type" => 'submit', "value" => 'Feature This']), + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image_id]), + INPUT(["type"=>'submit', "value"=>'Feature This']), ); } - public function build_featured_html(Image $image, ?string $query = null): \MicroHTML\HTMLElement + public function build_featured_html(Image $image, ?string $query=null): \MicroHTML\HTMLElement { $tsize = get_thumbnail_size($image->width, $image->height); return DIV( - ["style" => "text-align: center;"], + ["style"=>"text-align: center;"], A( - ["href" => make_link("post/view/{$image->id}", $query)], + ["href"=>make_link("post/view/{$image->id}", $query)], IMG([ - "id" => "thumb_rand_{$image->id}", - "title" => $image->get_tooltip(), - "alt" => $image->get_tooltip(), - "class" => 'highlighted', - "style" => "max-height: {$tsize[1]}px; max-width: 100%;", - "src" => $image->get_thumb_link() + "id"=>"thumb_rand_{$image->id}", + "title"=>$image->get_tooltip(), + "alt"=>$image->get_tooltip(), + "class"=>'highlighted', + "style"=>"max-height: {$tsize[1]}px; max-width: 100%;", + "src"=>$image->get_thumb_link() ]) ) ); diff --git a/ext/forum/info.php b/ext/forum/info.php index b4e5f97c..09021550 100644 --- a/ext/forum/info.php +++ b/ext/forum/info.php @@ -10,7 +10,7 @@ class ForumInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Forum"; - 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 = "Rough forum extension"; } diff --git a/ext/forum/main.php b/ext/forum/main.php index 176f1595..f7b70b5a 100644 --- a/ext/forum/main.php +++ b/ext/forum/main.php @@ -17,7 +17,7 @@ class Forum extends Extension /** @var ForumTheme */ protected Themelet $theme; - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $config, $database; @@ -65,7 +65,7 @@ class Forum extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Forum"); $sb->add_int_option("forumTitleSubString", "Title max long: "); @@ -75,14 +75,14 @@ class Forum extends Extension $sb->add_int_option("forumMaxCharsPerPost", "
    Max chars per post: "); } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onUserPageBuilding(UserPageBuildingEvent $event) { global $database; - $threads_count = $database->get_one("SELECT COUNT(*) FROM forum_threads WHERE user_id=:user_id", ['user_id' => $event->display_user->id]); - $posts_count = $database->get_one("SELECT COUNT(*) FROM forum_posts WHERE user_id=:user_id", ['user_id' => $event->display_user->id]); + $threads_count = $database->get_one("SELECT COUNT(*) FROM forum_threads WHERE user_id=:user_id", ['user_id'=>$event->display_user->id]); + $posts_count = $database->get_one("SELECT COUNT(*) FROM forum_posts WHERE user_id=:user_id", ['user_id'=>$event->display_user->id]); - $days_old = ((time() - strtotime_ex($event->display_user->join_date)) / 86400) + 1; + $days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; $threads_rate = sprintf("%.1f", ($threads_count / $days_old)); $posts_rate = sprintf("%.1f", ($posts_count / $days_old)); @@ -91,12 +91,12 @@ class Forum extends Extension $event->add_stats("Forum posts: $posts_count, $posts_rate per day"); } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("forum", new Link('forum/index'), "Forum"); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -111,10 +111,10 @@ class Forum extends Extension case "view": $threadID = int_escape($event->get_arg(1)); // $pageNumber = int_escape($event->get_arg(2)); - $errors = $this->sanity_check_viewed_thread($threadID); + list($errors) = $this->sanity_check_viewed_thread($threadID); - if (count($errors) > 0) { - $this->theme->display_error(500, "Error", implode("
    ", $errors)); + if ($errors!=null) { + $this->theme->display_error(500, "Error", $errors); break; } @@ -133,10 +133,10 @@ class Forum extends Extension case "create": $redirectTo = "forum/index"; if (!$user->is_anonymous()) { - $errors = $this->sanity_check_new_thread(); + list($errors) = $this->sanity_check_new_thread(); - if (count($errors) > 0) { - $this->theme->display_error(500, "Error", implode("
    ", $errors)); + if ($errors!=null) { + $this->theme->display_error(500, "Error", $errors); break; } @@ -174,10 +174,10 @@ class Forum extends Extension $threadID = int_escape($_POST["threadID"]); $total_pages = $this->get_total_pages_for_thread($threadID); if (!$user->is_anonymous()) { - $errors = $this->sanity_check_new_post(); + list($errors) = $this->sanity_check_new_post(); - if (count($errors) > 0) { - $this->theme->display_error(500, "Error", implode("
    ", $errors)); + if ($errors!=null) { + $this->theme->display_error(500, "Error", $errors); break; } $this->save_new_post($threadID, $user); @@ -197,75 +197,63 @@ class Forum extends Extension private function get_total_pages_for_thread(int $threadID): int { global $database, $config; - $result = $database->get_row(" - SELECT COUNT(1) AS count - FROM forum_posts - WHERE thread_id = :thread_id - ", ['thread_id' => $threadID]); + $result = $database->get_row("SELECT COUNT(1) AS count FROM forum_posts WHERE thread_id = :thread_id", ['thread_id'=>$threadID]); return (int)ceil($result["count"] / $config->get_int("forumPostsPerPage")); } - /** - * @return string[] - */ private function sanity_check_new_thread(): array { - $errors = []; + $errors = null; if (!array_key_exists("title", $_POST)) { - $errors[] = "No title supplied."; + $errors .= "
    No title supplied.
    "; } elseif (strlen($_POST["title"]) == 0) { - $errors[] = "You cannot have an empty title."; - } elseif (strlen($_POST["title"]) > 255) { - $errors[] = "Your title is too long."; + $errors .= "
    You cannot have an empty title.
    "; + } elseif (strlen(html_escape($_POST["title"])) > 255) { + $errors .= "
    Your title is too long.
    "; } if (!array_key_exists("message", $_POST)) { - $errors[] = "No message supplied."; + $errors .= "
    No message supplied.
    "; } elseif (strlen($_POST["message"]) == 0) { - $errors[] = "You cannot have an empty message."; + $errors .= "
    You cannot have an empty message.
    "; } - return $errors; + return [$errors]; } - /** - * @return string[] - */ private function sanity_check_new_post(): array { - $errors = []; + $errors = null; if (!array_key_exists("threadID", $_POST)) { - $errors[] = "No thread ID supplied."; + $errors = "
    No thread ID supplied.
    "; } elseif (strlen($_POST["threadID"]) == 0) { - $errors[] = "No thread ID supplied."; + $errors = "
    No thread ID supplied.
    "; } elseif (is_numeric($_POST["threadID"])) { if (!array_key_exists("message", $_POST)) { - $errors[] = "No message supplied."; + $errors .= "
    No message supplied.
    "; } elseif (strlen($_POST["message"]) == 0) { - $errors[] = "You cannot have an empty message."; + $errors .= "
    You cannot have an empty message.
    "; } } - return $errors; + return [$errors]; } - /** - * @return string[] - */ private function sanity_check_viewed_thread(int $threadID): array { - $errors = []; + $errors = null; if (!$this->threadExists($threadID)) { - $errors[] = "Inexistent thread."; + $errors = "
    Inexistent thread.
    "; } - return $errors; + return [$errors]; } private function get_thread_title(int $threadID): string { global $database; - return $database->get_one("SELECT t.title FROM forum_threads AS t WHERE t.id = :id ", ['id' => $threadID]); + $result = $database->get_row("SELECT t.title FROM forum_threads AS t WHERE t.id = :id ", ['id'=>$threadID]); + return $result["title"]; } private function show_last_threads(Page $page, PageRequestEvent $event, bool $showAdminOptions = false): void @@ -289,7 +277,7 @@ class Forum extends Extension "ON p.thread_id = f.id ". "GROUP BY f.id, f.sticky, f.title, f.date, u.name, u.email, u.class ". "ORDER BY f.sticky ASC, f.uptodate DESC LIMIT :limit OFFSET :offset", - ["limit" => $threadsPerPage, "offset" => $pageNumber * $threadsPerPage] + ["limit"=>$threadsPerPage, "offset"=>$pageNumber * $threadsPerPage] ); $this->theme->display_thread_list($page, $threads, $showAdminOptions, $pageNumber + 1, $totalPages); @@ -300,7 +288,7 @@ class Forum extends Extension global $config, $database; $threadID = int_escape($event->get_arg(1)); $postsPerPage = $config->get_int('forumPostsPerPage', 15); - $totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM forum_posts WHERE thread_id = :id", ['id' => $threadID]) / $postsPerPage); + $totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM forum_posts WHERE thread_id = :id", ['id'=>$threadID]) / $postsPerPage); $threadTitle = $this->get_thread_title($threadID); if ($event->count_args() >= 3) { @@ -317,14 +305,14 @@ class Forum extends Extension "WHERE thread_id = :thread_id ". "ORDER BY p.date ASC ". "LIMIT :limit OFFSET :offset", - ["thread_id" => $threadID, "offset" => $pageNumber * $postsPerPage, "limit" => $postsPerPage] + ["thread_id"=>$threadID, "offset"=>$pageNumber * $postsPerPage, "limit"=>$postsPerPage] ); $this->theme->display_thread($posts, $showAdminOptions, $threadTitle, $threadID, $pageNumber + 1, $totalPages); } private function save_new_thread(User $user): int { - $title = $_POST["title"]; + $title = html_escape($_POST["title"]); $sticky = !empty($_POST["sticky"]); global $database; @@ -334,7 +322,7 @@ class Forum extends Extension (title, sticky, user_id, date, uptodate) VALUES (:title, :sticky, :user_id, now(), now())", - ['title' => $title, 'sticky' => $sticky, 'user_id' => $user->id] + ['title'=>$title, 'sticky'=>$sticky, 'user_id'=>$user->id] ); $threadID = $database->get_last_insert_id("forum_threads_id_seq"); @@ -348,7 +336,7 @@ class Forum extends Extension { global $config; $userID = $user->id; - $message = $_POST["message"]; + $message = html_escape($_POST["message"]); $max_characters = $config->get_int('forumMaxCharsPerPost'); $message = substr($message, 0, $max_characters); @@ -357,32 +345,32 @@ class Forum extends Extension $database->execute(" INSERT INTO forum_posts (thread_id, user_id, date, message) VALUES (:thread_id, :user_id, now(), :message) - ", ['thread_id' => $threadID, 'user_id' => $userID, 'message' => $message]); + ", ['thread_id'=>$threadID, 'user_id'=>$userID, 'message'=>$message]); $postID = $database->get_last_insert_id("forum_posts_id_seq"); log_info("forum", "Post {$postID} created by {$user->name}"); - $database->execute("UPDATE forum_threads SET uptodate=now() WHERE id=:id", ['id' => $threadID]); + $database->execute("UPDATE forum_threads SET uptodate=now() WHERE id=:id", ['id'=>$threadID]); } private function delete_thread(int $threadID): void { global $database; - $database->execute("DELETE FROM forum_threads WHERE id = :id", ['id' => $threadID]); - $database->execute("DELETE FROM forum_posts WHERE thread_id = :thread_id", ['thread_id' => $threadID]); + $database->execute("DELETE FROM forum_threads WHERE id = :id", ['id'=>$threadID]); + $database->execute("DELETE FROM forum_posts WHERE thread_id = :thread_id", ['thread_id'=>$threadID]); } private function delete_post(int $postID): void { global $database; - $database->execute("DELETE FROM forum_posts WHERE id = :id", ['id' => $postID]); + $database->execute("DELETE FROM forum_posts WHERE id = :id", ['id'=>$postID]); } private function threadExists(int $threadID): bool { global $database; - $result = $database->get_one("SELECT EXISTS (SELECT * FROM forum_threads WHERE id=:id)", ['id' => $threadID]); + $result=$database->get_one("SELECT EXISTS (SELECT * FROM forum_threads WHERE id=:id)", ['id'=>$threadID]); return $result == 1; } } diff --git a/ext/forum/theme.php b/ext/forum/theme.php index ea7400f8..9e29ab59 100644 --- a/ext/forum/theme.php +++ b/ext/forum/theme.php @@ -4,20 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\{INPUT, LABEL, SMALL, TEXTAREA, TR, TD, TABLE, TH, TBODY, THEAD, DIV, A, BR, emptyHTML, SUP, rawHTML}; - -/** - * @phpstan-type Thread array{id:int,title:string,sticky:bool,user_name:string,uptodate:string,response_count:int} - * @phpstan-type Post array{id:int,user_name:string,user_class:string,date:string,message:string} - */ class ForumTheme extends Themelet { - /** - * @param Thread[] $threads - */ - public function display_thread_list(Page $page, array $threads, bool $showAdminOptions, int $pageNumber, int $totalPages): void + public function display_thread_list(Page $page, $threads, $showAdminOptions, $pageNumber, $totalPages) { if (count($threads) == 0) { $html = "There are no threads to show."; @@ -34,45 +23,33 @@ class ForumTheme extends Themelet - public function display_new_thread_composer(Page $page, string $threadText = null, string $threadTitle = null): void + public function display_new_thread_composer(Page $page, $threadText = null, $threadTitle = null) { global $config, $user; $max_characters = $config->get_int('forumMaxCharsPerPost'); + $html = make_form(make_link("forum/create")); - $html = SHM_SIMPLE_FORM( - "forum/create", - TABLE( - ["style" => "width: 500px;"], - TR( - TD("Title:"), - TD(INPUT(["type" => "text", "name" => "title", "value" => $threadTitle])) - ), - TR( - TD("Message:"), - TD(TEXTAREA( - ["id" => "message", "name" => "message"], - $threadText - )) - ), - TR( - TD(), - TD(SMALL("Max characters allowed: $max_characters.")) - ), - $user->can(Permissions::FORUM_ADMIN) ? TR( - TD(), - TD( - LABEL(["for" => "sticky"], "Sticky:"), - INPUT(["name" => "sticky", "id" => "sticky", "type" => "checkbox", "value" => "Y"]) - ) - ) : null, - TR( - TD( - ["colspan" => 2], - INPUT(["type" => "submit", "value" => "Submit"]) - ) - ) - ) - ); + + if (!is_null($threadTitle)) { + $threadTitle = html_escape($threadTitle); + } + + if (!is_null($threadText)) { + $threadText = html_escape($threadText); + } + + $html .= " +
    {$entry["date_sent"]}{$entry["message"]}
    + + + "; + if ($user->can(Permissions::FORUM_ADMIN)) { + $html .= ""; + } + $html .= " +
    Title:
    Message:
    Max characters alowed: $max_characters.
    + + "; $blockTitle = "Write a new thread"; $page->set_title(html_escape($blockTitle)); @@ -80,43 +57,36 @@ class ForumTheme extends Themelet $page->add_block(new Block($blockTitle, $html, "main", 120)); } - public function display_new_post_composer(Page $page, int $threadID): void + + + public function display_new_post_composer(Page $page, $threadID) { global $config; $max_characters = $config->get_int('forumMaxCharsPerPost'); - $html = SHM_SIMPLE_FORM( - "forum/answer", - INPUT(["type" => "hidden", "name" => "threadID", "value" => $threadID]), - TABLE( - ["style" => "width: 500px;"], - TR( - TD("Message:"), - TD(TEXTAREA(["id" => "message", "name" => "message"])) - ), - TR( - TD(), - TD(SMALL("Max characters allowed: $max_characters.")) - ), - TR( - TD( - ["colspan" => 2], - INPUT(["type" => "submit", "value" => "Submit"]) - ) - ) - ) - ); + $html = make_form(make_link("forum/answer")); + + $html .= ''; + + $html .= " + + + "; + + $html .= " +
    Message: +
    Max characters alowed: $max_characters.
    + + "; $blockTitle = "Answer to this thread"; $page->add_block(new Block($blockTitle, $html, "main", 130)); } - /** - * @param array $posts - */ - public function display_thread(array $posts, bool $showAdminOptions, string $threadTitle, int $threadID, int $pageNumber, int $totalPages): void + + public function display_thread($posts, $showAdminOptions, $threadTitle, $threadID, $pageNumber, $totalPages) { global $config, $page/*, $user*/; @@ -124,70 +94,68 @@ class ForumTheme extends Themelet $current_post = 0; - $tbody = TBODY(); + $html = + "

    ". + "". + "". + "". + "". + ""; + foreach ($posts as $post) { $current_post++; + $message = mb_convert_encoding($post["message"], "UTF-8", "HTML-ENTITIES"); - $post_number = (($pageNumber - 1) * $posts_per_page) + $current_post; - $tbody->appendChild( - emptyHTML( - TR( - ["class" => "postHead"], - TD(["class" => "forumSupuser"]), - TD( - ["class" => "forumSupmessage"], - DIV( - ["class" => "deleteLink"], - $showAdminOptions ? A(["href" => make_link("forum/delete/".$threadID."/".$post['id'])], "Delete") : null - ) - ) - ), - TR( - ["class" => "posBody"], - TD( - ["class" => "forumUser"], - A(["href" => make_link("user/".$post["user_name"])], $post["user_name"]), - BR(), - SUP(["class" => "user_rank"], $post["user_class"]), - BR(), - rawHTML(User::by_name($post["user_name"])->get_avatar_html()), - BR() - ), - TD( - ["class" => "forumMessage"], - DIV(["class" => "postDate"], SMALL(rawHTML(autodate($post['date'])))), - DIV(["class" => "postNumber"], " #".$post_number), - BR(), - DIV(["class" => "postMessage"], rawHTML(send_event(new TextFormattingEvent($post["message"]))->formatted)) - ) - ), - TR( - ["class" => "postFoot"], - TD(["class" => "forumSubuser"]), - TD(["class" => "forumSubmessage"]) - ) - ) - ); + $message = send_event(new TextFormattingEvent($message))->formatted; + $message = str_replace('\n\r', '
    ', $message); + $message = str_replace('\r\n', '
    ', $message); + $message = str_replace('\n', '
    ', $message); + $message = str_replace('\r', '
    ', $message); + + $message = stripslashes($message); + + $userLink = "".$post["user_name"].""; + + $poster = User::by_name($post["user_name"]); + $gravatar = $poster->get_avatar_html(); + + $rank = "{$post["user_class"]}"; + + $postID = $post['id']; + + //if($user->can(Permissions::FORUM_ADMIN)){ + //$delete_link = "Delete"; + //} else { + //$delete_link = ""; + //} + + if ($showAdminOptions) { + $delete_link = "Delete"; + } else { + $delete_link = ""; + } + + $post_number = (($pageNumber-1)*$posts_per_page)+$current_post; + $html .= " + + + + + + + + + + + + "; } - $html = emptyHTML( - DIV( - ["id" => "returnLink"], - A(["href" => make_link("forum/index/")], "Return") - ), - BR(), - BR(), - TABLE( - ["id" => "threadPosts", "class" => "zebra"], - THEAD( - TR( - TH(["id" => "threadHeadUser"], "User"), - TH("Message") - ) - ), - $tbody - ) - ); + $html .= "
    UserMessage
    ".$userLink."
    ".$rank."
    ".$gravatar."
    + +
    #".$post_number."
    +
    +
    ".$message."
    "; $this->display_paginator($page, "forum/view/".$threadID, null, $pageNumber, $totalPages); @@ -196,35 +164,37 @@ class ForumTheme extends Themelet $page->add_block(new Block($threadTitle, $html, "main", 20)); } - public function add_actions_block(Page $page, int $threadID): void + + + public function add_actions_block(Page $page, $threadID) { - $html = A(["href" => make_link("forum/nuke/".$threadID)], "Delete this thread and its posts."); + $html = 'Delete this thread and its posts.'; + $page->add_block(new Block("Admin Actions", $html, "main", 140)); } - /** - * @param Thread[] $threads - */ - private function make_thread_list(array $threads, bool $showAdminOptions): HTMLElement + + + private function make_thread_list($threads, $showAdminOptions): string { - global $config; + $html = "". + "". + "". + "". + "". + ""; - $tbody = TBODY(); - $html = TABLE( - ["id" => "threadList", "class" => "zebra"], - THEAD( - TR( - TH("Title"), - TH("Author"), - TH("Updated"), - TH("Responses"), - $showAdminOptions ? TH("Actions") : null - ) - ), - $tbody - ); + if ($showAdminOptions) { + $html .= ""; + } + $html .= ""; + + $current_post = 0; foreach ($threads as $thread) { + $oe = ($current_post++ % 2 == 0) ? "even" : "odd"; + + global $config; $titleSubString = $config->get_int('forumTitleSubString'); if ($titleSubString < strlen($thread["title"])) { @@ -234,23 +204,27 @@ class ForumTheme extends Themelet $title = $thread["title"]; } - $tbody->appendChild( - TR( - TD( - ["class" => "left"], - bool_escape($thread["sticky"]) ? "Sticky: " : "", - A(["href" => make_link("forum/view/".$thread["id"])], $title) - ), - TD( - A(["href" => make_link("user/".$thread["user_name"])], $thread["user_name"]) - ), - TD(rawHTML(autodate($thread["uptodate"]))), - TD($thread["response_count"]), - $showAdminOptions ? TD(A(["href" => make_link("forum/nuke/".$thread["id"])], "Delete")) : null - ) - ); + if (bool_escape($thread["sticky"])) { + $sticky = "Sticky: "; + } else { + $sticky = ""; + } + + $html .= "". + '". + '". + "". + ""; + + if ($showAdminOptions) { + $html .= ''; + } + + $html .= ""; } + $html .= "
    TitleAuthorUpdatedResponsesActions
    '.$sticky.''.$title."'.$thread["user_name"]."".autodate($thread["uptodate"])."".$thread["response_count"]."Delete
    "; + return $html; } } diff --git a/ext/four_oh_four/main.php b/ext/four_oh_four/main.php index 627e18d3..836c8f24 100644 --- a/ext/four_oh_four/main.php +++ b/ext/four_oh_four/main.php @@ -6,7 +6,7 @@ namespace Shimmie2; class FourOhFour extends Extension { - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page; // hax. @@ -21,10 +21,7 @@ class FourOhFour extends Extension } } - /** - * @param Block[] $blocks - */ - private function count_main(array $blocks): int + private function count_main($blocks): int { $n = 0; foreach ($blocks as $block) { diff --git a/ext/four_oh_four/test.php b/ext/four_oh_four/test.php index 2e0c0efe..5c71a839 100644 --- a/ext/four_oh_four/test.php +++ b/ext/four_oh_four/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class FourOhFourTest extends ShimmiePHPUnitTestCase { - public function test404Handler(): void + public function test404Handler() { $this->get_page('not/a/page'); // most descriptive error first diff --git a/ext/google_analytics/info.php b/ext/google_analytics/info.php index 5f9b6fdf..af4f5c9d 100644 --- a/ext/google_analytics/info.php +++ b/ext/google_analytics/info.php @@ -11,7 +11,7 @@ class GoogleAnalyticsInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Google Analytics"; public string $url = "http://drudexsoftware.com"; - public array $authors = ["Drudex Software" => "support@drudexsoftware.com"]; + public array $authors = ["Drudex Software"=>"support@drudexsoftware.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Integrates Google Analytics tracking"; public ?string $documentation = diff --git a/ext/google_analytics/main.php b/ext/google_analytics/main.php index ee7cd278..a9759374 100644 --- a/ext/google_analytics/main.php +++ b/ext/google_analytics/main.php @@ -7,7 +7,7 @@ namespace Shimmie2; class GoogleAnalytics extends Extension { # Add analytics to config - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Google Analytics"); $sb->add_text_option("google_analytics_id", "Analytics ID: "); @@ -15,7 +15,7 @@ class GoogleAnalytics extends Extension } # Load Analytics tracking code on page request - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page; diff --git a/ext/graphql/main.php b/ext/graphql/main.php index 48de5587..77e9b6cb 100644 --- a/ext/graphql/main.php +++ b/ext/graphql/main.php @@ -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; - use GraphQL\GraphQL as GQL; use GraphQL\Server\StandardServer; use GraphQL\Error\DebugFlag; @@ -74,14 +70,14 @@ class GraphQL extends Extension } } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string('graphql_cors_pattern', ""); $config->set_default_bool('graphql_debug', false); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page; if ($event->page_matches("graphql")) { @@ -92,8 +88,6 @@ class GraphQL extends Extension ]); $t2 = ftime(); $resp = $server->executeRequest(); - assert(!is_array($resp)); - assert(is_a($resp, \GraphQL\Executor\ExecutionResult::class)); if ($config->get_bool("graphql_debug")) { $debug = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS; $body = $resp->toArray($debug); @@ -107,19 +101,16 @@ class GraphQL extends Extension // sleep(1); $page->set_mode(PageMode::DATA); $page->set_mime("application/json"); - $page->set_data(json_encode_ex($body, JSON_UNESCAPED_UNICODE)); + $page->set_data(\json_encode($body, JSON_UNESCAPED_UNICODE)); } if ($event->page_matches("graphql_upload")) { $this->cors(); $page->set_mode(PageMode::DATA); $page->set_mime("application/json"); - $page->set_data(json_encode_ex(self::handle_uploads())); + $page->set_data(\json_encode(self::handle_uploads())); } } - /** - * @return array{error?:string,results?:array} - */ private static function handle_uploads(): array { global $user; @@ -132,7 +123,7 @@ class GraphQL extends Extension $common_source = $_POST['common_source']; $results = []; - for ($n = 0; $n < 100; $n++) { + for ($n=0; $n<100; $n++) { if (empty($_POST["url$n"]) && empty($_FILES["data$n"])) { break; } @@ -140,7 +131,7 @@ class GraphQL extends Extension break; } try { - $results[] = ["image_ids" => self::handle_upload($n, $common_tags, $common_source)]; + $results[] = self::handle_upload($n, $common_tags, $common_source); } catch(\Exception $e) { $results[] = ["error" => $e->getMessage()]; } @@ -148,14 +139,12 @@ class GraphQL extends Extension return ["results" => $results]; } - /** - * @return int[] - */ private static function handle_upload(int $n, string $common_tags, string $common_source): array { - global $database; if (!empty($_POST["url$n"])) { - throw new UploadException("URLs not handled yet"); + return ["error" => "URLs not handled yet"]; + $tmpname = "..."; + $filename = "..."; } else { $ec = $_FILES["data$n"]["error"]; switch($ec) { @@ -164,9 +153,9 @@ class GraphQL extends Extension $filename = $_FILES["data$n"]["name"]; break; case UPLOAD_ERR_INI_SIZE: - throw new UploadException("File larger than PHP can handle"); + return ["error" => "File larger than PHP can handle"]; default: - throw new UploadException("Mystery error: $ec"); + return ["error" => "Mystery error: $ec"]; } } @@ -175,42 +164,38 @@ class GraphQL extends Extension if (!empty($_POST["source$n"])) { $source = $_POST["source$n"]; } - $event = $database->with_savepoint(function () use ($tmpname, $filename, $tags, $source) { - return send_event(new DataUploadEvent($tmpname, [ - 'filename' => $filename, - 'tags' => Tag::explode($tags), - 'source' => $source, - ])); - }); + $event = send_event(new DataUploadEvent($tmpname, [ + 'filename' => $filename, + 'tags' => Tag::explode($tags), + 'source' => $source, + ])); - return array_map(fn ($im) => $im->id, $event->images); + return ["image_id" => $event->image_id]; } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('graphql:query') - ->addArgument('query', InputArgument::REQUIRED) - ->setDescription('Run a GraphQL query') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $query = $input->getArgument('query'); - $t1 = ftime(); - $schema = $this->get_schema(); - $t2 = ftime(); - $debug = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS; - $body = GQL::executeQuery($schema, $query)->toArray($debug); - $t3 = ftime(); - $body['stats'] = get_debug_info_arr(); - $body['stats']['graphql_schema_time'] = round($t2 - $t1, 2); - $body['stats']['graphql_execute_time'] = round($t3 - $t2, 2); - echo json_encode_ex($body, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - return Command::SUCCESS; - }); - $event->app->register('graphql:schema') - ->setDescription('Print out the GraphQL schema') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $schema = $this->get_schema(); - echo(SchemaPrinter::doPrint($schema)); - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + print "\tgraphql \n"; + print "\t\teg 'graphql \"{ post(id: 18) { id, hash } }\"'\n\n"; + print "\tgraphql-schema\n"; + print "\t\tdump the schema\n\n"; + } + if ($event->cmd == "graphql") { + $t1 = ftime(); + $schema = $this->get_schema(); + $t2 = ftime(); + $debug = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS; + $body = GQL::executeQuery($schema, $event->args[0])->toArray($debug); + $t3 = ftime(); + $body['stats'] = get_debug_info_arr(); + $body['stats']['graphql_schema_time'] = round($t2 - $t1, 2); + $body['stats']['graphql_execute_time'] = round($t3 - $t2, 2); + echo \json_encode($body, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT); + } + if ($event->cmd == "graphql-schema") { + $schema = $this->get_schema(); + echo(SchemaPrinter::doPrint($schema)); + } } } diff --git a/ext/graphql/test.php b/ext/graphql/test.php index 98146745..db30ca7f 100644 --- a/ext/graphql/test.php +++ b/ext/graphql/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class GraphQLTest extends ShimmiePHPUnitTestCase { - public function testSchema(): void + public function testSchema() { $schema = GraphQL::get_schema(); $schema->assertValid(); diff --git a/ext/handle_archive/main.php b/ext/handle_archive/main.php index 5d1fa85a..a975643b 100644 --- a/ext/handle_archive/main.php +++ b/ext/handle_archive/main.php @@ -8,13 +8,13 @@ class ArchiveFileHandler extends DataHandlerExtension { protected array $SUPPORTED_MIME = [MimeType::ZIP]; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string('archive_extract_command', 'unzip -d "%d" "%f"'); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Archive Handler Options"); $sb->add_text_option("archive_tmp_dir", "Temporary folder: "); @@ -22,7 +22,7 @@ class ArchiveFileHandler extends DataHandlerExtension $sb->add_label("
    %f for archive, %d for temporary directory"); } - public function onDataUpload(DataUploadEvent $event): void + public function onDataUpload(DataUploadEvent $event) { if ($this->supported_mime($event->mime)) { global $config, $page; @@ -31,27 +31,22 @@ class ArchiveFileHandler extends DataHandlerExtension $cmd = $config->get_string('archive_extract_command'); $cmd = str_replace('%f', $event->tmpname, $cmd); $cmd = str_replace('%d', $tmpdir, $cmd); - assert(is_string($cmd)); exec($cmd); if (file_exists($tmpdir)) { try { - $results = add_dir($tmpdir, $event->metadata['tags']); - foreach ($results as $r) { - if(is_a($r, UploadError::class)) { - $page->flash($r->name." failed: ".$r->error); - } - if(is_a($r, UploadSuccess::class)) { - $event->images[] = Image::by_id($r->image_id); - } + $results = add_dir($tmpdir); + if (count($results) > 0) { + $page->flash("Adding files" . implode("\n", $results)); } } finally { deltree($tmpdir); } + $event->image_id = -2; // default -1 = upload wasn't handled } } } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { } @@ -65,7 +60,7 @@ class ArchiveFileHandler extends DataHandlerExtension return false; } - protected function create_thumb(Image $image): bool + protected function create_thumb(string $hash, string $mime): bool { return false; } diff --git a/ext/handle_archive/test.php b/ext/handle_archive/test.php deleted file mode 100644 index a4582ffa..00000000 --- a/ext/handle_archive/test.php +++ /dev/null @@ -1,29 +0,0 @@ -log_in_as_user(); - system("zip -q tests/test.zip tests/pbx_screenshot.jpg tests/favicon.png"); - $this->post_image("tests/test.zip", "a z"); - - $images = Search::find_images(); - $this->assertEquals(2, count($images)); - $this->assertEquals("a tests z", $images[0]->get_tag_list()); - $this->assertEquals("a tests z", $images[1]->get_tag_list()); - } - - public function tearDown(): void - { - if(file_exists("tests/test.zip")) { - unlink("tests/test.zip"); - } - - parent::tearDown(); - } -} diff --git a/ext/handle_cbz/comic.js b/ext/handle_cbz/comic.js index 613e9cf7..8d0f12ee 100644 --- a/ext/handle_cbz/comic.js +++ b/ext/handle_cbz/comic.js @@ -11,8 +11,8 @@ function Comic(root, comicURL) { self.comicZip = zip; // Shimmie-specific; nullify existing back / forward - document.querySelector("LINK[rel='previous']").remove(); - document.querySelector("LINK[rel='next']").remove(); + $("[rel='previous']").remove(); + $("[rel='next']").remove(); zip.forEach(function (relativePath, file){ self.comicPages.push(relativePath); @@ -53,8 +53,7 @@ function Comic(root, comicURL) { }; this.onKeyUp = function(e) { - let t = e.target; - if (t.tagName === "INPUT" || t.tagName === "TEXTAREA") { return; } + if ($(e.target).is('input,textarea')) { return; } if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) { return; } if (e.keyCode === 37) {self.prev();} else if (e.keyCode === 39) {self.next();} diff --git a/ext/handle_cbz/main.php b/ext/handle_cbz/main.php index c160d52b..1ff44636 100644 --- a/ext/handle_cbz/main.php +++ b/ext/handle_cbz/main.php @@ -24,12 +24,12 @@ class CBZFileHandler extends DataHandlerExtension unlink($tmp); } - protected function create_thumb(Image $image): bool + protected function create_thumb(string $hash, string $mime): bool { - $cover = $this->get_representative_image($image->get_image_filename()); + $cover = $this->get_representative_image(warehouse_path(Image::IMAGE_DIR, $hash)); create_scaled_image( $cover, - $image->get_thumb_filename(), + warehouse_path(Image::THUMBNAIL_DIR, $hash), get_thumbnail_max_size_scaled(), MimeType::get_for_file($cover), null @@ -39,7 +39,7 @@ class CBZFileHandler extends DataHandlerExtension protected function check_contents(string $tmpname): bool { - $fp = false_throws(fopen($tmpname, "r")); + $fp = fopen($tmpname, "r"); $head = fread($fp, 4); fclose($fp); return $head == "PK\x03\x04"; @@ -52,8 +52,8 @@ class CBZFileHandler extends DataHandlerExtension $za = new \ZipArchive(); $za->open($archive); $names = []; - for ($i = 0; $i < $za->numFiles;$i++) { - $file = false_throws($za->statIndex($i)); + for ($i=0; $i<$za->numFiles;$i++) { + $file = $za->statIndex($i); $names[] = $file['name']; } sort($names); diff --git a/ext/handle_cbz/style.css b/ext/handle_cbz/style.css index bb9dbd34..4813925c 100644 --- a/ext/handle_cbz/style.css +++ b/ext/handle_cbz/style.css @@ -1,7 +1,7 @@ #comicMain { background: black; color: white; - font-size: 3rem; + font-size: 3em; } #comicPageList { width: 90%; diff --git a/ext/handle_cbz/theme.php b/ext/handle_cbz/theme.php index 5ff89f3b..ce574a8f 100644 --- a/ext/handle_cbz/theme.php +++ b/ext/handle_cbz/theme.php @@ -6,9 +6,8 @@ namespace Shimmie2; class CBZFileHandlerTheme extends Themelet { - public function display_image(Image $image): void + public function display_image(Page $page, Image $image) { - global $page; $data_href = get_base_href(); $ilink = $image->get_image_link(); $html = " diff --git a/ext/handle_flash/info.php b/ext/handle_flash/info.php new file mode 100644 index 00000000..24f36c39 --- /dev/null +++ b/ext/handle_flash/info.php @@ -0,0 +1,16 @@ +image->lossless = true; + $event->image->video = true; + $event->image->image = false; + + $info = getimagesize($event->image->get_image_filename()); + if ($info) { + $event->image->width = $info[0]; + $event->image->height = $info[1]; + } + } + + protected function create_thumb(string $hash, string $mime): bool + { + if (!Media::create_thumbnail_ffmpeg($hash)) { + copy("ext/handle_flash/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); + } + return true; + } + + protected function check_contents(string $tmpname): bool + { + $fp = fopen($tmpname, "r"); + $head = fread($fp, 3); + fclose($fp); + return in_array($head, ["CWS", "FWS"]); + } +} diff --git a/ext/handle_flash/theme.php b/ext/handle_flash/theme.php new file mode 100644 index 00000000..9e2c799f --- /dev/null +++ b/ext/handle_flash/theme.php @@ -0,0 +1,31 @@ +get_image_link(); + // FIXME: object and embed have "height" and "width" + $html = " + + + + + "; + $page->add_block(new Block("Flash Animation", $html, "main", 10)); + } +} diff --git a/ext/handle_flash/thumb.jpg b/ext/handle_flash/thumb.jpg new file mode 100644 index 00000000..3debb306 Binary files /dev/null and b/ext/handle_flash/thumb.jpg differ diff --git a/ext/handle_ico/main.php b/ext/handle_ico/main.php index 145857a7..2b03096d 100644 --- a/ext/handle_ico/main.php +++ b/ext/handle_ico/main.php @@ -15,10 +15,10 @@ class IcoFileHandler extends DataHandlerExtension $event->image->audio = false; $event->image->image = ($event->image->get_mime() != MimeType::ANI); - $fp = false_throws(fopen($event->image->get_image_filename(), "r")); + $fp = fopen($event->image->get_image_filename(), "r"); try { - fseek($fp, 6); // skip header - $subheader = false_throws(unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", false_throws(fread($fp, 16)))); + unpack("Snull/Stype/Scount", fread($fp, 6)); + $subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16)); $width = $subheader['width']; $height = $subheader['height']; $event->image->width = $width == 0 ? 256 : $width; @@ -28,10 +28,10 @@ class IcoFileHandler extends DataHandlerExtension } } - protected function create_thumb(Image $image): bool + protected function create_thumb(string $hash, string $mime): bool { try { - create_image_thumb($image, MediaEngine::IMAGICK); + create_image_thumb($hash, $mime, MediaEngine::IMAGICK); return true; } catch (MediaException $e) { log_warning("handle_ico", "Could not generate thumbnail. " . $e->getMessage()); @@ -41,8 +41,8 @@ class IcoFileHandler extends DataHandlerExtension protected function check_contents(string $tmpname): bool { - $fp = false_throws(fopen($tmpname, "r")); - $header = false_throws(unpack("Snull/Stype/Scount", false_throws(fread($fp, 6)))); + $fp = fopen($tmpname, "r"); + $header = unpack("Snull/Stype/Scount", fread($fp, 6)); fclose($fp); return ($header['null'] == 0 && ($header['type'] == 0 || $header['type'] == 1)); } diff --git a/ext/handle_ico/test.php b/ext/handle_ico/test.php index 2a1db252..9e108112 100644 --- a/ext/handle_ico/test.php +++ b/ext/handle_ico/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class IcoFileHandlerTest extends ShimmiePHPUnitTestCase { - public function testIcoHander(): void + public function testIcoHander() { $this->log_in_as_user(); $image_id = $this->post_image("ext/static_files/static/favicon.ico", "shimmie favicon"); diff --git a/ext/handle_ico/theme.php b/ext/handle_ico/theme.php index 39405e2c..81f6c74f 100644 --- a/ext/handle_ico/theme.php +++ b/ext/handle_ico/theme.php @@ -6,9 +6,8 @@ namespace Shimmie2; class IcoFileHandlerTheme extends Themelet { - public function display_image(Image $image): void + public function display_image(Page $page, Image $image) { - global $page; $ilink = $image->get_image_link(); $html = " main imagelength = ??? } - protected function create_thumb(Image $image): bool + protected function create_thumb(string $hash, string $mime): bool { - copy("ext/handle_mp3/thumb.jpg", $image->get_thumb_filename()); + copy("ext/handle_mp3/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); return true; } diff --git a/ext/handle_mp3/script.js b/ext/handle_mp3/script.js deleted file mode 100644 index b3a214a9..00000000 --- a/ext/handle_mp3/script.js +++ /dev/null @@ -1,30 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - let main_image = document.getElementById("main_image"); - if (main_image) { - main_image.setAttribute("volume", 0.25); - } - - let base_href = document.body.getAttribute("data-base-href"); - let audio_src = document.getElementById("audio_src"); - if (audio_src) { - let ilink = audio_src.getAttribute("src"); - window.jsmediatags.read(location.origin + base_href + ilink, { - onSuccess: function (tag) { - var artist = tag.tags.artist, - title = tag.tags.title; - - document.getElementById("audio-title").innerText = title; - document.getElementById("audio-artist").innerText = artist; - document - .getElementById("audio-download") - .setAttribute( - "download", - (artist + " - " + title).substring(0, 250) + ".mp3", - ); - }, - onError: function (error) { - console.log(error); - }, - }); - } -}); diff --git a/ext/handle_mp3/theme.php b/ext/handle_mp3/theme.php index ea94094c..769d187d 100644 --- a/ext/handle_mp3/theme.php +++ b/ext/handle_mp3/theme.php @@ -6,18 +6,37 @@ namespace Shimmie2; class MP3FileHandlerTheme extends Themelet { - public function display_image(Image $image): void + public function display_image(Page $page, Image $image) { - global $page; $data_href = get_base_href(); $ilink = $image->get_image_link(); $html = "

    Title: ??? | Artist: ???

    + +

    Download"; $page->add_html_header(""); diff --git a/ext/handle_pixel/main.php b/ext/handle_pixel/main.php index 9c89e4d3..eab2bfc7 100644 --- a/ext/handle_pixel/main.php +++ b/ext/handle_pixel/main.php @@ -42,20 +42,30 @@ class PixelFileHandler extends DataHandlerExtension return $info && in_array($info[2], $valid); } - protected function create_thumb(Image $image): bool + protected function create_thumb(string $hash, string $mime): bool { try { - create_image_thumb($image); + create_image_thumb($hash, $mime); + return true; + } catch (InsufficientMemoryException $e) { + $tsize = get_thumbnail_max_size_scaled(); + $thumb = imagecreatetruecolor($tsize[0], min($tsize[1], 64)); + $white = imagecolorallocate($thumb, 255, 255, 255); + $black = imagecolorallocate($thumb, 0, 0, 0); + imagefill($thumb, 0, 0, $white); + log_warning("handle_pixel", "Insufficient memory while creating thumbnail: ".$e->getMessage()); + imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black); return true; } catch (\Exception $e) { - throw new UploadException("Error while creating thumbnail: ".$e->getMessage()); + log_error("handle_pixel", "Error while creating thumbnail: ".$e->getMessage()); + return false; } } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { if ($event->context == "view") { - $event->add_part(\MicroHTML\rawHTML(" + $event->add_part("

    - "), 20); + ", 20); } } } diff --git a/ext/handle_pixel/script.js b/ext/handle_pixel/script.js index ce09d185..a46ab41e 100644 --- a/ext/handle_pixel/script.js +++ b/ext/handle_pixel/script.js @@ -24,7 +24,7 @@ document.addEventListener('DOMContentLoaded', () => { $(".shm-zoomer").val(zoom_type); if (save_cookie) { - shm_cookie_set("ui-image-zoom", zoom_type); + Cookies.set("ui-image-zoom", zoom_type, {expires: 365}); } } @@ -37,14 +37,23 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - $("img.shm-main-image").click(function(e) { - switch(shm_cookie_get("ui-image-zoom")) { - case "full": zoom("width"); break; - default: zoom("full"); break; + $("img.shm-main-image").click(function(e) + { + switch(Cookies.get("ui-image-zoom")) + { + case "full": zoom("both"); break; + case "both": zoom("full"); break; + default: zoom("both"); break; } }); - if(shm_cookie_get("ui-image-zoom")) { - zoom(shm_cookie_get("ui-image-zoom")); + if(Cookies.get("ui-image-zoom")) { + zoom(Cookies.get("ui-image-zoom")); } + + if(Cookies.get("ui-image-zoom") === undefined) + { + zoom("both"); + } + }); diff --git a/ext/handle_pixel/test.php b/ext/handle_pixel/test.php index c83f06d6..0b12a459 100644 --- a/ext/handle_pixel/test.php +++ b/ext/handle_pixel/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class PixelFileHandlerTest extends ShimmiePHPUnitTestCase { - public function testPixelHander(): void + public function testPixelHander() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); diff --git a/ext/handle_pixel/theme.php b/ext/handle_pixel/theme.php index da3e7f4b..3bf5b302 100644 --- a/ext/handle_pixel/theme.php +++ b/ext/handle_pixel/theme.php @@ -4,32 +4,14 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\IMG; - class PixelFileHandlerTheme extends Themelet { - public function display_image(Image $image): void + public function display_image(Page $page, Image $image) { - global $config, $page; + global $config; - $html = IMG([ - 'alt' => 'main image', - 'class' => 'shm-main-image', - 'id' => 'main_image', - 'src' => $image->get_image_link(), - 'data-width' => $image->width, - 'data-height' => $image->height, - 'data-mime' => $image->get_mime(), - 'onerror' => "shm_log('Error loading >>{$image->id}')", - ]); - $page->add_block(new Block("Image", $html, "main", 10)); - } - - public function display_metadata(Image $image): void - { - global $page; - - if (function_exists(ImageIO::EXIF_READ_FUNCTION)) { + $u_ilink = $image->get_image_link(); + if ($config->get_bool(ImageConfig::SHOW_META) && function_exists(ImageIO::EXIF_READ_FUNCTION)) { # FIXME: only read from jpegs? $exif = @exif_read_data($image->get_image_filename(), "IFD0", true); if ($exif) { @@ -50,5 +32,9 @@ class PixelFileHandlerTheme extends Themelet } } } + + $html = "main image"; + $page->add_block(new Block("Image", $html, "main", 10)); } } diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php index 1eff8776..be79cbb6 100644 --- a/ext/handle_svg/main.php +++ b/ext/handle_svg/main.php @@ -13,7 +13,7 @@ class SVGFileHandler extends DataHandlerExtension /** @var SVGFileHandlerTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page; if ($event->page_matches("get_svg")) { @@ -26,32 +26,12 @@ class SVGFileHandler extends DataHandlerExtension $sanitizer = new Sanitizer(); $sanitizer->removeRemoteReferences(true); - $dirtySVG = file_get_contents_ex(warehouse_path(Image::IMAGE_DIR, $hash)); + $dirtySVG = file_get_contents(warehouse_path(Image::IMAGE_DIR, $hash)); $cleanSVG = $sanitizer->sanitize($dirtySVG); $page->set_data($cleanSVG); } } - public function onDataUpload(DataUploadEvent $event): void - { - global $config; - - if ($this->supported_mime($event->mime)) { - // If the SVG handler intends to handle this file, - // then sanitise it before touching it - $sanitizer = new Sanitizer(); - $sanitizer->removeRemoteReferences(true); - $dirtySVG = file_get_contents_ex($event->tmpname); - $cleanSVG = false_throws($sanitizer->sanitize($dirtySVG)); - $event->hash = md5($cleanSVG); - $new_tmpname = shm_tempnam("svg"); - file_put_contents($new_tmpname, $cleanSVG); - $event->set_tmpname($new_tmpname); - - parent::onDataUpload($event); - } - } - protected function media_check_properties(MediaCheckPropertiesEvent $event): void { $event->image->lossless = true; @@ -64,26 +44,36 @@ class SVGFileHandler extends DataHandlerExtension $event->image->height = $msp->height; } - protected function create_thumb(Image $image): bool + protected function move_upload_to_archive(DataUploadEvent $event) + { + $sanitizer = new Sanitizer(); + $sanitizer->removeRemoteReferences(true); + $dirtySVG = file_get_contents($event->tmpname); + $cleanSVG = $sanitizer->sanitize($dirtySVG); + $event->hash = md5($cleanSVG); + file_put_contents(warehouse_path(Image::IMAGE_DIR, $event->hash), $cleanSVG); + } + + protected function create_thumb(string $hash, string $mime): bool { try { // Normally we require imagemagick, but for unit tests we can use a no-op engine if (defined('UNITTEST')) { - create_image_thumb($image); + create_image_thumb($hash, $mime); } else { - create_image_thumb($image, MediaEngine::IMAGICK); + create_image_thumb($hash, $mime, MediaEngine::IMAGICK); } return true; } catch (MediaException $e) { log_warning("handle_svg", "Could not generate thumbnail. " . $e->getMessage()); - copy("ext/handle_svg/thumb.jpg", $image->get_thumb_filename()); + copy("ext/handle_svg/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); return false; } } protected function check_contents(string $tmpname): bool { - if (MimeType::get_for_file($tmpname) !== MimeType::SVG) { + if (MimeType::get_for_file($tmpname)!==MimeType::SVG) { return false; } @@ -95,22 +85,19 @@ class SVGFileHandler extends DataHandlerExtension class MiniSVGParser { public bool $valid = false; - public int $width = 0; - public int $height = 0; - private int $xml_depth = 0; + public int $width=0; + public int $height=0; + private int $xml_depth=0; public function __construct(string $file) { $xml_parser = xml_parser_create(); xml_set_element_handler($xml_parser, [$this, "startElement"], [$this, "endElement"]); - $this->valid = bool_escape(xml_parse($xml_parser, file_get_contents_ex($file), true)); + $this->valid = bool_escape(xml_parse($xml_parser, file_get_contents($file), true)); xml_parser_free($xml_parser); } - /** - * @param array $attrs - */ - public function startElement(mixed $parser, string $name, array $attrs): void + public function startElement($parser, $name, $attrs): void { if ($name == "SVG" && $this->xml_depth == 0) { $this->width = int_escape($attrs["WIDTH"]); @@ -119,7 +106,7 @@ class MiniSVGParser $this->xml_depth++; } - public function endElement(mixed $parser, string $name): void + public function endElement($parser, $name): void { $this->xml_depth--; } diff --git a/ext/handle_svg/test.php b/ext/handle_svg/test.php index 958f4cf1..60652ffe 100644 --- a/ext/handle_svg/test.php +++ b/ext/handle_svg/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class SVGFileHandlerTest extends ShimmiePHPUnitTestCase { - public function testSVGHander(): void + public function testSVGHander() { $this->log_in_as_user(); $image_id = $this->post_image("tests/test.svg", "something"); @@ -18,7 +18,7 @@ class SVGFileHandlerTest extends ShimmiePHPUnitTestCase # FIXME: test that it gets displayed properly } - public function testAbusiveSVG(): void + public function testAbusiveSVG() { $this->log_in_as_user(); $image_id = $this->post_image("tests/alert.svg", "something"); diff --git a/ext/handle_svg/theme.php b/ext/handle_svg/theme.php index a37d0051..37557581 100644 --- a/ext/handle_svg/theme.php +++ b/ext/handle_svg/theme.php @@ -6,9 +6,8 @@ namespace Shimmie2; class SVGFileHandlerTheme extends Themelet { - public function display_image(Image $image): void + public function display_image(Page $page, Image $image) { - global $page; $ilink = make_link("get_svg/{$image->id}/{$image->id}.svg"); // $ilink = $image->get_image_link(); $html = " diff --git a/ext/handle_video/info.php b/ext/handle_video/info.php index 3385d212..d200f785 100644 --- a/ext/handle_video/info.php +++ b/ext/handle_video/info.php @@ -10,7 +10,7 @@ class VideoFileHandlerInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Handle Video"; - public array $authors = ["velocity37" => "velocity37@gmail.com",self::SHISH_NAME => self::SHISH_EMAIL, "jgen" => "jeffgenovy@gmail.com", "im-mi" => "im.mi.mail.mi@gmail.com"]; + public array $authors = ["velocity37"=>"velocity37@gmail.com",self::SHISH_NAME=>self::SHISH_EMAIL, "jgen"=>"jeffgenovy@gmail.com", "im-mi"=>"im.mi.mail.mi@gmail.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Handle FLV, MP4, OGV and WEBM video files."; public ?string $documentation = diff --git a/ext/handle_video/main.php b/ext/handle_video/main.php index e3f78053..9d195b9d 100644 --- a/ext/handle_video/main.php +++ b/ext/handle_video/main.php @@ -26,7 +26,7 @@ class VideoFileHandler extends DataHandlerExtension ]; protected array $SUPPORTED_MIME = self::SUPPORTED_MIME; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; @@ -39,9 +39,6 @@ class VideoFileHandler extends DataHandlerExtension ); } - /** - * @return array - */ private function get_options(): array { $output = []; @@ -51,7 +48,7 @@ class VideoFileHandler extends DataHandlerExtension return $output; } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Video Options"); $sb->start_table(); @@ -89,14 +86,20 @@ class VideoFileHandler extends DataHandlerExtension break; } } - $event->image->width = max($event->image->width, @$stream["width"]); - $event->image->height = max($event->image->height, @$stream["height"]); + if (array_key_exists("width", $stream) && !empty($stream["width"]) + && is_numeric($stream["width"]) && intval($stream["width"]) > ($event->image->width) ?? 0) { + $event->image->width = intval($stream["width"]); + } + if (array_key_exists("height", $stream) && !empty($stream["height"]) + && is_numeric($stream["height"]) && intval($stream["height"]) > ($event->image->height) ?? 0) { + $event->image->height = intval($stream["height"]); + } } } $event->image->video = $video; $event->image->video_codec = $video_codec; $event->image->audio = $audio; - if ($event->image->get_mime() == MimeType::MKV && + if ($event->image->get_mime()==MimeType::MKV && VideoContainers::is_video_codec_supported(VideoContainers::WEBM, $event->image->video_codec)) { // WEBMs are MKVs with the VP9 or VP8 codec // For browser-friendliness, we'll just change the mime type @@ -104,7 +107,7 @@ class VideoFileHandler extends DataHandlerExtension } } } - if (array_key_exists("format", $data) && is_array($data["format"])) { + if (array_key_exists("format", $data)&& is_array($data["format"])) { $format = $data["format"]; if (array_key_exists("duration", $format) && is_numeric($format["duration"])) { $event->image->length = (int)floor(floatval($format["duration"]) * 1000); @@ -124,9 +127,9 @@ class VideoFileHandler extends DataHandlerExtension return MimeType::matches_array($mime, $enabled_formats, true); } - protected function create_thumb(Image $image): bool + protected function create_thumb(string $hash, string $mime): bool { - return Media::create_thumbnail_ffmpeg($image); + return Media::create_thumbnail_ffmpeg($hash); } protected function check_contents(string $tmpname): bool diff --git a/ext/handle_video/theme.php b/ext/handle_video/theme.php index 24664cc0..87453343 100644 --- a/ext/handle_video/theme.php +++ b/ext/handle_video/theme.php @@ -4,48 +4,52 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\{A, BR, VIDEO, SOURCE, emptyHTML}; - class VideoFileHandlerTheme extends Themelet { - public function display_image(Image $image): void + public function display_image(Page $page, Image $image) { - global $config, $page; + global $config; + $ilink = $image->get_image_link(); + $thumb_url = make_http($image->get_thumb_link()); //used as fallback image + $mime = strtolower($image->get_mime()); + $autoplay = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY); + $loop = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_LOOP); + $mute = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_MUTE); - $width = "auto"; - if ($image->width > 1) { + $width="auto"; + if ($image->width>1) { $width = $image->width."px"; } - $height = "auto"; - if ($image->height > 1) { + $height="auto"; + if ($image->height>1) { $height = $image->height."px"; } - $html = emptyHTML( - "Video not playing? ", - A(['href' => $image->get_image_link()], "Click here"), - " to download the file.", - BR(), - VIDEO( - [ - 'controls' => true, - 'class' => 'shm-main-image', - 'id' => 'main_image', - 'alt' => 'main image', - 'poster' => make_http($image->get_thumb_link()), - 'autoplay' => $config->get_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY), - 'loop' => $config->get_bool(VideoFileHandlerConfig::PLAYBACK_LOOP), - 'muted' => $config->get_bool(VideoFileHandlerConfig::PLAYBACK_MUTE), - 'style' => "height: $height; width: $width; max-width: 100%; object-fit: contain; background-color: black;", - 'onloadstart' => 'this.volume = 0.25', - ], - SOURCE([ - 'src' => $image->get_image_link(), - 'type' => strtolower($image->get_mime()) - ]) - ) - ); + $html = "Video not playing? Click here to download the file.
    "; + //Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats + + if (MimeType::matches_array($mime, VideoFileHandler::SUPPORTED_MIME)) { + if ($mime == MimeType::WEBM) { + //Several browsers still lack WebM support sadly: https://caniuse.com/#feat=webm + $html .= ""; + } + + $autoplay = ($autoplay ? ' autoplay' : ''); + $loop = ($loop ? ' loop' : ''); + $mute = ($mute ? ' muted' : ''); + + $html .= " + + + "; + } else { + //This should never happen, but just in case let's have a fallback.. + $html = "Video type '$mime' not recognised"; + } $page->add_block(new Block("Video", $html, "main", 10)); } } diff --git a/ext/hellban/info.php b/ext/hellban/info.php new file mode 100644 index 00000000..12a3caaa --- /dev/null +++ b/ext/hellban/info.php @@ -0,0 +1,19 @@ +can(Permissions::HELLBANNED)) { + $s = ""; + } elseif ($user->can(Permissions::VIEW_HELLBANNED)) { + $s = "DIV.hb, TR.hb TD {border: 1px solid red !important;}"; + } else { + $s = ".hb {display: none !important;}"; + } + + if ($s) { + $page->add_html_header(""); + } + } +} diff --git a/ext/help_pages/info.php b/ext/help_pages/info.php index f31a3837..e01c44ae 100644 --- a/ext/help_pages/info.php +++ b/ext/help_pages/info.php @@ -10,7 +10,7 @@ class HelpPagesInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Help Pages"; - 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 documentation screens"; public ExtensionVisibility $visibility = ExtensionVisibility::HIDDEN; diff --git a/ext/help_pages/main.php b/ext/help_pages/main.php index e63f5c67..770b0cb1 100644 --- a/ext/help_pages/main.php +++ b/ext/help_pages/main.php @@ -6,10 +6,9 @@ namespace Shimmie2; class HelpPageListBuildingEvent extends Event { - /** @var array */ public array $pages = []; - public function add_page(string $key, string $name): void + public function add_page(string $key, string $name) { $this->pages[$key] = $name; } @@ -18,7 +17,6 @@ class HelpPageListBuildingEvent extends Event class HelpPageBuildingEvent extends Event { public string $key; - /** @var array */ public array $blocks = []; public function __construct(string $key) @@ -27,12 +25,12 @@ class HelpPageBuildingEvent extends Event $this->key = $key; } - public function add_block(Block $block, int $position = 50): void + public function add_block(Block $block, int $position = 50) { - while (array_key_exists($position, $this->blocks)) { - $position++; + if (!array_key_exists("$position", $this->blocks)) { + $this->blocks["$position"] = []; } - $this->blocks[$position] = $block; + $this->blocks["$position"][] = $block; } } @@ -42,7 +40,7 @@ class HelpPages extends Extension protected Themelet $theme; public const SEARCH = "search"; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page; @@ -65,43 +63,46 @@ class HelpPages extends Extension $this->theme->display_help_page($title); $hpbe = send_event(new HelpPageBuildingEvent($name)); - ksort($hpbe->blocks); - foreach ($hpbe->blocks as $block) { - $page->add_block($block); + asort($hpbe->blocks); + + foreach ($hpbe->blocks as $key=>$value) { + foreach ($value as $block) { + $page->add_block($block); + } } } } } - public function onHelpPageListBuilding(HelpPageListBuildingEvent $event): void + public function onHelpPageListBuilding(HelpPageListBuildingEvent $event) { $event->add_page("search", "Searching"); $event->add_page("licenses", "Licenses"); } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("help", new Link('help'), "Help"); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "help") { + if ($event->parent=="help") { $pages = send_event(new HelpPageListBuildingEvent())->pages; - foreach ($pages as $key => $value) { + foreach ($pages as $key=>$value) { $event->add_nav_link("help_".$key, new Link('help/'.$key), $value); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { $event->add_link("Help", make_link("help")); } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key == "licenses") { + if ($event->key=="licenses") { $block = new Block("Software Licenses"); $block->body = "The code in Shimmie is contributed by numerous authors under multiple licenses. For reference, these licenses are listed below. The base software is in general licensed under the GPLv2 license."; $event->add_block($block); diff --git a/ext/help_pages/test.php b/ext/help_pages/test.php index 151f7455..80a5de59 100644 --- a/ext/help_pages/test.php +++ b/ext/help_pages/test.php @@ -6,13 +6,13 @@ namespace Shimmie2; class HelpPagesTest extends ShimmiePHPUnitTestCase { - public function test_list(): void + public function test_list() { send_event(new HelpPageListBuildingEvent()); $this->assertTrue(true); } - public function test_page(): void + public function test_page() { send_event(new HelpPageBuildingEvent("test")); $this->assertTrue(true); diff --git a/ext/help_pages/theme.php b/ext/help_pages/theme.php index 0087a4bb..669d378f 100644 --- a/ext/help_pages/theme.php +++ b/ext/help_pages/theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class HelpPagesTheme extends Themelet { - /** - * @param array $pages - */ - public function display_list_page(array $pages): void + public function display_list_page(array $pages) { global $page; @@ -17,7 +14,7 @@ class HelpPagesTheme extends Themelet $page->set_heading("Help Pages"); $nav_block = new Block("Help", "", "left", 0); - foreach ($pages as $link => $desc) { + foreach ($pages as $link=>$desc) { $link = make_link("help/{$link}"); $nav_block->body .= "".html_escape($desc)."
    "; } @@ -26,7 +23,7 @@ class HelpPagesTheme extends Themelet $page->add_block(new Block("Help Pages", "See list of pages to left")); } - public function display_help_page(string $title): void + public function display_help_page(String $title) { global $page; diff --git a/ext/holiday/info.php b/ext/holiday/info.php index c6092af7..a1963a04 100644 --- a/ext/holiday/info.php +++ b/ext/holiday/info.php @@ -11,7 +11,7 @@ class HolidayInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Holiday Theme"; public string $url = "http://www.codeanimu.net"; - public array $authors = ["DakuTree" => "thedakutree@codeanimu.net"]; + public array $authors = ["DakuTree"=>"thedakutree@codeanimu.net"]; public string $license = self::LICENSE_GPLV2; public string $description = "Use an additional stylesheet on certain holidays"; } diff --git a/ext/holiday/main.php b/ext/holiday/main.php index 28793cc9..4386fe5c 100644 --- a/ext/holiday/main.php +++ b/ext/holiday/main.php @@ -9,19 +9,19 @@ class Holiday extends Extension /** @var HolidayTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool("holiday_aprilfools", false); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Holiday Theme"); $sb->add_bool_option("holiday_aprilfools", "Enable April Fools"); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config; if (date('d/m') == '01/04' && $config->get_bool("holiday_aprilfools")) { diff --git a/ext/holiday/theme.php b/ext/holiday/theme.php index b1504ccc..762e0b99 100644 --- a/ext/holiday/theme.php +++ b/ext/holiday/theme.php @@ -6,7 +6,7 @@ namespace Shimmie2; class HolidayTheme extends Themelet { - public function display_holiday(?string $holiday): void + public function display_holiday(?string $holiday) { global $page; if ($holiday) { diff --git a/ext/home/info.php b/ext/home/info.php index c7fb1518..e0b3a6cd 100644 --- a/ext/home/info.php +++ b/ext/home/info.php @@ -10,7 +10,7 @@ class HomeInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Home Page"; - public array $authors = ["Bzchan" => "bzchan@animemahou.com"]; + public array $authors =["Bzchan"=>"bzchan@animemahou.com"]; public string $license = self::LICENSE_GPLV2; public ExtensionVisibility $visibility = ExtensionVisibility::ADMIN; public string $description = "Displays a front page with logo, search box and post count"; diff --git a/ext/home/main.php b/ext/home/main.php index 9aac0011..6928167f 100644 --- a/ext/home/main.php +++ b/ext/home/main.php @@ -9,7 +9,7 @@ class Home extends Extension /** @var HomeTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page; if ($event->page_matches("home")) { @@ -23,12 +23,12 @@ class Home extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $counters = []; $counters["None"] = "none"; $counters["Text-only"] = "text-only"; - foreach (glob_ex("ext/home/counters/*") as $counter_dirname) { + foreach (glob("ext/home/counters/*") as $counter_dirname) { $name = str_replace("ext/home/counters/", "", $counter_dirname); $counters[ucfirst($name)] = $name; } @@ -52,14 +52,16 @@ class Home extends Extension } $counter_dir = $config->get_string('home_counter', 'default'); - $total = Search::count_images(); - $num_comma = number_format($total); + $num_comma = ""; $counter_text = ""; if ($counter_dir != 'none') { + $total = Image::count_images(); + $num_comma = number_format($total); + if ($counter_dir != 'text-only') { $strtotal = "$total"; $length = strlen($strtotal); - for ($n = 0; $n < $length; $n++) { + for ($n=0; $n<$length; $n++) { $cur = $strtotal[$n]; $counter_text .= "$cur"; } @@ -71,10 +73,10 @@ class Home extends Extension $main_links = $config->get_string('home_links'); } else { $main_links = '[url=site://post/list]Posts[/url][url=site://comment/list]Comments[/url][url=site://tags]Tags[/url]'; - if (Extension::is_enabled(PoolsInfo::KEY)) { + if (class_exists("Shimmie2\Pools")) { $main_links .= '[url=site://pool/list]Pools[/url]'; } - if (Extension::is_enabled(WikiInfo::KEY)) { + if (class_exists("Shimmie2\Wiki")) { $main_links .= '[url=site://wiki]Wiki[/url]'; } diff --git a/ext/home/style.css b/ext/home/style.css index a8f7f416..234f512d 100644 --- a/ext/home/style.css +++ b/ext/home/style.css @@ -1,12 +1,13 @@ -div#front-page h1 {font-size: 4rem; margin-top: 2em; margin-bottom: 0; text-align: center; border: none; background: none; box-shadow: none; -webkit-box-shadow: none; -moz-box-shadow: none;} +div#front-page h1 {font-size: 4em; margin-top: 2em; margin-bottom: 0; text-align: center; border: none; background: none; box-shadow: none; -webkit-box-shadow: none; -moz-box-shadow: none;} div#front-page {text-align:center;} -div#front-page .space {margin-bottom: 1em;} +.space {margin-bottom: 1em;} div#front-page div#links a {margin: 0 0.5em;} div#front-page li {list-style-type: none; margin: 0;} @media (max-width: 800px) { - div#front-page h1 {font-size: 3rem; margin-top: 0.5em; margin-bottom: 0.5em;} - div#front-page #counter {display: none;} + div#front-page h1 {font-size: 3em; margin-top: 0.5em; margin-bottom: 0.5em;} + #counter {display: none;} } div#front-page > #search > form { margin: 0 auto; } +div#front-page > #search > form > ul { width: 225px; vertical-align: middle; display: inline-block; } div#front-page > #search > form > input[type=submit]{ padding: 4px 6px; } diff --git a/ext/home/test.php b/ext/home/test.php index b4813cda..3dece097 100644 --- a/ext/home/test.php +++ b/ext/home/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class HomeTest extends ShimmiePHPUnitTestCase { - public function testHomePage(): void + public function testHomePage() { $page = $this->get_page('home'); $this->assertStringContainsString("Posts", $page->data); diff --git a/ext/home/theme.php b/ext/home/theme.php index 574e7eb4..a0b37efe 100644 --- a/ext/home/theme.php +++ b/ext/home/theme.php @@ -29,7 +29,7 @@ EOD ); } - public function build_body(string $sitename, string $main_links, string $main_text, string $contact_link, string $num_comma, string $counter_text): string + public function build_body(string $sitename, string $main_links, string $main_text, string $contact_link, $num_comma, string $counter_text): string { $main_links_html = empty($main_links) ? "" : ""; $message_html = empty($main_text) ? "" : "
    $main_text
    "; @@ -37,9 +37,9 @@ EOD $contact_link = empty($contact_link) ? "" : "
    Contact –"; $search_html = " diff --git a/ext/image/config.php b/ext/image/config.php index ce65612b..7f031131 100644 --- a/ext/image/config.php +++ b/ext/image/config.php @@ -15,7 +15,7 @@ abstract class ImageConfig public const THUMB_QUALITY = 'thumb_quality'; public const THUMB_MIME = 'thumb_mime'; public const THUMB_FIT = 'thumb_fit'; - public const THUMB_ALPHA_COLOR = 'thumb_alpha_color'; + public const THUMB_ALPHA_COLOR ='thumb_alpha_color'; public const SHOW_META = 'image_show_meta'; public const ILINK = 'image_ilink'; diff --git a/ext/image/info.php b/ext/image/info.php index b7202dff..8d9b7ba6 100644 --- a/ext/image/info.php +++ b/ext/image/info.php @@ -11,7 +11,7 @@ class ImageIOInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Post Manager"; public string $url = self::SHIMMIE_URL; - public array $authors = [self::SHISH_NAME => self::SHISH_EMAIL, "jgen" => "jgen.tech@gmail.com"]; + public array $authors = [self::SHISH_NAME=> self::SHISH_EMAIL, "jgen"=>"jgen.tech@gmail.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Handle the image database"; public ExtensionVisibility $visibility = ExtensionVisibility::HIDDEN; diff --git a/ext/image/main.php b/ext/image/main.php index b7f8627d..94effd32 100644 --- a/ext/image/main.php +++ b/ext/image/main.php @@ -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; - require_once "config.php"; /** @@ -19,13 +15,13 @@ class ImageIO extends Extension protected Themelet $theme; public const COLLISION_OPTIONS = [ - 'Error' => ImageConfig::COLLISION_ERROR, - 'Merge' => ImageConfig::COLLISION_MERGE + 'Error'=>ImageConfig::COLLISION_ERROR, + 'Merge'=>ImageConfig::COLLISION_MERGE ]; public const ON_DELETE_OPTIONS = [ - 'Return to post list' => ImageConfig::ON_DELETE_LIST, - 'Go to next post' => ImageConfig::ON_DELETE_NEXT + 'Return to post list'=>ImageConfig::ON_DELETE_LIST, + 'Go to next post'=>ImageConfig::ON_DELETE_NEXT ]; public const EXIF_READ_FUNCTION = "exif_read_data"; @@ -40,7 +36,7 @@ class ImageIO extends Extension 'WEBP (Not IE compatible)' => MimeType::WEBP ]; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string(ImageConfig::THUMB_ENGINE, MediaEngine::GD); @@ -59,10 +55,10 @@ class ImageIO extends Extension $config->set_default_string(ImageConfig::TLINK, ''); $config->set_default_string(ImageConfig::TIP, '$tags // $size // $filesize'); $config->set_default_string(ImageConfig::UPLOAD_COLLISION_HANDLER, ImageConfig::COLLISION_ERROR); - $config->set_default_int(ImageConfig::EXPIRES, (60 * 60 * 24 * 31)); // defaults to one month + $config->set_default_int(ImageConfig::EXPIRES, (60*60*24*31)); // defaults to one month } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $config; @@ -81,14 +77,9 @@ class ImageIO extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { - global $config, $page; - - $thumb_width = $config->get_int(ImageConfig::THUMB_WIDTH, 192); - $thumb_height = $config->get_int(ImageConfig::THUMB_HEIGHT, 192); - $page->add_html_header(""); - + global $config; if ($event->page_matches("image/delete")) { global $page, $user; if ($user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) { @@ -96,14 +87,26 @@ class ImageIO extends Extension if ($image) { send_event(new ImageDeletionEvent($image)); - if ($config->get_string(ImageConfig::ON_DELETE) === ImageConfig::ON_DELETE_NEXT) { + if ($config->get_string(ImageConfig::ON_DELETE)===ImageConfig::ON_DELETE_NEXT) { redirect_to_next_image($image); } else { $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(referer_or(make_link(), ['post/view'])); + $page->set_redirect(referer_or(make_link("post/list"), ['post/view'])); } } } + } elseif ($event->page_matches("image/replace")) { + global $page, $user; + if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) { + $image = Image::by_id(int_escape($_POST['image_id'])); + if ($image) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link('upload/replace/'.$image->id)); + } else { + /* Invalid image ID */ + throw new ImageReplaceException("Post to replace does not exist."); + } + } } elseif ($event->page_matches("image")) { $num = int_escape($event->get_arg(0)); $this->send_file($num, "image"); @@ -113,50 +116,161 @@ class ImageIO extends Extension } } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::DELETE_IMAGE)) { $event->add_part($this->theme->get_deleter_html($event->image->id)); } + /* In the future, could perhaps allow users to replace images that they own as well... */ + if ($user->can(Permissions::REPLACE_IMAGE)) { + $event->add_part($this->theme->get_replace_html($event->image->id)); + } } - public function onCliGen(CliGenEvent $event): void + public function onImageAddition(ImageAdditionEvent $event) { - $event->app->register('delete') - ->addArgument('id', InputArgument::REQUIRED) - ->setDescription('Delete a specific post') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $post_id = (int)$input->getArgument('id'); - $image = Image::by_id($post_id); - send_event(new ImageDeletionEvent($image)); - return Command::SUCCESS; - }); + global $config; + + try { + $image = $event->image; + + /* + * Validate things + */ + if (strlen(trim($image->source ?? '')) == 0) { + $image->source = null; + } + + /* + * Check for an existing image + */ + $existing = Image::by_hash($image->hash); + if (!is_null($existing)) { + $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); + if ($handler == ImageConfig::COLLISION_MERGE || isset($_GET['update'])) { + $merged = array_merge($image->get_tag_array(), $existing->get_tag_array()); + send_event(new TagSetEvent($existing, $merged)); + if (isset($_GET['rating']) && isset($_GET['update']) && Extension::is_enabled(RatingsInfo::KEY)) { + send_event(new RatingSetEvent($existing, $_GET['rating'])); + } + if (isset($_GET['source']) && isset($_GET['update'])) { + send_event(new SourceSetEvent($existing, $_GET['source'])); + } + $event->merged = true; + $im = Image::by_id($existing->id); + assert(!is_null($image)); + $event->image = $im; + return; + } else { + $error = "Post {$existing->id} ". + "already has hash {$image->hash}:

    ".$this->theme->build_thumb_html($existing); + throw new ImageAdditionException($error); + } + } + + // actually insert the info + $image->save_to_db(); + + log_info("image", "Uploaded >>{$image->id} ({$image->hash})"); + + # at this point in time, the image's tags haven't really been set, + # and so, having $image->tag_array set to something is a lie (but + # a useful one, as we want to know what the tags are /supposed/ to + # be). Here we correct the lie, by first nullifying the wrong tags + # then using the standard mechanism to set them properly. + $tags_to_set = $image->get_tag_array(); + $image->tag_array = []; + send_event(new TagSetEvent($image, $tags_to_set)); + if ($image->source) { + send_event(new SourceSetEvent($image, $image->source)); + } + } catch (ImageAdditionException $e) { + throw new UploadException($e->error); + } } - public function onImageAddition(ImageAdditionEvent $event): void - { - send_event(new ThumbnailGenerationEvent($event->image)); - log_info("image", "Uploaded >>{$event->image->id} ({$event->image->hash})"); - } - - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { $event->image->delete(); } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onCommand(CommandEvent $event) + { + if ($event->cmd == "help") { + print "\tdelete \n"; + print "\t\tdelete a specific post\n\n"; + } + if ($event->cmd == "delete") { + $post_id = (int)$event->args[0]; + $image = Image::by_id($post_id); + send_event(new ImageDeletionEvent($image)); + } + } + + public function onImageReplace(ImageReplaceEvent $event) + { + try { + $id = $event->id; + $image = $event->image; + + $image->set_mime( + MimeType::get_for_file($image->get_image_filename()) + ); + + /* Check to make sure the image exists. */ + $existing = Image::by_id($id); + + if (is_null($existing)) { + throw new ImageReplaceException("Post to replace does not exist!"); + } + + $duplicate = Image::by_hash($image->hash); + if (!is_null($duplicate) && $duplicate->id!=$id) { + $error = "Post {$duplicate->id} " . + "already has hash {$image->hash}:

    " . $this->theme->build_thumb_html($duplicate); + throw new ImageReplaceException($error); + } + + if (strlen(trim($image->source ?? '')) == 0) { + $image->source = $existing->get_source(); + } + + // Update the data in the database. + $image->id = $id; + send_event(new MediaCheckPropertiesEvent($image)); + $image->save_to_db(); + + /* + This step could be optional, ie: perhaps move the image somewhere + and have it stored in a 'replaced images' list that could be + inspected later by an admin? + */ + + log_debug("image", "Removing image with hash " . $existing->hash); + $existing->remove_image_only(); // Actually delete the old image file from disk + + /* Generate new thumbnail */ + send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->get_mime()))); + + log_info("image", "Replaced >>{$id} with ({$image->hash})"); + } catch (ImageReplaceException $e) { + throw new UploadException($e->error); + } + } + + public function onUserPageBuilding(UserPageBuildingEvent $event) { $u_name = url_escape($event->display_user->name); - $i_image_count = Search::count_images(["user={$event->display_user->name}"]); - $i_days_old = ((time() - strtotime_ex($event->display_user->join_date)) / 86400) + 1; + $i_image_count = Image::count_images(["user={$event->display_user->name}"]); + $i_days_old = ((time() - strtotime($event->display_user->join_date)) / 86400) + 1; $h_image_rate = sprintf("%.1f", ($i_image_count / $i_days_old)); - $images_link = search_link(["user=$u_name"]); + $images_link = make_link("post/list/user=$u_name/1"); $event->add_stats("Posts uploaded: $i_image_count, $h_image_rate per day"); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { global $config; @@ -192,17 +306,17 @@ class ImageIO extends Extension $sb->add_int_option(ImageConfig::THUMB_QUALITY, "Quality", true); $sb->add_int_option(ImageConfig::THUMB_SCALING, "High-DPI Scale %", true); - if ($config->get_string(ImageConfig::THUMB_MIME) === MimeType::JPEG) { + if ($config->get_string(ImageConfig::THUMB_MIME)===MimeType::JPEG) { $sb->add_color_option(ImageConfig::THUMB_ALPHA_COLOR, "Alpha Conversion Color", true); } $sb->end_table(); } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { $fname = $event->image->get_filename(); - $base_fname = basename($fname, '.' . $event->image->get_ext()); + $base_fname = str_contains($fname, '.') ? substr($fname, 0, strrpos($fname, '.')) : $fname; $event->replace('$id', (string)$event->image->id); $event->replace('$hash_ab', substr($event->image->hash, 0, 2)); @@ -211,13 +325,11 @@ class ImageIO extends Extension $event->replace('$filesize', to_shorthand_int($event->image->filesize)); $event->replace('$filename', $base_fname); $event->replace('$ext', $event->image->get_ext()); - if(isset($event->image->posted)) { - $event->replace('$date', autodate($event->image->posted, false)); - } + $event->replace('$date', autodate($event->image->posted, false)); $event->replace("\\n", "\n"); } - private function send_file(int $image_id, string $type): void + private function send_file(int $image_id, string $type) { global $config, $page; @@ -243,7 +355,7 @@ class ImageIO extends Extension } else { $if_modified_since = ""; } - $gmdate_mod = gmdate('D, d M Y H:i:s', false_throws(filemtime($file))) . ' GMT'; + $gmdate_mod = gmdate('D, d M Y H:i:s', filemtime($file)) . ' GMT'; if ($if_modified_since == $gmdate_mod) { $page->set_mode(PageMode::DATA); diff --git a/ext/image/test.php b/ext/image/test.php index d2eef83c..881d56a3 100644 --- a/ext/image/test.php +++ b/ext/image/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class ImageIOTest extends ShimmiePHPUnitTestCase { - public function testUserStats(): void + public function testUserStats() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); @@ -26,12 +26,22 @@ class ImageIOTest extends ShimmiePHPUnitTestCase $this->assertEquals(200, $page->code); } - public function testDeleteRequest(): void + public function testDeleteRequest() { $this->log_in_as_admin(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); $_POST['image_id'] = "$image_id"; - send_event(new PageRequestEvent("POST", "image/delete")); + send_event(new PageRequestEvent("image/delete")); $this->assertTrue(true); // FIXME: assert image was deleted? } + + public function testReplaceRequest() + { + global $page; + $this->log_in_as_admin(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); + $_POST['image_id'] = "$image_id"; + send_event(new PageRequestEvent("image/replace")); + $this->assertEquals(PageMode::REDIRECT, $page->mode); + } } diff --git a/ext/image/theme.php b/ext/image/theme.php index 8d5200fb..3a016fc7 100644 --- a/ext/image/theme.php +++ b/ext/image/theme.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\{INPUT, emptyHTML}; +use function MicroHTML\INPUT; class ImageIOTheme extends Themelet { @@ -12,13 +12,24 @@ class ImageIOTheme extends Themelet * Display a link to delete an image * (Added inline Javascript to confirm the deletion) */ - public function get_deleter_html(int $image_id): \MicroHTML\HTMLElement + public function get_deleter_html(int $image_id): string { - $form = SHM_FORM("image/delete", form_id: "image_delete_form"); - $form->appendChild(emptyHTML( - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image_id]), - INPUT(["type" => 'submit', "value" => 'Delete', "onclick" => 'return confirm("Delete the image?");', "id" => "image_delete_button"]), - )); - return $form; + return (string)"".SHM_SIMPLE_FORM( + "image/delete", + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image_id]), + INPUT(["type"=>'submit', "value"=>'Delete', "onclick"=>'return confirm("Delete the image?");', "id"=>"image_delete_button"]), + ).""; + } + + /** + * Display link to replace the image + */ + public function get_replace_html(int $image_id): string + { + return (string)SHM_SIMPLE_FORM( + "image/replace", + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image_id]), + INPUT(["type"=>'submit', "value"=>'Replace']), + ); } } diff --git a/ext/image_hash_ban/info.php b/ext/image_hash_ban/info.php index 1feefb55..339fa5ce 100644 --- a/ext/image_hash_ban/info.php +++ b/ext/image_hash_ban/info.php @@ -11,7 +11,7 @@ class ImageBanInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Post Hash Ban"; public string $url = "http://atravelinggeek.com/"; - public array $authors = ["ATravelingGeek" => "atg@atravelinggeek.com"]; + public array $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Ban images based on their hash"; public ?string $version = "0.1, October 21, 2007"; diff --git a/ext/image_hash_ban/main.php b/ext/image_hash_ban/main.php index 33fe41be..c50604cf 100644 --- a/ext/image_hash_ban/main.php +++ b/ext/image_hash_ban/main.php @@ -29,7 +29,7 @@ class HashBanTable extends Table $this->order_by = ["date DESC", "id"]; $this->create_url = make_link("image_hash_ban/add"); $this->delete_url = make_link("image_hash_ban/remove"); - $this->table_attrs = ["class" => "zebra form"]; + $this->table_attrs = ["class" => "zebra"]; } } @@ -62,7 +62,7 @@ class ImageBan extends Extension /** @var ImageBanTheme */ protected Themelet $theme; - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; if ($this->get_version("ext_imageban_version") < 1) { @@ -76,17 +76,17 @@ class ImageBan extends Extension } } - public function onDataUpload(DataUploadEvent $event): void + public function onDataUpload(DataUploadEvent $event) { global $database; - $row = $database->get_row("SELECT * FROM image_bans WHERE hash = :hash", ["hash" => $event->hash]); + $row = $database->get_row("SELECT * FROM image_bans WHERE hash = :hash", ["hash"=>$event->hash]); if ($row) { log_info("image_hash_ban", "Attempted to upload a blocked image ({$event->hash} - {$row['reason']})"); - throw new UploadException("Post {$row["hash"]} has been banned, reason: {$row["reason"]}"); + throw new UploadException("Post ".html_escape($row["hash"])." has been banned, reason: ".format_text($row["reason"])); } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $database, $page, $user; @@ -94,7 +94,7 @@ class ImageBan extends Extension if ($user->can(Permissions::BAN_IMAGE)) { if ($event->get_arg(0) == "add") { $user->ensure_authed(); - $input = validate_input(["c_hash" => "optional,string", "c_reason" => "string", "c_image_id" => "optional,int"]); + $input = validate_input(["c_hash"=>"optional,string", "c_reason"=>"string", "c_image_id"=>"optional,int"]); $image = isset($input['c_image_id']) ? Image::by_id($input['c_image_id']) : null; $hash = isset($input["c_hash"]) ? $input["c_hash"] : $image->hash; $reason = isset($input['c_reason']) ? $input['c_reason'] : "DNP"; @@ -113,7 +113,7 @@ class ImageBan extends Extension } } elseif ($event->get_arg(0) == "remove") { $user->ensure_authed(); - $input = validate_input(["d_hash" => "string"]); + $input = validate_input(["d_hash"=>"string"]); send_event(new RemoveImageHashBanEvent($input['d_hash'])); $page->flash("Post ban removed"); $page->set_mode(PageMode::REDIRECT); @@ -128,17 +128,17 @@ class ImageBan 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::BAN_IMAGE)) { $event->add_nav_link("image_bans", new Link('image_hash_ban/list/1'), "Post Bans", NavLink::is_active(["image_hash_ban"])); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::BAN_IMAGE)) { @@ -146,23 +146,23 @@ class ImageBan extends Extension } } - public function onAddImageHashBan(AddImageHashBanEvent $event): void + public function onAddImageHashBan(AddImageHashBanEvent $event) { global $database; $database->execute( "INSERT INTO image_bans (hash, reason, date) VALUES (:hash, :reason, now())", - ["hash" => $event->hash, "reason" => $event->reason] + ["hash"=>$event->hash, "reason"=>$event->reason] ); log_info("image_hash_ban", "Banned hash {$event->hash} because '{$event->reason}'"); } - public function onRemoveImageHashBan(RemoveImageHashBanEvent $event): void + public function onRemoveImageHashBan(RemoveImageHashBanEvent $event) { global $database; - $database->execute("DELETE FROM image_bans WHERE hash = :hash", ["hash" => $event->hash]); + $database->execute("DELETE FROM image_bans WHERE hash = :hash", ["hash"=>$event->hash]); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::BAN_IMAGE)) { diff --git a/ext/image_hash_ban/test.php b/ext/image_hash_ban/test.php index 9ad35fde..5c8e433f 100644 --- a/ext/image_hash_ban/test.php +++ b/ext/image_hash_ban/test.php @@ -8,14 +8,14 @@ class ImageBanTest extends ShimmiePHPUnitTestCase { private string $hash = "feb01bab5698a11dd87416724c7a89e3"; - public function testPages(): void + public function testPages() { $this->log_in_as_admin(); $page = $this->get_page("image_hash_ban/list"); $this->assertEquals(200, $page->code); } - public function testBan(): void + public function testBan() { $this->log_in_as_admin(); @@ -33,9 +33,12 @@ class ImageBanTest extends ShimmiePHPUnitTestCase $this->assertEquals(404, $page->code); // Can't repost - $this->assertException(UploadException::class, function () { + try { $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - }); + $this->fail(); + } catch (UploadException $e) { + $this->assertTrue(true); + } // Remove ban send_event(new RemoveImageHashBanEvent($this->hash)); @@ -46,7 +49,7 @@ class ImageBanTest extends ShimmiePHPUnitTestCase $this->assertEquals(200, $page->code); } - public function onNotSuccessfulTest(\Throwable $t): never + public function onNotSuccessfulTest(\Throwable $t): void { send_event(new RemoveImageHashBanEvent($this->hash)); parent::onNotSuccessfulTest($t); // TODO: Change the autogenerated stub diff --git a/ext/image_hash_ban/theme.php b/ext/image_hash_ban/theme.php index e0d5d2c1..723d197a 100644 --- a/ext/image_hash_ban/theme.php +++ b/ext/image_hash_ban/theme.php @@ -4,34 +4,32 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\{INPUT,emptyHTML}; +use function MicroHTML\INPUT; class ImageBanTheme extends Themelet { /* * Show all the bans */ - public function display_bans(Page $page, HTMLElement $table, HTMLElement $paginator): void + public function display_bans(Page $page, $table, $paginator): void { $page->set_title("Post Bans"); $page->set_heading("Post Bans"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Edit Post Bans", emptyHTML($table, $paginator))); + $page->add_block(new Block("Edit Post Bans", $table . $paginator)); } /* * Display a link to delete an image */ - public function get_buttons_html(Image $image): HTMLElement + public function get_buttons_html(Image $image): string { - return SHM_SIMPLE_FORM( + return (string)SHM_SIMPLE_FORM( "image_hash_ban/add", - INPUT(["type" => 'hidden', "name" => 'c_hash', "value" => $image->hash]), - INPUT(["type" => 'hidden', "name" => 'c_image_id', "value" => $image->id]), - INPUT(["type" => 'text', "name" => 'c_reason']), - INPUT(["type" => 'submit', "value" => 'Ban Hash and Delete Post']), + INPUT(["type"=>'hidden', "name"=>'c_hash', "value"=>$image->hash]), + INPUT(["type"=>'hidden', "name"=>'c_image_id', "value"=>$image->id]), + INPUT(["type"=>'text', "name"=>'c_reason']), + INPUT(["type"=>'submit', "value"=>'Ban Hash and Delete Post']), ); } } diff --git a/ext/image_view_counter/info.php b/ext/image_view_counter/info.php index 48558ac3..7a4a42f4 100644 --- a/ext/image_view_counter/info.php +++ b/ext/image_view_counter/info.php @@ -11,7 +11,7 @@ class ImageViewCounterInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Post View Counter"; public string $url = "http://www.drudexsoftware.com/"; - public array $authors = ["Drudex Software" => "support@drudexsoftware.com"]; + public array $authors = ["Drudex Software"=>"support@drudexsoftware.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Tracks & displays how many times a post is viewed"; public ?string $documentation = diff --git a/ext/image_view_counter/main.php b/ext/image_view_counter/main.php index c3534589..4b79d50e 100644 --- a/ext/image_view_counter/main.php +++ b/ext/image_view_counter/main.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\{TD,TH,TR}; - class ImageViewCounter extends Extension { /** @var ImageViewCounterTheme */ @@ -13,14 +11,14 @@ class ImageViewCounter extends Extension private int $view_interval = 3600; # allows views to be added each hour # Add Setup Block with options for view counter - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Post View Counter"); $sb->add_bool_option("image_viewcounter_adminonly", "Display view counter only to admin"); } # Adds view to database if needed - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $database, $user; @@ -60,21 +58,24 @@ class ImageViewCounter extends Extension ); } - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { global $user, $database; if ($user->can(Permissions::SEE_IMAGE_VIEW_COUNTS)) { - $view_count = (string)$database->get_one( + $view_count = (int)$database->get_one( "SELECT COUNT(*) FROM image_views WHERE image_id =:image_id", ["image_id" => $event->image->id] ); - $event->add_part(SHM_POST_INFO("Views", $view_count), 38); + $event->add_part( + "Views:$view_count", + 38 + ); } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database, $config; @@ -89,28 +90,31 @@ class ImageViewCounter extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $database; if ($event->page_matches("popular_images")) { - $popular_ids = $database->get_col(" + $sql = " SELECT image_id, count(*) AS total_views FROM image_views, images WHERE image_views.image_id = image_views.image_id AND image_views.image_id = images.id GROUP BY image_views.image_id ORDER BY total_views DESC - LIMIT 100 - "); - $images = Search::get_images($popular_ids); + "; + $result = $database->get_col($sql); + $images = []; + foreach ($result as $id) { + $images[] = Image::by_id(intval($id)); + } $this->theme->view_popular($images); } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "posts") { + if ($event->parent=="posts") { $event->add_nav_link("sort_by_visits", new Link('popular_images'), "Popular Posts"); } } diff --git a/ext/image_view_counter/test.php b/ext/image_view_counter/test.php deleted file mode 100644 index 61cf0100..00000000 --- a/ext/image_view_counter/test.php +++ /dev/null @@ -1,24 +0,0 @@ -post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - $this->log_in_as_admin(); - $this->get_page("post/view/$image_id"); - $this->assert_text("Views"); - } - - public function testPopular(): void - { - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - $this->get_page("post/view/$image_id"); - $this->get_page("popular_images"); - $this->assert_text("$image_id"); - } -} diff --git a/ext/image_view_counter/theme.php b/ext/image_view_counter/theme.php index 485b82b7..d6c07f34 100644 --- a/ext/image_view_counter/theme.php +++ b/ext/image_view_counter/theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class ImageViewCounterTheme extends Themelet { - /** - * @param Image[] $images - */ - public function view_popular(array $images): void + public function view_popular($images) { global $page, $config; $pop_images = ""; diff --git a/ext/index/events.php b/ext/index/events.php index ca94cfed..235cfc4e 100644 --- a/ext/index/events.php +++ b/ext/index/events.php @@ -21,10 +21,7 @@ class SearchTermParseEvent extends Event public array $tag_conditions = []; public ?string $order = null; - /** - * @param string[] $context - */ - public function __construct(int $id, string $term = null, array $context = []) + public function __construct(int $id, string $term=null, array $context=[]) { parent::__construct(); @@ -44,17 +41,17 @@ class SearchTermParseEvent extends Event $this->context = $context; } - public function add_querylet(Querylet $q): void + public function add_querylet(Querylet $q) { $this->add_img_condition(new ImgCondition($q, $this->positive)); } - public function add_img_condition(ImgCondition $c): void + public function add_img_condition(ImgCondition $c) { $this->img_conditions[] = $c; } - public function add_tag_condition(TagCondition $c): void + public function add_tag_condition(TagCondition $c) { $this->tag_conditions[] = $c; } @@ -66,13 +63,11 @@ class SearchTermParseException extends SCoreException class PostListBuildingEvent extends Event { - /** @var string[] */ public array $search_terms = []; - /** @var array */ public array $parts = []; /** - * @param string[] $search + * #param string[] $search */ public function __construct(array $search) { @@ -80,7 +75,7 @@ class PostListBuildingEvent extends Event $this->search_terms = $search; } - public function add_control(string $html, int $position = 50): void + public function add_control(string $html, int $position=50) { while (isset($this->parts[$position])) { $position++; diff --git a/ext/index/main.php b/ext/index/main.php index ed929800..a851fddf 100644 --- a/ext/index/main.php +++ b/ext/index/main.php @@ -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; - require_once "config.php"; require_once "events.php"; @@ -16,7 +12,7 @@ class Index extends Extension /** @var IndexTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int(IndexConfig::IMAGES, 24); @@ -24,13 +20,20 @@ class Index extends Extension $config->set_default_string(IndexConfig::ORDER, "id DESC"); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { - global $cache, $config, $page, $user; + global $cache, $page, $user; if ($event->page_matches("post/list")) { if (isset($_GET['search'])) { - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(search_link(Tag::explode($_GET['search'], false))); + // implode(explode()) to resolve aliases and sanitise + $search = url_escape(Tag::caret(Tag::implode(Tag::explode($_GET['search'], false)))); + if (empty($search)) { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/list/1")); + } else { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link('post/list/'.$search.'/1')); + } return; } @@ -71,24 +74,26 @@ class Index extends Extension return; } - $total_pages = (int)ceil(Search::count_images($search_terms) / $config->get_int(IndexConfig::IMAGES)); + $total_pages = Image::count_pages($search_terms); + $images = []; + if (SPEED_HAX && $total_pages > $fast_page_limit && !$user->can("big_search")) { $total_pages = $fast_page_limit; } - $images = null; if (SPEED_HAX) { if ($count_search_terms === 0 && ($page_number < 10)) { // extra caching for the first few post/list pages - $images = cache_get_or_set( - "post-list:$page_number", - fn () => Search::find_images(($page_number - 1) * $page_size, $page_size, $search_terms), - 60 - ); + $images = $cache->get("post-list:$page_number"); + if (is_null($images)) { + $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); + $cache->set("post-list:$page_number", $images, 60); + } } } - if (is_null($images)) { - $images = Search::find_images(($page_number - 1) * $page_size, $page_size, $search_terms); + + if (!$images) { + $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); } } catch (PermissionDeniedException $pde) { $this->theme->display_error(403, "Permission denied", $pde->error); @@ -120,7 +125,7 @@ class Index extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Index Options"); $sb->position = 20; @@ -130,41 +135,42 @@ class Index extends Extension $sb->add_label(" images on the post list"); } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("posts", new Link('post/list'), "Posts", NavLink::is_active(["post","view"]), 20); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "posts") { + if ($event->parent=="posts") { $event->add_nav_link("posts_all", new Link('post/list'), "All"); } } - 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("General", $this->theme->get_help_html()), 0); } } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('search') - ->addArgument('query', InputArgument::REQUIRED) - ->setDescription('Search the database and print results') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $query = Tag::explode($input->getArgument('query')); - $items = Search::find_images(limit: 1000, tags: $query); - foreach ($items as $item) { - $output->writeln($item->hash); - } - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + # TODO: --fields a,b,c + print "\tsearch \n"; + print "\t\tsearch the database and print results\n\n"; + } + if ($event->cmd == "search") { + $query = count($event->args) > 0 ? Tag::explode($event->args[0]) : []; + $items = Image::find_images(limit: 1000, tags: $query); + foreach ($items as $item) { + print("{$item->hash}\n"); + } + } } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -187,20 +193,13 @@ class Index extends Extension ); } elseif (preg_match("/^ratio([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+):(\d+)$/i", $event->term, $matches)) { $cmp = preg_replace('/^:/', '=', $matches[1]); - $args = ["width{$event->id}" => int_escape($matches[2]), "height{$event->id}" => int_escape($matches[3])]; + $args = ["width{$event->id}"=>int_escape($matches[2]), "height{$event->id}"=>int_escape($matches[3])]; $event->add_querylet(new Querylet("width / :width{$event->id} $cmp height / :height{$event->id}", $args)); - } elseif (preg_match("/^filesize([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+[kmg]?b?)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $val = parse_shorthand_int($matches[2]); - $event->add_querylet(new Querylet("images.filesize $cmp :val{$event->id}", ["val{$event->id}" => $val])); - } elseif (preg_match("/^id=([\d,]+)$/i", $event->term, $matches)) { - $val = array_map(fn ($x) => int_escape($x), explode(",", $matches[1])); - $set = implode(",", $val); - $event->add_querylet(new Querylet("images.id IN ($set)")); - } elseif (preg_match("/^id([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { - $cmp = ltrim($matches[1], ":") ?: "="; - $val = int_escape($matches[2]); - $event->add_querylet(new Querylet("images.id $cmp :val{$event->id}", ["val{$event->id}" => $val])); + } elseif (preg_match("/^(filesize|id)([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+[kmg]?b?)$/i", $event->term, $matches)) { + $col = $matches[1]; + $cmp = ltrim($matches[2], ":") ?: "="; + $val = parse_shorthand_int($matches[3]); + $event->add_querylet(new Querylet("images.$col $cmp :val{$event->id}", ["val{$event->id}"=>$val])); } elseif (preg_match("/^(hash|md5)[=|:]([0-9a-fA-F]*)$/i", $event->term, $matches)) { $hash = strtolower($matches[2]); $event->add_querylet(new Querylet('images.hash = :hash', ["hash" => $hash])); @@ -209,7 +208,7 @@ class Index extends Extension $event->add_querylet(new Querylet('images.phash = :phash', ["phash" => $phash])); } elseif (preg_match("/^(filename|name)[=|:](.+)$/i", $event->term, $matches)) { $filename = strtolower($matches[2]); - $event->add_querylet(new Querylet("lower(images.filename) LIKE :filename{$event->id}", ["filename{$event->id}" => "%$filename%"])); + $event->add_querylet(new Querylet("lower(images.filename) LIKE :filename{$event->id}", ["filename{$event->id}"=>"%$filename%"])); } elseif (preg_match("/^(source)[=|:](.*)$/i", $event->term, $matches)) { $source = strtolower($matches[2]); @@ -217,27 +216,27 @@ class Index extends Extension $not = ($source == "any" ? "NOT" : ""); $event->add_querylet(new Querylet("images.source IS $not NULL")); } else { - $event->add_querylet(new Querylet('images.source LIKE :src', ["src" => "%$source%"])); + $event->add_querylet(new Querylet('images.source LIKE :src', ["src"=>"%$source%"])); } } elseif (preg_match("/^posted([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])([0-9-]*)$/i", $event->term, $matches)) { // TODO Make this able to search = without needing a time component. $cmp = ltrim($matches[1], ":") ?: "="; $val = $matches[2]; - $event->add_querylet(new Querylet("images.posted $cmp :posted{$event->id}", ["posted{$event->id}" => $val])); + $event->add_querylet(new Querylet("images.posted $cmp :posted{$event->id}", ["posted{$event->id}"=>$val])); } elseif (preg_match("/^size([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)x(\d+)$/i", $event->term, $matches)) { $cmp = ltrim($matches[1], ":") ?: "="; - $args = ["width{$event->id}" => int_escape($matches[2]), "height{$event->id}" => int_escape($matches[3])]; + $args = ["width{$event->id}"=>int_escape($matches[2]), "height{$event->id}"=>int_escape($matches[3])]; $event->add_querylet(new Querylet("width $cmp :width{$event->id} AND height $cmp :height{$event->id}", $args)); } elseif (preg_match("/^width([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { $cmp = ltrim($matches[1], ":") ?: "="; - $event->add_querylet(new Querylet("width $cmp :width{$event->id}", ["width{$event->id}" => int_escape($matches[2])])); + $event->add_querylet(new Querylet("width $cmp :width{$event->id}", ["width{$event->id}"=>int_escape($matches[2])])); } elseif (preg_match("/^height([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)$/i", $event->term, $matches)) { $cmp = ltrim($matches[1], ":") ?: "="; - $event->add_querylet(new Querylet("height $cmp :height{$event->id}", ["height{$event->id}" => int_escape($matches[2])])); + $event->add_querylet(new Querylet("height $cmp :height{$event->id}", ["height{$event->id}"=>int_escape($matches[2])])); } elseif (preg_match("/^length([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(.+)$/i", $event->term, $matches)) { $value = parse_to_milliseconds($matches[2]); $cmp = ltrim($matches[1], ":") ?: "="; - $event->add_querylet(new Querylet("length $cmp :length{$event->id}", ["length{$event->id}" => $value])); + $event->add_querylet(new Querylet("length $cmp :length{$event->id}", ["length{$event->id}"=>$value])); } elseif (preg_match("/^order[=|:](id|width|height|length|filesize|filename)[_]?(desc|asc)?$/i", $event->term, $matches)) { $ord = strtolower($matches[1]); $default_order_for_column = preg_match("/^(id|filename)$/", $matches[1]) ? "ASC" : "DESC"; diff --git a/ext/index/script.js b/ext/index/script.js index 1ee59f9f..0f17ce93 100644 --- a/ext/index/script.js +++ b/ext/index/script.js @@ -1,15 +1,23 @@ /*jshint bitwise:false, curly:true, eqeqeq:true, evil:true, forin:false, noarg:true, noempty:true, nonew:true, undef:false, strict:false, browser:true, jquery:true */ document.addEventListener('DOMContentLoaded', () => { - let blocked_tags = (shm_cookie_get("ui-blocked-tags") || "").split(" "); - let blocked_css = blocked_tags - .filter(tag => tag.length > 0) - .map(tag => tag.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")) - .map(tag => `.shm-thumb[data-tags~="${tag}"]`).join(", "); - if(blocked_css) { - let style = document.createElement("style"); - style.innerHTML = blocked_css + " { display: none; }"; - document.head.appendChild(style); + var blocked_tags = (Cookies.get("ui-blocked-tags") || "").split(" "); + var needs_refresh = false; + for(var i=0; i { * This allows us to cache the same thumb for all query * strings, adding the query in the browser. */ - document.querySelectorAll(".shm-image-list").forEach(function(list) { - var query = list.getAttribute("data-query"); + $(".shm-image-list").each(function(idx, elm) { + var query = $(this).data("query"); if(query) { - list.querySelectorAll(".shm-thumb-link").forEach(function(thumb) { - thumb.setAttribute("href", thumb.getAttribute("href") + query); + $(this).find(".shm-thumb-link").each(function(idx2, elm2) { + $(this).attr("href", $(this).attr("href") + query); }); } }); }); function select_blocked_tags() { - var blocked_tags = prompt("Enter tags to ignore", shm_cookie_get("ui-blocked-tags") || "AI-generated"); + var blocked_tags = prompt("Enter tags to ignore", Cookies.get("ui-blocked-tags") || "AI-generated"); if(blocked_tags !== null) { - shm_cookie_set("ui-blocked-tags", blocked_tags.toLowerCase()); + Cookies.set("ui-blocked-tags", blocked_tags.toLowerCase(), {expires: 365}); location.reload(true); } } diff --git a/ext/index/style.css b/ext/index/style.css index 3bebb4bf..5ecd5a28 100644 --- a/ext/index/style.css +++ b/ext/index/style.css @@ -1,3 +1,5 @@ + +/*noinspection CssRedundantUnit*/ #image-list .blockbody { background: none; border: none; @@ -7,11 +9,3 @@ text-align: left; margin: 0 10px 10px 0; } -.shm-image-list { - display: grid; - grid-template-columns: repeat( auto-fill, calc(var(--thumb-width) + 42px) ); - place-items: center; -} -.shm-image-list .thumb { - margin-bottom: 8px; -} diff --git a/ext/index/test.php b/ext/index/test.php index f5542a2a..6f21fa62 100644 --- a/ext/index/test.php +++ b/ext/index/test.php @@ -4,11 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -use PHPUnit\Framework\Attributes\Depends; - class IndexTest extends ShimmiePHPUnitTestCase { - public function testIndexPage(): void + public function testIndexPage() { $this->get_page('post/list'); $this->assert_title("Welcome to Shimmie"); @@ -49,9 +47,180 @@ class IndexTest extends ShimmiePHPUnitTestCase $this->assert_response(200); } + public function testWeirdTags() + { + $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]); + } + + // base case + 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]; + } + + /* * * * * * * * * * * + * Tag Search * + * * * * * * * * * * */ + /** @depends testUpload */ + public function testTagSearchNoResults($image_ids) + { + $this->testUpload(); + $this->assert_search_results(["maumaumau"], []); + } + + /** @depends testUpload */ + public function testTagSearchOneResult($image_ids) + { + $image_ids = $this->testUpload(); + $this->assert_search_results(["pbx"], [$image_ids[0]]); + } + + /** @depends testUpload */ + public function testTagSearchManyResults($image_ids) + { + $image_ids = $this->testUpload(); + $this->assert_search_results(["computer"], [$image_ids[1], $image_ids[0]]); + } + + /* * * * * * * * * * * + * Multi-Tag Search * + * * * * * * * * * * */ + /** @depends testUpload */ + public function testMultiTagSearchNoResults($image_ids) + { + $this->testUpload(); + # multiple tags, one of which doesn't exist + # (test the "one tag doesn't exist = no hits" path) + $this->assert_search_results(["computer", "asdfasdfwaffle"], []); + } + + /** @depends testUpload */ + public function testMultiTagSearchOneResult($image_ids) + { + $image_ids = $this->testUpload(); + $this->assert_search_results(["computer", "screenshot"], [$image_ids[0]]); + } + + /** @depends testUpload */ + public function testMultiTagSearchManyResults($image_ids) + { + $image_ids = $this->testUpload(); + $this->assert_search_results(["computer", "thing"], [$image_ids[1], $image_ids[0]]); + } + + /* * * * * * * * * * * + * Meta Search * + * * * * * * * * * * */ + /** @depends testUpload */ + public function testMetaSearchNoResults($image_ids) + { + $this->testUpload(); + $this->assert_search_results(["hash=1234567890"], []); + $this->assert_search_results(["ratio=42:12345"], []); + } + + /** @depends testUpload */ + public function testMetaSearchOneResult($image_ids) + { + $image_ids = $this->testUpload(); + $this->assert_search_results(["hash=feb01bab5698a11dd87416724c7a89e3"], [$image_ids[0]]); + $this->assert_search_results(["md5=feb01bab5698a11dd87416724c7a89e3"], [$image_ids[0]]); + $this->assert_search_results(["id={$image_ids[1]}"], [$image_ids[1]]); + $this->assert_search_results(["filename=screenshot"], [$image_ids[0]]); + } + + /** @depends testUpload */ + public function testMetaSearchManyResults($image_ids) + { + $image_ids = $this->testUpload(); + $this->assert_search_results(["size=640x480"], [$image_ids[1], $image_ids[0]]); + $this->assert_search_results(["tags=5"], [$image_ids[1], $image_ids[0]]); + $this->assert_search_results(["ext=jpg"], [$image_ids[1], $image_ids[0]]); + } + + /* * * * * * * * * * * + * Wildcards * + * * * * * * * * * * */ + /** @depends testUpload */ + public function testWildSearchNoResults($image_ids) + { + $this->testUpload(); + $this->assert_search_results(["asdfasdf*"], []); + } + + /** @depends testUpload */ + public function testWildSearchOneResult($image_ids) + { + $image_ids = $this->testUpload(); + // Only the first image matches both the wildcard and the tag. + // This checks for https://github.com/shish/shimmie2/issues/547 + $this->assert_search_results(["comp*", "screenshot"], [$image_ids[0]]); + } + + /** @depends testUpload */ + public function testWildSearchManyResults($image_ids) + { + $image_ids = $this->testUpload(); + // two images match comp* - one matches it once, + // one matches it twice + $this->assert_search_results(["comp*"], [$image_ids[1], $image_ids[0]]); + } + + /* * * * * * * * * * * + * Mixed * + * * * * * * * * * * */ + /** @depends testUpload */ + public function testMixedSearchTagMeta($image_ids) + { + $image_ids = $this->testUpload(); + // multiple tags, many results + $this->assert_search_results(["computer", "size=640x480"], [$image_ids[1], $image_ids[0]]); + } + // tag + negative + // wildcards + ??? + + /* * * * * * * * * * * + * Negative * + * * * * * * * * * * */ + /** @depends testUpload */ + public function testNegative($image_ids) + { + $image_ids = $this->testUpload(); + + // negative tag, should have one result + $this->assert_search_results(["computer", "-pbx"], [$image_ids[1]]); + + // negative tag alone, should work + $this->assert_search_results(["-pbx"], [$image_ids[1]]); + + // negative that doesn't exist + $this->assert_search_results(["-not_a_tag"], [$image_ids[1], $image_ids[0]]); + + // multiple negative tags that don't exist + $this->assert_search_results(["-not_a_tag", "-also_not_a_tag"], [$image_ids[1], $image_ids[0]]); + } + // This isn't really an index thing, we just want to test this from // SOMEWHERE because the default theme doesn't use them. - public function test_nav(): void + public function test_nav() { send_event(new UserLoginEvent(User::by_name(self::$user_name))); send_event(new PageNavBuildingEvent()); diff --git a/ext/index/theme.php b/ext/index/theme.php index 94761dda..f6bd16dc 100644 --- a/ext/index/theme.php +++ b/ext/index/theme.php @@ -13,20 +13,16 @@ class IndexTheme extends Themelet { protected int $page_number; protected int $total_pages; - /** @var string[] */ protected array $search_terms; - /** - * @param string[] $search_terms - */ - public function set_page(int $page_number, int $total_pages, array $search_terms): void + public function set_page(int $page_number, int $total_pages, array $search_terms) { $this->page_number = $page_number; $this->total_pages = $total_pages; $this->search_terms = $search_terms; } - public function display_intro(Page $page): void + public function display_intro(Page $page) { $text = "

    @@ -46,9 +42,9 @@ and of course start organising your images :-) } /** - * @param Image[] $images + * #param Image[] $images */ - public function display_page(Page $page, array $images): void + public function display_page(Page $page, array $images) { $this->display_shortwiki($page); @@ -65,9 +61,9 @@ and of course start organising your images :-) } /** - * @param string[] $parts + * #param string[] $parts */ - public function display_admin_block(array $parts): void + public function display_admin_block(array $parts) { global $page; $page->add_block(new Block("List Controls", join("
    ", $parts), "left", 50)); @@ -75,23 +71,27 @@ and of course start organising your images :-) /** - * @param string[] $search_terms + * #param string[] $search_terms */ protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string { $prev = $page_number - 1; $next = $page_number + 1; - $h_prev = ($page_number <= 1) ? "Prev" : 'Prev'; + $u_tags = url_escape(Tag::implode($search_terms)); + $query = empty($u_tags) ? "" : '/'.$u_tags; + + + $h_prev = ($page_number <= 1) ? "Prev" : 'Prev'; $h_index = "Index"; - $h_next = ($page_number >= $total_pages) ? "Next" : 'Next'; + $h_next = ($page_number >= $total_pages) ? "Next" : 'Next'; $h_search_string = html_escape(Tag::implode($search_terms)); - $h_search_link = search_link(); + $h_search_link = make_link("post/list"); $h_search = "

    - - + +
    "; @@ -100,7 +100,7 @@ and of course start organising your images :-) } /** - * @param Image[] $images + * #param Image[] $images */ protected function build_table(array $images, ?string $query): string { @@ -113,11 +113,11 @@ and of course start organising your images :-) return $table; } - protected function display_shortwiki(Page $page): void + protected function display_shortwiki(Page $page) { global $config; - if (Extension::is_enabled(WikiInfo::KEY) && $config->get_bool(WikiConfig::TAG_SHORTWIKIS)) { + if (class_exists('Shimmie2\Wiki') && $config->get_bool(WikiConfig::TAG_SHORTWIKIS)) { if (count($this->search_terms) == 1) { $st = Tag::implode($this->search_terms); @@ -131,7 +131,7 @@ and of course start organising your images :-) $short_wiki_description = $tfe->formatted; } $wikiLink = make_link("wiki/$st"); - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $tagcategories = new TagCategories(); $tag_category_dict = $tagcategories->getKeyedDict(); $st = $tagcategories->getTagHtml(html_escape($st), $tag_category_dict); @@ -143,9 +143,9 @@ and of course start organising your images :-) } /** - * @param Image[] $images + * #param Image[] $images */ - protected function display_page_header(Page $page, array $images): void + protected function display_page_header(Page $page, array $images) { global $config; @@ -169,16 +169,16 @@ and of course start organising your images :-) } /** - * @param Image[] $images + * #param Image[] $images */ - protected function display_page_images(Page $page, array $images): void + protected function display_page_images(Page $page, array $images) { if (count($this->search_terms) > 0) { if ($this->page_number > 3) { // only index the first pages of each term $page->add_html_header(''); } - $query = url_escape(Tag::implode($this->search_terms)); + $query = url_escape(Tag::caret(Tag::implode($this->search_terms))); $page->add_block(new Block("Posts", $this->build_table($images, "#search=$query"), "main", 10, "image-list")); $this->display_paginator($page, "post/list/$query", null, $this->page_number, $this->total_pages, true); } else { diff --git a/ext/ipban/main.php b/ext/ipban/main.php index 2978fb3b..ce97c6af 100644 --- a/ext/ipban/main.php +++ b/ext/ipban/main.php @@ -29,10 +29,10 @@ class IPBanTable extends Table $this->set_columns([ new InetColumn("ip", "IP"), new EnumColumn("mode", "Mode", [ - "Block" => "block", - "Firewall" => "firewall", - "Ghost" => "ghost", - "Anon Ghost" => "anon-ghost" + "Block"=>"block", + "Firewall"=>"firewall", + "Ghost"=>"ghost", + "Anon Ghost"=>"anon-ghost" ]), new TextColumn("reason", "Reason"), new StringColumn("banner", "Banner"), @@ -46,7 +46,7 @@ class IPBanTable extends Table ]; $this->create_url = make_link("ip_ban/create"); $this->delete_url = make_link("ip_ban/delete"); - $this->table_attrs = ["class" => "zebra form"]; + $this->table_attrs = ["class" => "zebra"]; } } @@ -88,7 +88,7 @@ class IPBan extends Extension return 10; } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string( @@ -100,9 +100,9 @@ class IPBan extends Extension ); } - public function onUserLogin(UserLoginEvent $event): void + public function onUserLogin(UserLoginEvent $event) { - global $cache, $config, $database, $page; + global $cache, $config, $database, $page, $_shm_user_classes; // Get lists of banned IPs and banned networks $ips = $cache->get("ip_bans"); @@ -130,19 +130,19 @@ class IPBan extends Extension // Check if our current IP is in either of the ban lists $active_ban_id = ( - $this->find_active_ban(get_real_ip(), $ips, $networks) + $this->find_active_ban($ips, get_real_ip(), $networks) ); // If an active ban is found, act on it if (!is_null($active_ban_id)) { - $row = $database->get_row("SELECT * FROM bans WHERE id=:id", ["id" => $active_ban_id]); + $row = $database->get_row("SELECT * FROM bans WHERE id=:id", ["id"=>$active_ban_id]); if (empty($row)) { return; } $row_banner_id_int = intval($row['banner_id']); - $msg = $config->get_string("ipban_message_{$row['mode']}") ?? $config->get_string("ipban_message") ?? "(no message)"; + $msg = $config->get_string("ipban_message_{$row['mode']}") ?? $config->get_string("ipban_message"); $msg = str_replace('$IP', $row["ip"], $msg); $msg = str_replace('$DATE', $row['expires'] ?? 'the end of time', $msg); $msg = str_replace('$ADMIN', User::by_id($row_banner_id_int)->name, $msg); @@ -153,22 +153,21 @@ class IPBan extends Extension } else { $msg = str_replace('$CONTACT', "", $msg); } - assert(is_string($msg)); $msg .= ""; if ($row["mode"] == "ghost") { $b = new Block(null, $msg, "main", 0); $b->is_content = false; $page->add_block($b); - $page->add_cookie("nocache", "Ghost Banned", time() + 60 * 60 * 2, "/"); - $event->user->class = UserClass::$known_classes["ghost"]; + $page->add_cookie("nocache", "Ghost Banned", time()+60*60*2, "/"); + $event->user->class = $_shm_user_classes["ghost"]; } elseif ($row["mode"] == "anon-ghost") { if ($event->user->is_anonymous()) { $b = new Block(null, $msg, "main", 0); $b->is_content = false; $page->add_block($b); - $page->add_cookie("nocache", "Ghost Banned", time() + 60 * 60 * 2, "/"); - $event->user->class = UserClass::$known_classes["ghost"]; + $page->add_cookie("nocache", "Ghost Banned", time()+60*60*2, "/"); + $event->user->class = $_shm_user_classes["ghost"]; } } else { header("HTTP/1.1 403 Forbidden"); @@ -178,21 +177,21 @@ class IPBan extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { if ($event->page_matches("ip_ban")) { global $database, $page, $user; if ($user->can(Permissions::BAN_IP)) { if ($event->get_arg(0) == "create") { $user->ensure_authed(); - $input = validate_input(["c_ip" => "string", "c_mode" => "string", "c_reason" => "string", "c_expires" => "optional,date"]); + $input = validate_input(["c_ip"=>"string", "c_mode"=>"string", "c_reason"=>"string", "c_expires"=>"optional,date"]); send_event(new AddIPBanEvent($input['c_ip'], $input['c_mode'], $input['c_reason'], $input['c_expires'])); $page->flash("Ban for {$input['c_ip']} added"); $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("ip_ban/list")); } elseif ($event->get_arg(0) == "delete") { $user->ensure_authed(); - $input = validate_input(["d_id" => "int"]); + $input = validate_input(["d_id"=>"int"]); send_event(new RemoveIPBanEvent($input['d_id'])); $page->flash("Ban removed"); $page->set_mode(PageMode::REDIRECT); @@ -211,7 +210,7 @@ class IPBan extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { global $config; @@ -225,17 +224,17 @@ class IPBan 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::BAN_IP)) { $event->add_nav_link("ip_bans", new Link('ip_ban/list'), "IP Bans", NavLink::is_active(["ip_ban"])); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::BAN_IP)) { @@ -243,29 +242,29 @@ class IPBan extends Extension } } - public function onAddIPBan(AddIPBanEvent $event): void + public function onAddIPBan(AddIPBanEvent $event) { global $cache, $user, $database; $sql = "INSERT INTO bans (ip, mode, reason, expires, banner_id) VALUES (:ip, :mode, :reason, :expires, :admin_id)"; - $database->execute($sql, ["ip" => $event->ip, "mode" => $event->mode, "reason" => $event->reason, "expires" => $event->expires, "admin_id" => $user->id]); + $database->execute($sql, ["ip"=>$event->ip, "mode"=>$event->mode, "reason"=>$event->reason, "expires"=>$event->expires, "admin_id"=>$user->id]); $cache->delete("ip_bans"); $cache->delete("network_bans"); log_info("ipban", "Banned ({$event->mode}) {$event->ip} because '{$event->reason}' until {$event->expires}"); } - public function onRemoveIPBan(RemoveIPBanEvent $event): void + public function onRemoveIPBan(RemoveIPBanEvent $event) { global $cache, $database; - $ban = $database->get_row("SELECT * FROM bans WHERE id = :id", ["id" => $event->id]); + $ban = $database->get_row("SELECT * FROM bans WHERE id = :id", ["id"=>$event->id]); if ($ban) { - $database->execute("DELETE FROM bans WHERE id = :id", ["id" => $event->id]); + $database->execute("DELETE FROM bans WHERE id = :id", ["id"=>$event->id]); $cache->delete("ip_bans"); $cache->delete("network_bans"); log_info("ipban", "Removed {$ban['ip']}'s ban"); } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -356,12 +355,11 @@ class IPBan extends Extension } } - /** - * @param array $ips - * @param array $networks - */ - public function find_active_ban(string $remote, array $ips, array $networks): ?int + public function find_active_ban($ips, $remote, $networks) { + if (!$remote) { + return null; + } $active_ban_id = null; if (isset($ips[$remote])) { $active_ban_id = $ips[$remote]; diff --git a/ext/ipban/test.php b/ext/ipban/test.php index 4be3e31d..4dbb9f1e 100644 --- a/ext/ipban/test.php +++ b/ext/ipban/test.php @@ -8,14 +8,14 @@ class IPBanTest extends ShimmiePHPUnitTestCase { # FIXME: test that the IP is actually banned - public function testAccess(): void + public function testAccess() { $page = $this->get_page('ip_ban/list'); $this->assertEquals(403, $page->code); $this->assertEquals("Permission Denied", $page->title); } - public function testIPBan(): void + public function testIPBan() { global $database; @@ -52,11 +52,11 @@ class IPBanTest extends ShimmiePHPUnitTestCase ); } - public function test_all(): void + public function test_all() { // just test it doesn't crash for now $this->log_in_as_admin(); - $page = $this->get_page('ip_ban/list', ['r_all' => 'on']); + $page = $this->get_page('ip_ban/list', ['r_all'=>'on']); $this->assertEquals(200, $page->code); } } diff --git a/ext/ipban/theme.php b/ext/ipban/theme.php index 16f7dd9a..996b40a7 100644 --- a/ext/ipban/theme.php +++ b/ext/ipban/theme.php @@ -4,13 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\emptyHTML; - class IPBanTheme extends Themelet { - public function display_bans(Page $page, HTMLElement $table, HTMLElement $paginator): void + public function display_bans(Page $page, $table, $paginator) { $html = " Show All Active / diff --git a/ext/link_image/info.php b/ext/link_image/info.php index c735b524..c0cedfe5 100644 --- a/ext/link_image/info.php +++ b/ext/link_image/info.php @@ -10,7 +10,7 @@ class LinkImageInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Link to Post"; - public array $authors = ["Artanis" => "artanis.00@gmail.com"]; + public array $authors = ["Artanis"=>"artanis.00@gmail.com"]; public string $description = "Show various forms of link to each image, for copy & paste"; public string $license = self::LICENSE_GPLV2; public ?string $documentation = "There is one option in Board Config: Text Link Format. diff --git a/ext/link_image/main.php b/ext/link_image/main.php index 297f62ff..cb56b787 100644 --- a/ext/link_image/main.php +++ b/ext/link_image/main.php @@ -9,27 +9,24 @@ class LinkImage extends Extension /** @var LinkImageTheme */ protected Themelet $theme; - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $page; $this->theme->links_block($page, $this->data($event->image)); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Link to Post"); $sb->add_text_option("ext_link-img_text-link_format", "Text Link Format: "); } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string("ext_link-img_text-link_format", '$title - $id ($ext $size $filesize)'); } - /** - * @return array{thumb_src: string, image_src: string, post_link: string, text_link: string|null} - */ private function data(Image $image): array { global $config; @@ -41,7 +38,6 @@ class LinkImage extends Extension 'thumb_src' => make_http($image->get_thumb_link()), 'image_src' => make_http($image->get_image_link()), 'post_link' => make_http(make_link("post/view/{$image->id}")), - 'text_link' => $text_link - ]; + 'text_link' => $text_link]; } } diff --git a/ext/link_image/test.php b/ext/link_image/test.php index cf4df5ae..5681bf12 100644 --- a/ext/link_image/test.php +++ b/ext/link_image/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class LinkImageTest extends ShimmiePHPUnitTestCase { - public function testLinkImage(): void + public function testLinkImage() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pie"); diff --git a/ext/link_image/theme.php b/ext/link_image/theme.php index 1305df4e..fe4a7b68 100644 --- a/ext/link_image/theme.php +++ b/ext/link_image/theme.php @@ -6,16 +6,14 @@ namespace Shimmie2; class LinkImageTheme extends Themelet { - /** - * @param array{thumb_src:string,image_src:string,post_link:string,text_link:string|null} $data - */ - public function links_block(Page $page, array $data): void + public function links_block(Page $page, $data) { $thumb_src = $data['thumb_src']; $image_src = $data['image_src']; $post_link = $data['post_link']; $text_link = $data['text_link']; + $page->add_block(new Block( "Link to Post", " @@ -64,7 +62,7 @@ class LinkImageTheme extends Themelet protected function url(string $url, string $content, string $type): string { if (empty($content)) { - $content = $url; + $content=$url; } switch ($type) { @@ -95,7 +93,7 @@ class LinkImageTheme extends Themelet return $text; } - protected function link_code(string $label, string $content, string $id): string + protected function link_code(string $label, string $content, $id=null): string { return " diff --git a/ext/link_scan/info.php b/ext/link_scan/info.php deleted file mode 100644 index 8d828ef1..00000000 --- a/ext/link_scan/info.php +++ /dev/null @@ -1,28 +0,0 @@ -By default scan-for-URLs mode will be activated if somebody - searches for text which includes http:// - or https:// - but you can change this by setting - link_scan_trigger in the config table, eg to - https?://www.mysite.com/ - "; -} diff --git a/ext/link_scan/main.php b/ext/link_scan/main.php deleted file mode 100644 index 9084f796..00000000 --- a/ext/link_scan/main.php +++ /dev/null @@ -1,53 +0,0 @@ -page_matches("post/list") && !empty($search)) { - $trigger = $config->get_string("link_scan_trigger", "https?://"); - if (preg_match("#.*{$trigger}.*#", $search)) { - $ids = $this->scan($search); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(search_link(["id=".implode(",", $ids)])); - $event->stop_processing = true; - } - } - } - - /** - * @return int[] - */ - private function scan(string $text): array - { - $ids = []; - $matches = []; - preg_match_all("/post\/view\/(\d+)/", $text, $matches); - foreach($matches[1] as $match) { - $img = Image::by_id((int)$match); - if ($img) { - $ids[] = $img->id; - } - } - preg_match_all("/\b([0-9a-fA-F]{32})\b/", $text, $matches); - foreach($matches[1] as $match) { - $img = Image::by_hash($match); - if ($img) { - $ids[] = $img->id; - } - } - return array_unique($ids); - } -} diff --git a/ext/link_scan/test.php b/ext/link_scan/test.php deleted file mode 100644 index 9fe8a785..00000000 --- a/ext/link_scan/test.php +++ /dev/null @@ -1,56 +0,0 @@ -post_image("tests/pbx_screenshot.jpg", "TeStCase"); - $image_id_2 = $this->post_image("tests/favicon.png", "TeStCase"); - - $text = " - Look at http://example.com/post/view/{$image_id_1} there is an image - - http://example.com/post/view/{$image_id_2} is another one - - But there is no http://example.com/post/view/65432 - "; - $page = $this->get_page("post/list", ["search" => $text]); - - $this->assertEquals(PageMode::REDIRECT, $page->mode); - $this->assertEquals("/test/post/list/id%3D{$image_id_1}%2C{$image_id_2}/1", $page->redirect); - } - - public function testScanPostHash(): void - { - global $page; - $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase"); - $image_id_2 = $this->post_image("tests/favicon.png", "TeStCase"); - - $text = " - Look at http://example.com/_images/feb01bab5698a11dd87416724c7a89e3/foobar.jpg - there is an image or search for e106ea2983e1b77f11e00c0c54e53805 but one that - doesn't exist is e106ea2983e1b77f11e00c0c54e50000 o.o"; - $page = $this->get_page("post/list", ["search" => $text]); - - $this->assertEquals(PageMode::REDIRECT, $page->mode); - $this->assertEquals("/test/post/list/id%3D{$image_id_1}%2C{$image_id_2}/1", $page->redirect); - } - - public function testNotTriggered(): void - { - global $page; - $this->post_image("tests/pbx_screenshot.jpg", "TeStCase"); - $this->post_image("tests/favicon.png", "TeStCase"); - - $text = "Look at feb01bab5698a11dd87416724c7a89e3/foobar.jpg"; - $page = $this->get_page("post/list", ["search" => $text]); - - $this->assertEquals(PageMode::REDIRECT, $page->mode); - $this->assertEquals("/test/post/list/at%20feb01bab5698a11dd87416724c7a89e3%2Ffoobar.jpg%20Look/1", $page->redirect); - } -} diff --git a/ext/livefeed/main.php b/ext/livefeed/main.php index 3b05ee3a..acaff4b3 100644 --- a/ext/livefeed/main.php +++ b/ext/livefeed/main.php @@ -6,18 +6,18 @@ namespace Shimmie2; class LiveFeed extends Extension { - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Live Feed"); $sb->add_text_option("livefeed_host", "IP:port to send events to: "); } - public function onUserCreation(UserCreationEvent $event): void + public function onUserCreation(UserCreationEvent $event) { $this->msg("New user created: {$event->username}"); } - public function onImageAddition(ImageAdditionEvent $event): void + public function onImageAddition(ImageAdditionEvent $event) { global $user; $this->msg( @@ -26,15 +26,15 @@ class LiveFeed extends Extension ); } - public function onTagSet(TagSetEvent $event): void + public function onTagSet(TagSetEvent $event) { $this->msg( make_http(make_link("post/view/".$event->image->id))." - ". - "tags set to: ".Tag::implode($event->new_tags) + "tags set to: ".Tag::implode($event->tags) ); } - public function onCommentPosting(CommentPostingEvent $event): void + public function onCommentPosting(CommentPostingEvent $event) { global $user; $this->msg( @@ -48,7 +48,7 @@ class LiveFeed extends Extension return 99; } - private function msg(string $data): void + private function msg(string $data) { global $config; @@ -63,7 +63,7 @@ class LiveFeed extends Extension $host = $parts[0]; $port = (int)$parts[1]; $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (!$fp) { + if (! $fp) { return; } fwrite($fp, "$data\n"); diff --git a/ext/log_console/main.php b/ext/log_console/main.php index 09aca5b5..abfbe0bd 100644 --- a/ext/log_console/main.php +++ b/ext/log_console/main.php @@ -6,7 +6,7 @@ namespace Shimmie2; class LogConsole extends Extension { - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool("log_console_access", true); @@ -14,18 +14,19 @@ class LogConsole extends Extension $config->set_default_int("log_console_level", SCORE_LOG_INFO); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page; if ( $config->get_bool("log_console_access") && + isset($_SERVER['REQUEST_METHOD']) && isset($_SERVER['REQUEST_URI']) ) { $this->log(new LogEvent( "access", SCORE_LOG_INFO, - "{$event->method} {$_SERVER['REQUEST_URI']}" + "{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}" )); } @@ -40,7 +41,7 @@ class LogConsole extends Extension */ } - public function onLog(LogEvent $event): void + public function onLog(LogEvent $event) { global $config; if ($event->priority >= $config->get_int("log_console_level")) { @@ -48,7 +49,7 @@ class LogConsole extends Extension } } - private function log(LogEvent $event): void + private function log(LogEvent $event) { global $config, $user; // TODO: colour based on event->priority diff --git a/ext/log_db/main.php b/ext/log_db/main.php index 1873465a..fc7cea05 100644 --- a/ext/log_db/main.php +++ b/ext/log_db/main.php @@ -22,19 +22,19 @@ use function MicroHTML\rawHTML; class ShortDateTimeColumn extends DateTimeColumn { - public function read_input(array $inputs): HTMLElement + public function read_input(array $inputs) { return emptyHTML( INPUT([ - "type" => "date", - "name" => "r_{$this->name}[]", - "value" => @$inputs["r_{$this->name}"][0] + "type"=>"date", + "name"=>"r_{$this->name}[]", + "value"=>@$inputs["r_{$this->name}"][0] ]), BR(), INPUT([ - "type" => "date", - "name" => "r_{$this->name}[]", - "value" => @$inputs["r_{$this->name}"][1] + "type"=>"date", + "name"=>"r_{$this->name}[]", + "value"=>@$inputs["r_{$this->name}"][1] ]) ); } @@ -42,7 +42,7 @@ class ShortDateTimeColumn extends DateTimeColumn class ActorColumn extends Column { - public function __construct(string $name, string $title) + public function __construct($name, $title) { parent::__construct($name, $title); $this->sortable = false; @@ -59,7 +59,7 @@ class ActorColumn extends Column } } - public function read_input(array $inputs): HTMLElement + public function read_input($inputs) { return emptyHTML( INPUT([ @@ -78,12 +78,8 @@ class ActorColumn extends Column ); } - /** - * @return array{0: string|null, 1: string|null} - */ - public function modify_input_for_read(string|array $input): array + public function modify_input_for_read($input): array { - assert(is_array($input)); list($un, $ip) = $input; if (empty($un)) { $un = null; @@ -94,14 +90,11 @@ class ActorColumn extends Column return [$un, $ip]; } - /** - * @param array{username: string, address: string} $row - */ - public function display(array $row): HTMLElement + public function display($row): HTMLElement { $ret = emptyHTML(); if ($row['username'] != "Anonymous") { - $ret->appendChild(A(["href" => make_link("user/{$row['username']}"), "title" => $row['address']], $row['username'])); + $ret->appendChild(A(["href"=>make_link("user/{$row['username']}"), "title"=>$row['address']], $row['username'])); $ret->appendChild(BR()); } $ret->appendChild($row['address']); @@ -128,14 +121,14 @@ class MessageColumn extends Column } } - public function read_input(array $inputs): HTMLElement + public function read_input(array $inputs) { $ret = emptyHTML( INPUT([ - "type" => "text", - "name" => "r_{$this->name}[]", - "placeholder" => $this->title, - "value" => @$inputs["r_{$this->name}"][0] + "type"=>"text", + "name"=>"r_{$this->name}[]", + "placeholder"=>$this->title, + "value"=>@$inputs["r_{$this->name}"][0] ]) ); @@ -146,10 +139,10 @@ class MessageColumn extends Column "Error" => SCORE_LOG_ERROR, "Critical" => SCORE_LOG_CRITICAL, ]; - $s = SELECT(["name" => "r_{$this->name}[]"]); - $s->appendChild(OPTION(["value" => ""], '-')); + $s = SELECT(["name"=>"r_{$this->name}[]"]); + $s->appendChild(OPTION(["value"=>""], '-')); foreach ($options as $k => $v) { - $attrs = ["value" => $v]; + $attrs = ["value"=>$v]; if ($v == @$inputs["r_{$this->name}"][1]) { $attrs["selected"] = true; } @@ -159,9 +152,8 @@ class MessageColumn extends Column return $ret; } - public function modify_input_for_read(array|string $input): mixed + public function modify_input_for_read($input) { - assert(is_array($input)); list($m, $l) = $input; if (empty($m)) { $m = "%"; @@ -174,7 +166,7 @@ class MessageColumn extends Column return [$m, $l]; } - public function display(array $row): HTMLElement + public function display($row) { $c = "#000"; switch ($row['priority']) { @@ -194,10 +186,10 @@ class MessageColumn extends Column $c = "#F00"; break; } - return SPAN(["style" => "color: $c"], rawHTML($this->scan_entities($row[$this->name]))); + return SPAN(["style"=>"color: $c"], rawHTML($this->scan_entities($row[$this->name]))); } - protected function scan_entities(string $line): string + protected function scan_entities(string $line) { $line = preg_replace_callback("/Image #(\d+)/s", [$this, "link_image"], $line); $line = preg_replace_callback("/Post #(\d+)/s", [$this, "link_image"], $line); @@ -205,10 +197,7 @@ class MessageColumn extends Column return $line; } - /** - * @param array{1: string} $id - */ - protected function link_image(array $id): string + protected function link_image($id) { $iid = int_escape($id[1]); return ">>$iid"; @@ -232,7 +221,7 @@ class LogTable extends Table new ActionColumn("id"), ]); $this->order_by = ["date_sent DESC"]; - $this->table_attrs = ["class" => "zebra form"]; + $this->table_attrs = ["class" => "zebra"]; } } @@ -241,13 +230,13 @@ class LogDatabase extends Extension /** @var LogDatabaseTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int("log_db_priority", SCORE_LOG_INFO); } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -266,7 +255,7 @@ class LogDatabase extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Logging (Database)"); $sb->add_choice_option("log_db_priority", [ @@ -278,7 +267,7 @@ class LogDatabase extends Extension ], "Debug Level: "); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $database, $user; if ($event->page_matches("log/view")) { @@ -290,17 +279,17 @@ class LogDatabase 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::VIEW_EVENTLOG)) { $event->add_nav_link("event_log", new Link('log/view'), "Event Log"); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::VIEW_EVENTLOG)) { @@ -308,7 +297,7 @@ class LogDatabase extends Extension } } - public function onLog(LogEvent $event): void + public function onLog(LogEvent $event) { global $config, $database, $user; @@ -324,8 +313,8 @@ class LogDatabase extends Extension INSERT INTO score_log(date_sent, section, priority, username, address, message) VALUES(now(), :section, :priority, :username, :address, :message) ", [ - "section" => $event->section, "priority" => $event->priority, "username" => $username, - "address" => get_real_ip(), "message" => $event->message + "section"=>$event->section, "priority"=>$event->priority, "username"=>$username, + "address"=>get_real_ip(), "message"=>$event->message ]); } } diff --git a/ext/log_db/test.php b/ext/log_db/test.php index 2aa05ff7..6185d4c6 100644 --- a/ext/log_db/test.php +++ b/ext/log_db/test.php @@ -6,15 +6,15 @@ namespace Shimmie2; class LogDatabaseTest extends ShimmiePHPUnitTestCase { - public function testLog(): void + public function testLog() { $this->log_in_as_admin(); $this->get_page("log/view"); - $this->get_page("log/view", ["r_module" => "core-image"]); - $this->get_page("log/view", ["r_time" => "2012-03-01"]); - $this->get_page("log/view", ["r_user" => "demo"]); + $this->get_page("log/view", ["r_module"=>"core-image"]); + $this->get_page("log/view", ["r_time"=>"2012-03-01"]); + $this->get_page("log/view", ["r_user"=>"demo"]); - $page = $this->get_page("log/view", ["r_priority" => "10"]); + $page = $this->get_page("log/view", ["r_priority"=>"10"]); $this->assertEquals(200, $page->code); } } diff --git a/ext/log_db/theme.php b/ext/log_db/theme.php index f57183cf..fa12da1b 100644 --- a/ext/log_db/theme.php +++ b/ext/log_db/theme.php @@ -4,18 +4,14 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\emptyHTML; - class LogDatabaseTheme extends Themelet { - public function display_events(HTMLElement $table, HTMLElement $paginator): void + public function display_events($table, $paginator) { global $page; $page->set_title("Event Log"); $page->set_heading("Event Log"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Events", emptyHTML($table, $paginator))); + $page->add_block(new Block("Events", $table . $paginator)); } } diff --git a/ext/log_logstash/main.php b/ext/log_logstash/main.php index 33cc2c69..c0f0a2bb 100644 --- a/ext/log_logstash/main.php +++ b/ext/log_logstash/main.php @@ -6,13 +6,13 @@ namespace Shimmie2; class LogLogstash extends Extension { - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string("log_logstash_host", "127.0.0.1:1234"); } - public function onLog(LogEvent $event): void + public function onLog(LogEvent $event) { global $user; @@ -39,10 +39,7 @@ class LogLogstash extends Extension } } - /** - * @param array $data - */ - private function send_data(array $data): void + private function send_data($data) { global $config; @@ -56,10 +53,10 @@ class LogLogstash extends Extension $host = $parts[0]; $port = (int)$parts[1]; $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (!$fp) { + if (! $fp) { return; } - fwrite($fp, json_encode_ex($data)); + fwrite($fp, json_encode($data)); fclose($fp); } catch (\Exception $e) { // we can't log that logging is broken diff --git a/ext/log_net/main.php b/ext/log_net/main.php index c237d98f..7faa07ec 100644 --- a/ext/log_net/main.php +++ b/ext/log_net/main.php @@ -8,13 +8,13 @@ class LogNet extends Extension { private int $count = 0; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string("log_net_host", "127.0.0.1:35353"); } - public function onLog(LogEvent $event): void + public function onLog(LogEvent $event) { global $user; @@ -31,7 +31,7 @@ class LogNet extends Extension } } - private function msg(string $data): void + private function msg($data) { global $config; $host = $config->get_string("log_net_host"); @@ -45,7 +45,7 @@ class LogNet extends Extension $host = $parts[0]; $port = (int)$parts[1]; $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (!$fp) { + if (! $fp) { return; } fwrite($fp, "$data\n"); diff --git a/ext/media/info.php b/ext/media/info.php index 39efc2c2..5490639e 100644 --- a/ext/media/info.php +++ b/ext/media/info.php @@ -11,7 +11,7 @@ class MediaInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Media"; public string $url = self::SHIMMIE_URL; - 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 common functions and settings used for media operations."; public bool $core = true; diff --git a/ext/media/main.php b/ext/media/main.php index 3d57a9e1..3af4cbed 100644 --- a/ext/media/main.php +++ b/ext/media/main.php @@ -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; - require_once "config.php"; require_once "events.php"; require_once "media_engine.php"; @@ -61,7 +57,7 @@ class Media extends Extension return 30; } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string(MediaConfig::FFPROBE_PATH, 'ffprobe'); @@ -70,7 +66,7 @@ class Media extends Extension $config->set_default_string(MediaConfig::CONVERT_PATH, 'convert'); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -85,7 +81,7 @@ class Media extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Media Engine Commands"); @@ -110,7 +106,7 @@ class Media extends Extension $sb->end_table(); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::DELETE_IMAGE)) { @@ -118,7 +114,7 @@ class Media extends Extension } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::RESCAN_MEDIA)) { @@ -126,7 +122,7 @@ class Media extends Extension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $page, $user; @@ -151,22 +147,22 @@ class Media extends Extension } } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('media-rescan') - ->addArgument('id_or_hash', InputArgument::REQUIRED) - ->setDescription('Refresh metadata for a given post') - ->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 MediaCheckPropertiesEvent($image)); - $image->save_to_db(); - } else { - $output->writeln("No post with ID '$uid'"); - } - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + print "\tmedia-rescan \n"; + print "\t\trefresh metadata for a given post\n\n"; + } + if ($event->cmd == "media-rescan") { + $uid = $event->args[0]; + $image = Image::by_id_or_hash($uid); + if ($image) { + send_event(new MediaCheckPropertiesEvent($image)); + $image->save_to_db(); + } else { + print("No post with ID '$uid'\n"); + } + } } /** @@ -174,7 +170,7 @@ class Media extends Extension * @throws MediaException * @throws InsufficientMemoryException */ - public function onMediaResize(MediaResizeEvent $event): void + public function onMediaResize(MediaResizeEvent $event) { if (!in_array( $event->resize_type, @@ -237,7 +233,7 @@ class Media extends Extension public const CONTENT_SEARCH_TERM_REGEX = "/^content[=|:]((video)|(audio)|(image)|(unknown))$/i"; - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -246,17 +242,17 @@ class Media extends Extension $matches = []; if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term, $matches)) { $field = $matches[1]; - if ($field === "unknown") { + if ($field==="unknown") { $event->add_querylet(new Querylet("video IS NULL OR audio IS NULL OR image IS NULL")); } else { - $event->add_querylet(new Querylet("$field = :true", ["true" => true])); + $event->add_querylet(new Querylet("$field = :true", ["true"=>true])); } } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Media"; $block->body = $this->theme->get_help_html(); @@ -264,25 +260,23 @@ class Media extends Extension } } - public function onTagTermCheck(TagTermCheckEvent $event): void + public function onTagTermCheck(TagTermCheckEvent $event) { if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term)) { $event->metatag = true; } } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { if ($event->image->width && $event->image->height && $event->image->length) { - $s = ((int)($event->image->length / 100)) / 10; + $s = ((int)($event->image->length / 100))/10; $event->replace('$size', "{$event->image->width}x{$event->image->height}, {$s}s"); } elseif ($event->image->width && $event->image->height) { $event->replace('$size', "{$event->image->width}x{$event->image->height}"); } elseif ($event->image->length) { - $s = ((int)($event->image->length / 100)) / 10; + $s = ((int)($event->image->length / 100))/10; $event->replace('$size', "{$s}s"); - } else { - $event->replace('$size', "unknown size"); } } @@ -298,7 +292,7 @@ class Media extends Extension * The factor of 2.5 is simply a rough guideline. * https://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize * - * @param array{0:int,1:int,2:int,bits?:int,channels?:int} $info The output of getimagesize() for the source file in question. + * @param array $info The output of getimagesize() for the source file in question. * @return int The number of bytes an image resize operation is estimated to use. */ public static function calc_memory_use(array $info): int @@ -321,7 +315,7 @@ class Media extends Extension * @return bool true if successful, false if not. * @throws MediaException */ - public static function create_thumbnail_ffmpeg(Image $image): bool + public static function create_thumbnail_ffmpeg($hash): bool { global $config; @@ -331,10 +325,10 @@ class Media extends Extension } $ok = false; - $inname = $image->get_image_filename(); - $tmpname = shm_tempnam("ffmpeg_thumb"); + $inname = warehouse_path(Image::IMAGE_DIR, $hash); + $tmpname = tempnam(sys_get_temp_dir(), "shimmie_ffmpeg_thumb"); try { - $outname = $image->get_thumb_filename(); + $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash); $orig_size = self::video_size($inname); $scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1], true); @@ -368,10 +362,8 @@ class Media extends Extension return $ok; } - /** - * @return array - */ - public static function get_ffprobe_data(string $filename): array + + public static function get_ffprobe_data($filename): array { global $config; @@ -412,112 +404,112 @@ class Media extends Extension return $ext; } - // private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize): void - // { - // switch ($format) { - // case FileExtension::PNG: - // $result = $image->setOption('png:compression-level', 9); - // if ($result !== true) { - // throw new GraphicsException("Could not set png compression option"); - // } - // break; - // case Graphics::WEBP_LOSSLESS: - // $result = $image->setOption('webp:lossless', true); - // if ($result !== true) { - // throw new GraphicsException("Could not set lossless webp option"); - // } - // break; - // default: - // $result = $image->setImageCompressionQuality($output_quality); - // if ($result !== true) { - // throw new GraphicsException("Could not set compression quality for $path to $output_quality"); - // } - // break; - // } - // - // if (self::supports_alpha($format)) { - // $result = $image->setImageBackgroundColor(new \ImagickPixel('transparent')); - // } else { - // $result = $image->setImageBackgroundColor(new \ImagickPixel('black')); - // } - // if ($result !== true) { - // throw new GraphicsException("Could not set background color"); - // } - // - // - // if ($minimize) { - // $profiles = $image->getImageProfiles("icc", true); - // $result = $image->stripImage(); - // if ($result !== true) { - // throw new GraphicsException("Could not strip information from image"); - // } - // if (!empty($profiles)) { - // $image->profileImage("icc", $profiles['icc']); - // } - // } - // - // $ext = self::determine_ext($format); - // - // $result = $image->writeImage($ext . ":" . $path); - // if ($result !== true) { - // throw new GraphicsException("Could not write image to $path"); - // } - // } +// private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize) +// { +// switch ($format) { +// case FileExtension::PNG: +// $result = $image->setOption('png:compression-level', 9); +// if ($result !== true) { +// throw new GraphicsException("Could not set png compression option"); +// } +// break; +// case Graphics::WEBP_LOSSLESS: +// $result = $image->setOption('webp:lossless', true); +// if ($result !== true) { +// throw new GraphicsException("Could not set lossless webp option"); +// } +// break; +// default: +// $result = $image->setImageCompressionQuality($output_quality); +// if ($result !== true) { +// throw new GraphicsException("Could not set compression quality for $path to $output_quality"); +// } +// break; +// } +// +// if (self::supports_alpha($format)) { +// $result = $image->setImageBackgroundColor(new \ImagickPixel('transparent')); +// } else { +// $result = $image->setImageBackgroundColor(new \ImagickPixel('black')); +// } +// if ($result !== true) { +// throw new GraphicsException("Could not set background color"); +// } +// +// +// if ($minimize) { +// $profiles = $image->getImageProfiles("icc", true); +// $result = $image->stripImage(); +// if ($result !== true) { +// throw new GraphicsException("Could not strip information from image"); +// } +// if (!empty($profiles)) { +// $image->profileImage("icc", $profiles['icc']); +// } +// } +// +// $ext = self::determine_ext($format); +// +// $result = $image->writeImage($ext . ":" . $path); +// if ($result !== true) { +// throw new GraphicsException("Could not write image to $path"); +// } +// } - // public static function image_resize_imagick( - // string $input_path, - // string $input_type, - // int $new_width, - // int $new_height, - // string $output_filename, - // string $output_type = null, - // bool $ignore_aspect_ratio = false, - // int $output_quality = 80, - // bool $minimize = false, - // bool $allow_upscale = true - // ): void - // { - // global $config; - // - // if (!empty($input_type)) { - // $input_type = self::determine_ext($input_type); - // } - // - // try { - // $image = new Imagick($input_type . ":" . $input_path); - // try { - // $result = $image->flattenImages(); - // if ($result !== true) { - // throw new GraphicsException("Could not flatten image $input_path"); - // } - // - // $height = $image->getImageHeight(); - // $width = $image->getImageWidth(); - // if (!$allow_upscale && - // ($new_width > $width || $new_height > $height)) { - // $new_height = $height; - // $new_width = $width; - // } - // - // $result = $image->resizeImage($new_width, $new_width, Imagick::FILTER_LANCZOS, 0, !$ignore_aspect_ratio); - // if ($result !== true) { - // throw new GraphicsException("Could not perform image resize on $input_path"); - // } - // - // - // if (empty($output_type)) { - // $output_type = $input_type; - // } - // - // self::image_save_imagick($image, $output_filename, $output_type, $output_quality); - // - // } finally { - // $image->destroy(); - // } - // } catch (ImagickException $e) { - // throw new GraphicsException("Error while resizing with Imagick: " . $e->getMessage(), $e->getCode(), $e); - // } - // } +// public static function image_resize_imagick( +// String $input_path, +// String $input_type, +// int $new_width, +// int $new_height, +// string $output_filename, +// string $output_type = null, +// bool $ignore_aspect_ratio = false, +// int $output_quality = 80, +// bool $minimize = false, +// bool $allow_upscale = true +// ): void +// { +// global $config; +// +// if (!empty($input_type)) { +// $input_type = self::determine_ext($input_type); +// } +// +// try { +// $image = new Imagick($input_type . ":" . $input_path); +// try { +// $result = $image->flattenImages(); +// if ($result !== true) { +// throw new GraphicsException("Could not flatten image $input_path"); +// } +// +// $height = $image->getImageHeight(); +// $width = $image->getImageWidth(); +// if (!$allow_upscale && +// ($new_width > $width || $new_height > $height)) { +// $new_height = $height; +// $new_width = $width; +// } +// +// $result = $image->resizeImage($new_width, $new_width, Imagick::FILTER_LANCZOS, 0, !$ignore_aspect_ratio); +// if ($result !== true) { +// throw new GraphicsException("Could not perform image resize on $input_path"); +// } +// +// +// if (empty($output_type)) { +// $output_type = $input_type; +// } +// +// self::image_save_imagick($image, $output_filename, $output_type, $output_quality); +// +// } finally { +// $image->destroy(); +// } +// } catch (ImagickException $e) { +// throw new GraphicsException("Error while resizing with Imagick: " . $e->getMessage(), $e->getCode(), $e); +// } +// } public static function is_lossless(string $filename, string $mime): bool { @@ -556,7 +548,7 @@ class Media extends Extension $output_mime = $input_mime; } - if ($output_mime == MimeType::WEBP && self::is_lossless($input_path, $input_mime)) { + if ($output_mime==MimeType::WEBP && self::is_lossless($input_path, $input_mime)) { $output_mime = MimeType::WEBP_LOSSLESS; } @@ -569,7 +561,7 @@ class Media extends Extension if (!$allow_upscale) { $resize_suffix .= "\>"; } - if ($resize_type == Media::RESIZE_TYPE_STRETCH) { + if ($resize_type==Media::RESIZE_TYPE_STRETCH) { $resize_suffix .= "\!"; } @@ -584,8 +576,8 @@ class Media extends Extension $file_arg = "{$input_ext}:\"{$input_path}[0]\""; - if ($resize_type === Media::RESIZE_TYPE_FIT_BLUR_PORTRAIT) { - if ($new_height > $new_width) { + if ($resize_type===Media::RESIZE_TYPE_FIT_BLUR_PORTRAIT) { + if ($new_height>$new_width) { $resize_type = Media::RESIZE_TYPE_FIT_BLUR; } else { $resize_type = Media::RESIZE_TYPE_FILL; @@ -639,8 +631,8 @@ class Media extends Extension /** * Performs a resize operation on an image file using GD. * - * @param string $image_filename The source file to be resized. - * @param array{0:int,1:int,2:int} $info The output of getimagesize() for the source file. + * @param String $image_filename The source file to be resized. + * @param array $info The output of getimagesize() for the source file. * @param int $new_width * @param int $new_height * @param string $output_filename @@ -660,7 +652,7 @@ class Media extends Extension string $resize_type = self::RESIZE_TYPE_FIT, int $output_quality = 80, bool $allow_upscale = true - ): void { + ) { $width = $info[0]; $height = $info[1]; @@ -693,7 +685,7 @@ class Media extends Extension throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)"); } - if ($resize_type == Media::RESIZE_TYPE_FIT) { + if ($resize_type==Media::RESIZE_TYPE_FIT) { list($new_width, $new_height) = get_scaled_by_aspect_ratio($width, $height, $new_width, $new_height); } if (!$allow_upscale && @@ -702,17 +694,16 @@ class Media extends Extension $new_width = $width; } - $image = imagecreatefromstring(file_get_contents_ex($image_filename)); - if ($image === false) { - throw new MediaException("Could not load image: " . $image_filename); - } - + $image = imagecreatefromstring(file_get_contents($image_filename)); $image_resized = imagecreatetruecolor($new_width, $new_height); - if ($image_resized === false) { - throw new MediaException("Could not create output image with dimensions $new_width x $new_height "); - } - try { + if ($image === false) { + throw new MediaException("Could not load image: " . $image_filename); + } + if ($image_resized === false) { + throw new MediaException("Could not create output image with dimensions $new_width c $new_height "); + } + // Handle transparent images switch ($info[2]) { case IMAGETYPE_GIF: @@ -783,15 +774,18 @@ class Media extends Extension $width = imagesx($image_resized); $height = imagesy($image_resized); $new_image = imagecreatetruecolor($width, $height); - if ($new_image === false) { + if ($new_image===false) { throw new ImageTranscodeException("Could not create image with dimensions $width x $height"); } $background_color = Media::hex_color_allocate($new_image, $alpha_color); - if (imagefilledrectangle($new_image, 0, 0, $width, $height, $background_color) === false) { + if ($background_color===false) { + throw new ImageTranscodeException("Could not allocate background color"); + } + if (imagefilledrectangle($new_image, 0, 0, $width, $height, $background_color)===false) { throw new ImageTranscodeException("Could not fill background color"); } - if (imagecopy($new_image, $image_resized, 0, 0, 0, 0, $width, $height) === false) { + if (imagecopy($new_image, $image_resized, 0, 0, 0, 0, $width, $height)===false) { throw new ImageTranscodeException("Could not copy source image to new image"); } @@ -839,7 +833,7 @@ class Media extends Extension * Determines the dimensions of a video file using ffmpeg. * * @param string $filename - * @return array{0: int, 1: int} + * @return array [width, height] */ public static function video_size(string $filename): array { @@ -850,7 +844,7 @@ class Media extends Extension "-y", "-i", escapeshellarg($filename), "-vstats" ])); - $output = null_throws(false_throws(shell_exec($cmd . " 2>&1"))); + $output = shell_exec($cmd . " 2>&1"); // error_log("Getting size with `$cmd`"); $regex_sizes = "/Video: .* ([0-9]{1,4})x([0-9]{1,4})/"; @@ -867,7 +861,7 @@ class Media extends Extension return $size; } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $config, $database; if ($this->get_version(MediaConfig::VERSION) < 1) { @@ -933,20 +927,18 @@ class Media extends Extension } if ($this->get_version(MediaConfig::VERSION) < 5) { - $database->execute("UPDATE images SET image = :f WHERE ext IN ('swf','mp3','ani','flv','mp4','m4v','ogv','webm')", ["f" => false]); - $database->execute("UPDATE images SET image = :t WHERE ext IN ('jpg','jpeg','ico','cur','png')", ["t" => true]); + $database->execute("UPDATE images SET image = :f WHERE ext IN ('swf','mp3','ani','flv','mp4','m4v','ogv','webm')", ["f"=>false]); + $database->execute("UPDATE images SET image = :t WHERE ext IN ('jpg','jpeg','ico','cur','png')", ["t"=>true]); $this->set_version(MediaConfig::VERSION, 5); } } - public static function hex_color_allocate(mixed $im, string $hex): int + public static function hex_color_allocate($im, $hex) { $hex = ltrim($hex, '#'); - $a = (int)hexdec(substr($hex, 0, 2)); - $b = (int)hexdec(substr($hex, 2, 2)); - $c = (int)hexdec(substr($hex, 4, 2)); - $col = imagecolorallocate($im, $a, $b, $c); - assert($col !== false); - return $col; + $a = hexdec(substr($hex, 0, 2)); + $b = hexdec(substr($hex, 2, 2)); + $c = hexdec(substr($hex, 4, 2)); + return imagecolorallocate($im, $a, $b, $c); } } diff --git a/ext/media/theme.php b/ext/media/theme.php index 3438b866..00ec1922 100644 --- a/ext/media/theme.php +++ b/ext/media/theme.php @@ -8,11 +8,11 @@ use function MicroHTML\INPUT; class MediaTheme extends Themelet { - public function get_buttons_html(int $image_id): \MicroHTML\HTMLElement + public function get_buttons_html(int $image_id): string { - return SHM_SIMPLE_FORM( + return (string)SHM_SIMPLE_FORM( "media_rescan/", - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image_id]), + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image_id]), SHM_SUBMIT('Scan Media Properties'), ); } diff --git a/ext/mime/file_extension.php b/ext/mime/file_extension.php index 2526939e..52b72fe6 100644 --- a/ext/mime/file_extension.php +++ b/ext/mime/file_extension.php @@ -77,12 +77,12 @@ class FileExtension return null; } - if ($mime == MimeType::OCTET_STREAM) { + if ($mime==MimeType::OCTET_STREAM) { return null; } $data = MimeMap::get_for_mime($mime); - if ($data != null) { + if ($data!=null) { return $data[MimeMap::MAP_EXT][0]; } return null; @@ -90,8 +90,6 @@ class FileExtension /** * Returns all the file extension associated with the specified mimetype. - * - * @return string[] */ public static function get_all_for_mime(string $mime): array { @@ -99,12 +97,12 @@ class FileExtension return []; } - if ($mime == MimeType::OCTET_STREAM) { + if ($mime==MimeType::OCTET_STREAM) { return []; } $data = MimeMap::get_for_mime($mime); - if ($data != null) { + if ($data!=null) { return $data[MimeMap::MAP_EXT]; } diff --git a/ext/mime/info.php b/ext/mime/info.php index 9818481a..fb8173e9 100644 --- a/ext/mime/info.php +++ b/ext/mime/info.php @@ -10,7 +10,7 @@ class MimeSystemInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "MIME"; - 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 system mime-related functionality"; public bool $core = true; diff --git a/ext/mime/main.php b/ext/mime/main.php index 88ba8674..d5fe4402 100644 --- a/ext/mime/main.php +++ b/ext/mime/main.php @@ -15,14 +15,14 @@ class MimeSystem extends Extension public const VERSION = "ext_mime_version"; - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { $event->replace('$ext', $event->image->get_ext()); $event->replace('$mime', $event->image->get_mime()); } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -42,7 +42,7 @@ class MimeSystem extends Extension foreach ($extensions as $ext) { $mime = MimeType::get_for_extension($ext); - if (empty($mime) || $mime === MimeType::OCTET_STREAM) { + if (empty($mime) || $mime===MimeType::OCTET_STREAM) { throw new SCoreException("Unknown extension: $ext"); } @@ -55,13 +55,12 @@ class MimeSystem extends Extension } $this->set_version(self::VERSION, 1); - $database->begin_transaction(); } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "File Types"; $block->body = $this->theme->get_help_html(); @@ -69,7 +68,7 @@ class MimeSystem extends Extension } } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -82,7 +81,7 @@ class MimeSystem extends Extension $event->add_querylet(new Querylet('images.ext = :ext', ["ext" => $ext])); } elseif (preg_match("/^mime[=|:](.+)$/i", $event->term, $matches)) { $mime = strtolower($matches[1]); - $event->add_querylet(new Querylet("images.mime = :mime", ["mime" => $mime])); + $event->add_querylet(new Querylet("images.mime = :mime", ["mime"=>$mime])); } } } diff --git a/ext/mime/mime_map.php b/ext/mime/mime_map.php index 325869eb..b5f75b8c 100644 --- a/ext/mime/mime_map.php +++ b/ext/mime/mime_map.php @@ -235,9 +235,6 @@ class MimeMap ], ]; - /** - * @return array{name: string, ext: string[], mime: string[]} - */ public static function get_for_extension(string $ext): ?array { $ext = strtolower($ext); @@ -250,9 +247,6 @@ class MimeMap return null; } - /** - * @return array{name: string, ext: string[], mime: string[]} - */ public static function get_for_mime(string $mime): ?array { $mime = strtolower(MimeType::remove_parameters($mime)); @@ -268,7 +262,7 @@ class MimeMap public static function get_name_for_mime(string $mime): ?string { $data = self::get_for_mime($mime); - if ($data !== null) { + if ($data!==null) { return $data[self::MAP_NAME]; } return null; diff --git a/ext/mime/mime_type.php b/ext/mime/mime_type.php index 2ac38208..fbb0906f 100644 --- a/ext/mime/mime_type.php +++ b/ext/mime/mime_type.php @@ -80,10 +80,10 @@ class MimeType public static function is_mime(string $value): bool { - return preg_match(self::REGEX_MIME_TYPE, $value) === 1; + return preg_match(self::REGEX_MIME_TYPE, $value)===1; } - public static function add_parameters(string $mime, string ...$parameters): string + public static function add_parameters(String $mime, String...$parameters): string { if (empty($parameters)) { return $mime; @@ -94,15 +94,12 @@ class MimeType public static function remove_parameters(string $mime): string { $i = strpos($mime, ";"); - if ($i !== false) { + if ($i!==false) { return substr($mime, 0, $i); } return $mime; } - /** - * @param array $mime_array - */ public static function matches_array(string $mime, array $mime_array, bool $exact = false): bool { // If there's an exact match, find it and that's it @@ -124,14 +121,14 @@ class MimeType $mime1 = self::remove_parameters($mime1); $mime2 = self::remove_parameters($mime2); } - return strtolower($mime1) === strtolower($mime2); + return strtolower($mime1)===strtolower($mime2); } /** * Determines if a file is an animated gif. * - * @param string $image_filename The path of the file to check. + * @param String $image_filename The path of the file to check. * @return bool true if the file is an animated gif, false if it is not. */ public static function is_animated_gif(string $image_filename): bool @@ -150,16 +147,13 @@ class MimeType @fclose($fh); } } - return ($is_anim_gif >= 2); + return ($is_anim_gif >=2); } - /** - * @param array $comparison - */ private static function compare_file_bytes(string $file_name, array $comparison): bool { - $size = filesize_ex($file_name); + $size = filesize($file_name); $cc = count($comparison); if ($size < $cc) { // Can't match because it's too small @@ -168,7 +162,7 @@ class MimeType if (($fh = @fopen($file_name, 'rb'))) { try { - $chunk = false_throws(unpack("C*", false_throws(fread($fh, $cc)))); + $chunk = unpack("C*", fread($fh, $cc)); for ($i = 0; $i < $cc; $i++) { $byte = $comparison[$i]; @@ -208,11 +202,11 @@ class MimeType public static function get_for_extension(string $ext): ?string { $data = MimeMap::get_for_extension($ext); - if ($data != null) { + if ($data!=null) { return $data[MimeMap::MAP_MIME][0]; } // This was an old solution for differentiating lossless webps - if ($ext === "webp-lossless") { + if ($ext==="webp-lossless") { return MimeType::WEBP_LOSSLESS; } return null; @@ -220,8 +214,8 @@ class MimeType /** * Returns the mimetype for the specified file via file inspection - * @param string $file - * @return string The mimetype that was found. Returns generic octet binary mimetype if not found. + * @param String $file + * @return String The mimetype that was found. Returns generic octet binary mimetype if not found. */ public static function get_for_file(string $file, ?string $ext = null): string { @@ -231,9 +225,12 @@ class MimeType $output = self::OCTET_STREAM; - $finfo = false_throws(finfo_open(FILEINFO_MIME_TYPE)); - $type = finfo_file($finfo, $file); - finfo_close($finfo); + $finfo = finfo_open(FILEINFO_MIME_TYPE); + try { + $type = finfo_file($finfo, $file); + } finally { + finfo_close($finfo); + } if ($type !== false && !empty($type)) { $output = $type; @@ -242,10 +239,10 @@ class MimeType if (!empty($ext)) { // Here we handle the few file types that need extension-based handling $ext = strtolower($ext); - if ($type === MimeType::ZIP && $ext === FileExtension::CBZ) { + if ($type===MimeType::ZIP && $ext===FileExtension::CBZ) { $output = MimeType::COMIC_ZIP; } - if ($type === MimeType::OCTET_STREAM) { + if ($type===MimeType::OCTET_STREAM) { switch ($ext) { case FileExtension::ANI: $output = MimeType::ANI; diff --git a/ext/mime/test.php b/ext/mime/test.php index 788e55f5..1c374a32 100644 --- a/ext/mime/test.php +++ b/ext/mime/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class MimeSystemTest extends ShimmiePHPUnitTestCase { - public function testJPEG(): void + public function testJPEG() { $result = MimeType::get_for_file("tests/bedroom_workshop.jpg"); $this->assertEquals(MimeType::JPEG, $result); diff --git a/ext/nav_timing/info.php b/ext/nav_timing/info.php deleted file mode 100644 index a4bb46ea..00000000 --- a/ext/nav_timing/info.php +++ /dev/null @@ -1,16 +0,0 @@ - { - list.getEntries().forEach((entry) => { - shm_log("timing", {"v": 2, ...entry.toJSON()}); - }); -}); -observer.observe({ entryTypes: ["navigation"] }); diff --git a/ext/not_a_tag/main.php b/ext/not_a_tag/main.php index fa93f936..74cb064e 100644 --- a/ext/not_a_tag/main.php +++ b/ext/not_a_tag/main.php @@ -26,7 +26,7 @@ class NotATagTable extends Table $this->order_by = ["tag", "redirect"]; $this->create_url = make_link("untag/add"); $this->delete_url = make_link("untag/remove"); - $this->table_attrs = ["class" => "zebra form"]; + $this->table_attrs = ["class" => "zebra"]; } } @@ -40,7 +40,7 @@ class NotATag extends Extension return 30; } // before ImageUploadEvent and tag_history - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; if ($this->get_version("ext_notatag_version") < 1) { @@ -52,20 +52,25 @@ class NotATag extends Extension } } - public function onTagSet(TagSetEvent $event): void + public function onImageAddition(ImageAdditionEvent $event) + { + $this->scan($event->image->get_tag_array()); + } + + public function onTagSet(TagSetEvent $event) { global $user; if ($user->can(Permissions::BAN_IMAGE)) { - $event->new_tags = $this->strip($event->new_tags); + $event->tags = $this->strip($event->tags); } else { - $this->scan($event->new_tags); + $this->scan($event->tags); } } /** - * @param string[] $tags_mixed + * #param string[] $tags_mixed */ - private function scan(array $tags_mixed): void + private function scan(array $tags_mixed) { global $database; @@ -85,8 +90,7 @@ class NotATag extends Extension } /** - * @param string[] $tags - * @return string[] + * #param string[] $tags */ private function strip(array $tags): array { @@ -107,17 +111,17 @@ class NotATag extends Extension return $ok_tags; } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { global $user; - if ($event->parent === "tags") { + if ($event->parent==="tags") { if ($user->can(Permissions::BAN_IMAGE)) { $event->add_nav_link("untags", new Link('untag/list/1'), "UnTags"); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::BAN_IMAGE)) { @@ -125,7 +129,7 @@ class NotATag extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $database, $page, $user; @@ -133,19 +137,19 @@ class NotATag extends Extension if ($user->can(Permissions::BAN_IMAGE)) { if ($event->get_arg(0) == "add") { $user->ensure_authed(); - $input = validate_input(["c_tag" => "string", "c_redirect" => "string"]); + $input = validate_input(["c_tag"=>"string", "c_redirect"=>"string"]); $database->execute( "INSERT INTO untags(tag, redirect) VALUES (:tag, :redirect)", - ["tag" => $input['c_tag'], "redirect" => $input['c_redirect']] + ["tag"=>$input['c_tag'], "redirect"=>$input['c_redirect']] ); $page->set_mode(PageMode::REDIRECT); $page->set_redirect(referer_or(make_link())); } elseif ($event->get_arg(0) == "remove") { $user->ensure_authed(); - $input = validate_input(["d_tag" => "string"]); + $input = validate_input(["d_tag"=>"string"]); $database->execute( "DELETE FROM untags WHERE LOWER(tag) = LOWER(:tag)", - ["tag" => $input['d_tag']] + ["tag"=>$input['d_tag']] ); $page->flash("Post ban removed"); $page->set_mode(PageMode::REDIRECT); diff --git a/ext/not_a_tag/test.php b/ext/not_a_tag/test.php index 87e40560..3a9277c7 100644 --- a/ext/not_a_tag/test.php +++ b/ext/not_a_tag/test.php @@ -6,11 +6,11 @@ namespace Shimmie2; class NotATagTest extends ShimmiePHPUnitTestCase { - public function testUntags(): void + public function testUntags() { global $database; $database->execute("DELETE FROM untags"); - $database->execute("INSERT INTO untags(tag, redirect) VALUES (:tag, :redirect)", ["tag" => "face", "redirect" => "no-body-parts.html"]); + $database->execute("INSERT INTO untags(tag, redirect) VALUES (:tag, :redirect)", ["tag"=>"face", "redirect"=>"no-body-parts.html"]); $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); diff --git a/ext/not_a_tag/theme.php b/ext/not_a_tag/theme.php index 04acdfe4..7c23a7a3 100644 --- a/ext/not_a_tag/theme.php +++ b/ext/not_a_tag/theme.php @@ -4,17 +4,13 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\emptyHTML; - class NotATagTheme extends Themelet { - public function display_untags(Page $page, HTMLElement $table, HTMLElement $paginator): void + public function display_untags(Page $page, $table, $paginator) { $page->set_title("UnTags"); $page->set_heading("UnTags"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Edit UnTags", emptyHTML($table, $paginator))); + $page->add_block(new Block("Edit UnTags", $table . $paginator)); } } diff --git a/ext/notes/border-h.gif b/ext/notes/border-h.gif new file mode 100644 index 00000000..a2aa5b0d Binary files /dev/null and b/ext/notes/border-h.gif differ diff --git a/ext/notes/border-v.gif b/ext/notes/border-v.gif new file mode 100644 index 00000000..4bfd5556 Binary files /dev/null and b/ext/notes/border-v.gif differ diff --git a/ext/notes/info.php b/ext/notes/info.php index 3d6b125b..6ed211d0 100644 --- a/ext/notes/info.php +++ b/ext/notes/info.php @@ -10,7 +10,7 @@ class NotesInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Notes"; - public array $authors = ["Sein Kraft" => "mail@seinkraft.info"]; + public array $authors = ["Sein Kraft"=>"mail@seinkraft.info"]; public string $license = self::LICENSE_GPLV2; public string $description = "Annotate images"; } diff --git a/ext/notes/lib/jquery.imgareaselect-1.0.0-rc1.min.js b/ext/notes/lib/jquery.imgareaselect-1.0.0-rc1.min.js new file mode 100644 index 00000000..c842914d --- /dev/null +++ b/ext/notes/lib/jquery.imgareaselect-1.0.0-rc1.min.js @@ -0,0 +1,2 @@ +/*! imgareaselect 1.0.0-rc.1 */ +(function(e){function t(){return e("
    ")}var o=Math.abs,n=Math.max,i=Math.min,s=Math.round;e.imgAreaSelect=function(a,c){function r(e){return e+bt.left-St.left}function d(e){return e+bt.top-St.top}function u(e){return e-bt.left+St.left}function h(e){return e-bt.top+St.top}function f(e){var t,o=m(e)||e;return(t=parseInt(o.pageX))?t-St.left:void 0}function l(e){var t,o=m(e)||e;return(t=parseInt(o.pageY))?t-St.top:void 0}function m(e){var t=e.originalEvent||{};return t.touches&&t.touches.length?t.touches[0]:!1}function p(e){var t=e||G,o=e||J;return{x1:s(At.x1*t),y1:s(At.y1*o),x2:s(At.x2*t)-1,y2:s(At.y2*o)-1,width:s(At.x2*t)-s(At.x1*t),height:s(At.y2*o)-s(At.y1*o)}}function v(e,t,o,n,i){var a=i||G,c=i||J;At={x1:s(e/a||0),y1:s(t/c||0),x2:s(++o/a||0),y2:s(++n/c||0)},At.width=At.x2-At.x1,At.height=At.y2-At.y1}function y(){$&&pt.width()&&(bt={left:s(pt.offset().left),top:s(pt.offset().top)},Q=pt.innerWidth(),R=pt.innerHeight(),bt.top+=pt.outerHeight()-R>>1,bt.left+=pt.outerWidth()-Q>>1,V=s(c.minWidth/G)||0,Z=s(c.minHeight/J)||0,_=s(i(c.maxWidth/G||1<<24,Q)),et=s(i(c.maxHeight/J||1<<24,R)),St="fixed"==kt?{left:e(document).scrollLeft(),top:e(document).scrollTop()}:/static|^$/.test(X.css("position"))?{left:0,top:0}:{left:s(X.offset().left)-X.scrollLeft(),top:s(X.offset().top)-X.scrollTop()},L=r(0),j=d(0),(At.x2>Q||At.y2>R)&&P())}function g(t){if(ot){switch(vt.css({left:r(At.x1),top:d(At.y1)}).add(yt).width(ft=At.width).height(lt=At.height),yt.add(gt).add(wt).css({left:0,top:0}),gt.add(xt).width(n(ft-gt.outerWidth()+gt.innerWidth(),0)).height(n(lt-gt.outerHeight()+gt.innerHeight(),0)),xt.css({left:L,top:j,width:ft,height:lt,borderStyle:"solid",borderWidth:At.y1+"px "+(Q-At.x2)+"px "+(R-At.y2)+"px "+At.x1+"px"}),ft-=wt.outerWidth(),lt-=wt.outerHeight(),wt.length){case 8:e(wt[4]).css({left:ft>>1}),e(wt[5]).css({left:ft,top:lt>>1}),e(wt[6]).css({left:ft>>1,top:lt}),e(wt[7]).css({top:lt>>1});case 4:wt.slice(1,3).css({left:ft}),wt.slice(2,4).css({top:lt})}t!==!1&&(e.imgAreaSelect.keyPress!=Pt&&e(document).unbind(e.imgAreaSelect.keyPress,e.imgAreaSelect.onKeyPress),c.keys&&e(document)[e.imgAreaSelect.keyPress](e.imgAreaSelect.onKeyPress=Pt))}}function x(e){y(),g(e),nt=r(At.x1),it=d(At.y1),st=r(At.x2),at=d(At.y2)}function w(e,t){c.fadeDuration?e.fadeOut(c.fadeDuration,t):e.hide()}function b(e){return ct&&!/^touch/.test(e.type)}function S(e){var t=u(f(e))-At.x1,o=h(l(e))-At.y1;U="",c.resizable&&(c.resizeMargin>=o?U="n":o>=At.height-c.resizeMargin&&(U="s"),c.resizeMargin>=t?U+="w":t>=At.width-c.resizeMargin&&(U+="e")),vt.css("cursor",U?U+"-resize":c.movable?"move":"")}function z(e){b(e)||(mt||(y(),mt=!0,vt.one("mouseout",function(){mt=!1})),S(e))}function k(t){ct=!1,e("body").css("cursor",""),(c.autoHide||0==At.width*At.height)&&w(vt.add(xt),function(){e(this).hide()}),e(document).off("mousemove touchmove",N),vt.on("mousemove touchmove",z),t&&c.onSelectEnd(a,p())}function A(t){return"mousedown"==t.type&&1!=t.which?!1:("touchstart"==t.type?(ct&&k(),ct=!0,S(t)):y(),U?(nt=r(At["x"+(1+/w/.test(U))]),it=d(At["y"+(1+/n/.test(U))]),st=r(At["x"+(1+!/w/.test(U))]),at=d(At["y"+(1+!/n/.test(U))]),B=st-f(t),F=at-l(t),e(document).on("mousemove touchmove",N).one("mouseup touchend",k),vt.off("mousemove touchmove",z)):c.movable?(Y=L+At.x1-f(t),q=j+At.y1-l(t),vt.off("mousemove touchmove",z),e(document).on("mousemove touchmove",H).one("mouseup touchend",function(){ct=!1,c.onSelectEnd(a,p()),e(document).off("mousemove touchmove",H),vt.on("mousemove touchmove",z)})):pt.mousedown(t),!1)}function I(e){tt&&(e?(st=n(L,i(L+Q,nt+o(at-it)*tt*(st>nt||-1))),at=s(n(j,i(j+R,it+o(st-nt)/tt*(at>it||-1)))),st=s(st)):(at=n(j,i(j+R,it+o(st-nt)/tt*(at>it||-1))),st=s(n(L,i(L+Q,nt+o(at-it)*tt*(st>nt||-1)))),at=s(at)))}function P(){nt=i(nt,L+Q),it=i(it,j+R),V>o(st-nt)&&(st=nt-V*(nt>st||-1),L>st?nt=L+V:st>L+Q&&(nt=L+Q-V)),Z>o(at-it)&&(at=it-Z*(it>at||-1),j>at?it=j+Z:at>j+R&&(it=j+R-Z)),st=n(L,i(st,L+Q)),at=n(j,i(at,j+R)),I(o(st-nt)_&&(st=nt-_*(nt>st||-1),I()),o(at-it)>et&&(at=it-et*(it>at||-1),I(!0)),At={x1:u(i(nt,st)),x2:u(n(nt,st)),y1:h(i(it,at)),y2:h(n(it,at)),width:o(st-nt),height:o(at-it)}}function K(){P(),g(),c.onSelectChange(a,p())}function N(e){return b(e)?void 0:(P(),st=/w|e|^$/.test(U)||tt?f(e)+B:r(At.x2),at=/n|s|^$/.test(U)||tt?l(e)+F:d(At.y2),K(),!1)}function C(t,o){st=(nt=t)+At.width,at=(it=o)+At.height,e.extend(At,{x1:u(nt),y1:h(it),x2:u(st),y2:h(at)}),g(),c.onSelectChange(a,p())}function H(e){return b(e)?void 0:(nt=n(L,i(Y+f(e),L+Q-At.width)),it=n(j,i(q+l(e),j+R-At.height)),C(nt,it),e.preventDefault(),!1)}function M(){e(document).off("mousemove touchmove",M),y(),st=nt,at=it,K(),U="",xt.is(":visible")||vt.add(xt).hide().fadeIn(c.fadeDuration||0),ot=!0,e(document).off("mouseup touchend",W).on("mousemove touchmove",N).one("mouseup touchend",k),vt.off("mousemove touchmove",z),c.onSelectStart(a,p())}function W(){e(document).off("mousemove touchmove",M).off("mouseup touchend",W),w(vt.add(xt)),v(u(nt),h(it),u(nt),h(it)),this instanceof e.imgAreaSelect||(c.onSelectChange(a,p()),c.onSelectEnd(a,p()))}function D(t){return"mousedown"==t.type&&1!=t.which||xt.is(":animated")?!1:(ct="touchstart"==t.type,y(),Y=nt=f(t),q=it=l(t),B=F=0,e(document).on({"mousemove touchmove":M,"mouseup touchend":W}),!1)}function E(){x(!1)}function O(){$=!0,T(c=e.extend({classPrefix:"imgareaselect",movable:!0,parent:"body",resizable:!0,resizeMargin:10,onInit:function(){},onSelectStart:function(){},onSelectChange:function(){},onSelectEnd:function(){}},c)),c.show&&(ot=!0,y(),g(),vt.add(xt).hide().fadeIn(c.fadeDuration||0)),setTimeout(function(){c.onInit(a,p())},0)}function T(o){if(o.parent&&(X=e(o.parent)).append(vt).append(xt),e.extend(c,o),y(),null!=o.handles){for(wt.remove(),wt=e([]),ut=o.handles?"corners"==o.handles?4:8:0;ut--;)wt=wt.add(t());wt.addClass(c.classPrefix+"-handle").css({position:"absolute",fontSize:0,zIndex:zt+1||1}),!parseInt(wt.css("width"))>=0&&wt.width(5).height(5)}for(G=c.imageWidth/Q||1,J=c.imageHeight/R||1,null!=o.x1&&(v(o.x1,o.y1,o.x2,o.y2),o.show=!o.hide),o.keys&&(c.keys=e.extend({shift:1,ctrl:"resize"},o.keys)),xt.addClass(c.classPrefix+"-outer"),yt.addClass(c.classPrefix+"-selection"),ut=0;4>ut++;)e(gt[ut-1]).addClass(c.classPrefix+"-border"+ut);vt.append(yt.add(gt)).append(wt),Kt&&((ht=(xt.css("filter")||"").match(/opacity=(\d+)/))&&xt.css("opacity",ht[1]/100),(ht=(gt.css("filter")||"").match(/opacity=(\d+)/))&>.css("opacity",ht[1]/100)),o.hide?w(vt.add(xt)):o.show&&$&&(ot=!0,vt.add(xt).fadeIn(c.fadeDuration||0),x()),tt=(dt=(c.aspectRatio||"").split(/:/))[0]/dt[1],pt.add(xt).off("mousedown touchstart",D),c.disable||c.enable===!1?(vt.off({"mousemove touchmove":z,"mousedown touchstart":A}),e(window).off("resize",E)):((c.enable||c.disable===!1)&&((c.resizable||c.movable)&&vt.on({"mousemove touchmove":z,"mousedown touchstart":A}),e(window).resize(E)),c.persistent||pt.add(xt).on("mousedown touchstart",D)),c.enable=c.disable=void 0}var $,L,j,Q,R,X,Y,q,B,F,G,J,U,V,Z,_,et,tt,ot,nt,it,st,at,ct,rt,dt,ut,ht,ft,lt,mt,pt=e(a),vt=t(),yt=t(),gt=t().add(t()).add(t()).add(t()),xt=t(),wt=e([]),bt={left:0,top:0},St={left:0,top:0},zt=0,kt="absolute",At={x1:0,y1:0,x2:0,y2:0,width:0,height:0},It=navigator.userAgent,Pt=function(e){var t,o,s=c.keys,a=e.keyCode;if(t=isNaN(s.alt)||!e.altKey&&!e.originalEvent.altKey?!isNaN(s.ctrl)&&e.ctrlKey?s.ctrl:!isNaN(s.shift)&&e.shiftKey?s.shift:isNaN(s.arrows)?10:s.arrows:s.alt,"resize"==s.arrows||"resize"==s.shift&&e.shiftKey||"resize"==s.ctrl&&e.ctrlKey||"resize"==s.alt&&(e.altKey||e.originalEvent.altKey)){switch(a){case 37:t=-t;case 39:o=n(nt,st),nt=i(nt,st),st=n(o+t,nt),I();break;case 38:t=-t;case 40:o=n(it,at),it=i(it,at),at=n(o+t,it),I(!0);break;default:return}K()}else switch(nt=i(nt,st),it=i(it,at),a){case 37:C(n(nt-t,L),it);break;case 38:C(nt,n(it-t,j));break;case 39:C(nt+i(t,Q-u(st)),it);break;case 40:C(nt,it+i(t,R-h(at)));break;default:return}return!1};this.remove=function(){T({disable:!0}),vt.add(xt).remove()},this.getOptions=function(){return c},this.setOptions=T,this.getSelection=p,this.setSelection=v,this.cancelSelection=W,this.update=x;var Kt=(/msie ([\w.]+)/i.exec(It)||[])[1],Nt=/webkit/i.test(It)&&!/chrome/i.test(It);for(rt=pt;rt.length;)zt=n(zt,isNaN(rt.css("z-index"))?zt:rt.css("z-index")),c.parent||"fixed"!=rt.css("position")||(kt="fixed"),rt=rt.parent(":not(body)");zt=c.zIndex||zt,e.imgAreaSelect.keyPress=Kt||Nt?"keydown":"keypress",vt.add(xt).hide().css({position:kt,overflow:"hidden",zIndex:zt||"0"}),vt.css({zIndex:zt+2||2}),yt.add(gt).css({position:"absolute",fontSize:0}),a.complete||"complete"==a.readyState||!pt.is("img")?O():pt.one("load",O),!$&&Kt&&Kt>=7&&(a.src=a.src)},e.fn.imgAreaSelect=function(t){return t=t||{},this.each(function(){e(this).data("imgAreaSelect")?t.remove?(e(this).data("imgAreaSelect").remove(),e(this).removeData("imgAreaSelect")):e(this).data("imgAreaSelect").setOptions(t):t.remove||(void 0===t.enable&&void 0===t.disable&&(t.enable=!0),e(this).data("imgAreaSelect",new e.imgAreaSelect(this,t)))}),t.instance?e(this).data("imgAreaSelect"):this}})(jQuery); diff --git a/ext/notes/lib/jquery.imgnotes-1.0.min.css b/ext/notes/lib/jquery.imgnotes-1.0.min.css new file mode 100644 index 00000000..214f3c7a --- /dev/null +++ b/ext/notes/lib/jquery.imgnotes-1.0.min.css @@ -0,0 +1,2 @@ +/** imgnotes jQuery plugin v1.0.0 **/ +p{font-family:"Times New Roman";font-size:14px}.ui-widget-content{font-family:"Times New Roman"}.ui-widget-content a{color:blue}.marker{position:absolute;width:27px;height:40px}.marker-text{position:absolute;top:10%;width:100%;margin:0 0 0 0;z-index:1;font-size:12px;font-weight:700;text-align:center;color:#fff}.pin{position:absolute;width:20px;height:30px}.pin-text{position:absolute;top:10%;width:100%;margin:0 0 0 0;font-size:14px;font-weight:700;text-align:center;color:#000}.tooltip{position:relative;display:inline-block}.tooltip .tooltiptext{visibility:visible;width:180px;background-color:#fff;color:#000;text-align:center;padding:5px 0;border-radius:6px;position:absolute;z-index:1;bottom:7px;left:50%;margin-left:-90px}.tooltip .tooltiptext::after{content:"";position:absolute;top:100%;left:50%;margin-left:-7px;border-width:7px;border-style:solid;border-color:#fff transparent transparent transparent}table.gridtable{font-family:verdana,arial,sans-serif;font-size:11px;color:#333;border-width:1px;border-color:#666;border-collapse:collapse}table.gridtable th{border-width:1px;padding:8px;border-style:solid;border-color:#666;background-color:#dedede}table.gridtable td{border-width:1px;padding:8px;border-style:solid;border-color:#666;background-color:#fff} \ No newline at end of file diff --git a/ext/notes/lib/jquery.imgnotes-1.0.min.js b/ext/notes/lib/jquery.imgnotes-1.0.min.js new file mode 100644 index 00000000..241769f7 --- /dev/null +++ b/ext/notes/lib/jquery.imgnotes-1.0.min.js @@ -0,0 +1,15 @@ +/** + * imgnotes jQuery plugin + * version 1.0 + * + * Copyright (c) 2008 - Dr. Tarique Sani + * + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * @URL http://www.sanisoft.com/blog/2008/05/26/img-notes-jquery-plugin/ + * @Example example.html + * + **/ + +(function(e){function t(){e(".note").hover(function(){e(".note").show();e(this).next(".notep").show();e(this).next(".notep").css("z-index",1e4)},function(){e(".note").show();e(this).next(".notep").hide();e(this).next(".notep").css("z-index",0)})}function n(t){note_left=parseInt(imgOffset.left)+parseInt(t.x1);note_top=parseInt(imgOffset.top)+parseInt(t.y1);note_p_top=note_top+parseInt(t.height)+5;note_area_div=e("
    ").css({left:note_left+"px",top:note_top+"px",width:t.width+"px",height:t.height+"px"});note_text_div=e('
    '+t.note+"
    ").css({left:note_left+"px",top:note_p_top+"px"});e("body").append(note_area_div);e("body").append(note_text_div)}function r(t){if(true!==t){return}notes_icon_left=parseInt(imgOffset.left)+parseInt(imgWidth)-36;notes_icon_top=parseInt(imgOffset.top)+parseInt(imgHieght)-40;notes_icon_div=note_area_div=e("
    ").css({left:notes_icon_left+"px",top:notes_icon_top+"px"});e("body").append(notes_icon_div);e(".notesicon").toggle(function(){e.fn.imgNotes.showAll()},function(){e.fn.imgNotes.hideAll()})}e.fn.imgNotes=function(i){if(undefined==s){var s}if(undefined!=i.notes){s=i.notes}if(i.url){e.ajaxSetup({async:false});e.getJSON(i.url,function(e){s=e})}image=this;imgOffset=e(image).offset();imgHieght=e(image).height();imgWidth=e(image).width();e(s).each(function(){n(this)});e(image).hover(function(){e(".note").show()},function(){e(".note").hide();e(".notep").hide()});t();r(i.isMobile);e(window).resize(function(){e(".note").remove();e(".notep").remove();e(".notesicon").remove();imgOffset=e(image).offset();imgHieght=e(image).height();imgWidth=e(image).width();e(s).each(function(){n(this)});t();r(i.isMobile)})};e.fn.imgNotes.showAll=function(){e(".note").show();e(".notep").show()};e.fn.imgNotes.hideAll=function(){e(".note").hide();e(".notep").hide()}})(jQuery); diff --git a/ext/notes/lib/spacer.gif b/ext/notes/lib/spacer.gif new file mode 100644 index 00000000..e565824a Binary files /dev/null and b/ext/notes/lib/spacer.gif differ diff --git a/ext/notes/main.php b/ext/notes/main.php index a495b006..2b1c7b8c 100644 --- a/ext/notes/main.php +++ b/ext/notes/main.php @@ -9,12 +9,7 @@ class Notes extends Extension /** @var NotesTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void - { - Image::$prop_types["notes"] = ImagePropType::INT; - } - - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $config, $database; @@ -75,7 +70,7 @@ class Notes extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("note")) { @@ -103,48 +98,25 @@ class Notes extends Extension if (!$user->is_anonymous()) { $this->revert_history($noteID, $reviewID); } + $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("note/updated")); break; + case "add_note": + if (!$user->is_anonymous()) { + $this->add_new_note(); + } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + break; case "add_request": if (!$user->is_anonymous()) { $this->add_note_request(); } - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; - case "nuke_requests": - if ($user->can(Permissions::NOTES_ADMIN)) { - $this->nuke_requests(); - } - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/".$_POST["image_id"])); - break; - case "create_note": - $page->set_mode(PageMode::DATA); - if (!$user->is_anonymous()) { - $note_id = $this->add_new_note(); - $page->set_data(json_encode_ex([ - 'status' => 'success', - 'note_id' => $note_id, - ])); - } - break; - case "update_note": - $page->set_mode(PageMode::DATA); - if (!$user->is_anonymous()) { - $this->update_note(); - $page->set_data(json_encode_ex(['status' => 'success'])); - } - break; - case "delete_note": - $page->set_mode(PageMode::DATA); - if ($user->can(Permissions::NOTES_ADMIN)) { - $this->delete_note(); - $page->set_data(json_encode_ex(['status' => 'success'])); - } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); break; case "nuke_notes": if ($user->can(Permissions::NOTES_ADMIN)) { @@ -154,7 +126,28 @@ class Notes extends Extension $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("post/view/".$_POST["image_id"])); break; + case "nuke_requests": + if ($user->can(Permissions::NOTES_ADMIN)) { + $this->nuke_requests(); + } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + break; + case "edit_note": + if (!$user->is_anonymous()) { + $this->update_note(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/" . $_POST["image_id"])); + } + break; + case "delete_note": + if ($user->can(Permissions::NOTES_ADMIN)) { + $this->delete_note(); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$_POST["image_id"])); + } + break; default: $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("note/list")); @@ -167,7 +160,7 @@ class Notes extends Extension /* * HERE WE LOAD THE NOTES IN THE IMAGE */ - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $page, $user; @@ -180,7 +173,7 @@ class Notes extends Extension /* * HERE WE ADD THE BUTTONS ON SIDEBAR */ - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; if (!$user->is_anonymous()) { @@ -197,7 +190,7 @@ class Notes extends Extension /* * HERE WE ADD QUERYLETS TO ADD SEARCH SYSTEM */ - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -220,9 +213,9 @@ class Notes extends Extension } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Notes"; $block->body = $this->theme->get_help_html(); @@ -233,8 +226,6 @@ class Notes extends Extension /** * HERE WE GET ALL NOTES FOR DISPLAYED IMAGE. - * - * @return array */ private function get_notes(int $imageID): array { @@ -245,57 +236,42 @@ class Notes extends Extension FROM notes WHERE enable = :enable AND image_id = :image_id ORDER BY date ASC - ", ['enable' => '1', 'image_id' => $imageID]); + ", ['enable'=>'1', 'image_id'=>$imageID]); } /* * HERE WE ADD A NOTE TO DATABASE */ - private function add_new_note(): int + private function add_new_note() { global $database, $user; - $note = json_decode(file_get_contents_ex('php://input'), true); + $imageID = int_escape($_POST["image_id"]); + $user_id = $user->id; + $noteX1 = int_escape($_POST["note_x1"]); + $noteY1 = int_escape($_POST["note_y1"]); + $noteHeight = int_escape($_POST["note_height"]); + $noteWidth = int_escape($_POST["note_width"]); + $noteText = html_escape($_POST["note_text"]); $database->execute( " INSERT INTO notes (enable, image_id, user_id, user_ip, date, x1, y1, height, width, note) VALUES (:enable, :image_id, :user_id, :user_ip, now(), :x1, :y1, :height, :width, :note)", - [ - 'enable' => 1, - 'image_id' => $note['image_id'], - 'user_id' => $user->id, - 'user_ip' => get_real_ip(), - 'x1' => $note['x1'], - 'y1' => $note['y1'], - 'height' => $note['height'], - 'width' => $note['width'], - 'note' => $note['note'], - ] + ['enable'=>1, 'image_id'=>$imageID, 'user_id'=>$user_id, 'user_ip'=>get_real_ip(), 'x1'=>$noteX1, 'y1'=>$noteY1, 'height'=>$noteHeight, 'width'=>$noteWidth, 'note'=>$noteText] ); $noteID = $database->get_last_insert_id('notes_id_seq'); log_info("notes", "Note added {$noteID} by {$user->name}"); - $database->execute("UPDATE images SET notes=(SELECT COUNT(*) FROM notes WHERE image_id=:id) WHERE id=:id", ['id' => $note['image_id']]); + $database->execute("UPDATE images SET notes=(SELECT COUNT(*) FROM notes WHERE image_id=:id1) WHERE id=:id2", ['id1'=>$imageID, 'id2'=>$imageID]); - $this->add_history( - 1, - $noteID, - $note['image_id'], - $note['x1'], - $note['y1'], - $note['height'], - $note['width'], - $note['note'] - ); - - return $noteID; + $this->add_history(1, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText); } - private function add_note_request(): void + private function add_note_request() { global $database, $user; @@ -306,7 +282,7 @@ class Notes extends Extension " INSERT INTO note_request (image_id, user_id, date) VALUES (:image_id, :user_id, now())", - ['image_id' => $image_id, 'user_id' => $user_id] + ['image_id'=>$image_id, 'user_id'=>$user_id] ); $resultID = $database->get_last_insert_id('note_request_id_seq'); @@ -314,11 +290,19 @@ class Notes extends Extension log_info("notes", "Note requested {$resultID} by {$user->name}"); } - private function update_note(): void + private function update_note() { global $database; - $note = json_decode(file_get_contents_ex('php://input'), true); + $note = [ + "x1" => int_escape($_POST["note_x1"]), + "y1" => int_escape($_POST["note_y1"]), + "height" => int_escape($_POST["note_height"]), + "width" => int_escape($_POST["note_width"]), + "note" => $_POST["note_text"], + "image_id" => int_escape($_POST["image_id"]), + "id" => int_escape($_POST["note_id"]) + ]; // validate parameters if (empty($note['note'])) { @@ -328,69 +312,78 @@ class Notes extends Extension $database->execute(" UPDATE notes SET x1 = :x1, y1 = :y1, height = :height, width = :width, note = :note - WHERE image_id = :image_id AND id = :note_id", $note); + WHERE image_id = :image_id AND id = :id", $note); - $this->add_history(1, $note['note_id'], $note['image_id'], $note['x1'], $note['y1'], $note['height'], $note['width'], $note['note']); + $this->add_history(1, $note['id'], $note['image_id'], $note['x1'], $note['y1'], $note['height'], $note['width'], $note['note']); } - private function delete_note(): void + private function delete_note() { global $user, $database; - $note = json_decode(file_get_contents_ex('php://input'), true); + $imageID = int_escape($_POST["image_id"]); + $noteID = int_escape($_POST["note_id"]); + + // validate parameters + if (is_null($imageID) || !is_numeric($imageID) || is_null($noteID) || !is_numeric($noteID)) { + return; + } + $database->execute(" UPDATE notes SET enable = :enable WHERE image_id = :image_id AND id = :id - ", ['enable' => 0, 'image_id' => $note["image_id"], 'id' => $note["note_id"]]); + ", ['enable'=>0, 'image_id'=>$imageID, 'id'=>$noteID]); - log_info("notes", "Note deleted {$note["note_id"]} by {$user->name}"); + log_info("notes", "Note deleted {$noteID} by {$user->name}"); } - private function nuke_notes(): void + private function nuke_notes() { global $database, $user; $image_id = int_escape($_POST["image_id"]); - $database->execute("DELETE FROM notes WHERE image_id = :image_id", ['image_id' => $image_id]); + $database->execute("DELETE FROM notes WHERE image_id = :image_id", ['image_id'=>$image_id]); log_info("notes", "Notes deleted from {$image_id} by {$user->name}"); } - private function nuke_requests(): void + private function nuke_requests() { global $database, $user; $image_id = int_escape($_POST["image_id"]); - $database->execute("DELETE FROM note_request WHERE image_id = :image_id", ['image_id' => $image_id]); + $database->execute("DELETE FROM note_request WHERE image_id = :image_id", ['image_id'=>$image_id]); log_info("notes", "Requests deleted from {$image_id} by {$user->name}"); } - private function get_notes_list(PageRequestEvent $event): void + private function get_notes_list(PageRequestEvent $event) { global $database, $config; $pageNumber = $event->try_page_num(1); + $notesPerPage = $config->get_int('notesNotesPerPage'); - $totalPages = (int)ceil($database->get_one("SELECT COUNT(DISTINCT image_id) FROM notes") / $notesPerPage); //$result = $database->get_all("SELECT * FROM pool_images WHERE pool_id=:pool_id", ['pool_id'=>$poolID]); - $image_ids = $database->get_col( + $result = $database->execute( " SELECT DISTINCT image_id FROM notes WHERE enable = :enable ORDER BY date DESC LIMIT :limit OFFSET :offset", - ['enable' => 1, 'offset' => $pageNumber * $notesPerPage, 'limit' => $notesPerPage] + ['enable'=>1, 'offset'=>$pageNumber * $notesPerPage, 'limit'=>$notesPerPage] ); + $totalPages = ceil($database->get_one("SELECT COUNT(DISTINCT image_id) FROM notes") / $notesPerPage); + $images = []; - foreach($image_ids as $id) { - $images[] = Image::by_id($id); + while ($row = $result->fetch()) { + $images[] = [Image::by_id($row["image_id"])]; } $this->theme->display_note_list($images, $pageNumber + 1, $totalPages); } - private function get_notes_requests(PageRequestEvent $event): void + private function get_notes_requests(PageRequestEvent $event) { global $config, $database; @@ -398,31 +391,33 @@ class Notes extends Extension $requestsPerPage = $config->get_int('notesRequestsPerPage'); + //$result = $database->get_all("SELECT * FROM pool_images WHERE pool_id=:pool_id", ['pool_id'=>$poolID]); + $result = $database->execute( " SELECT DISTINCT image_id FROM note_request ORDER BY date DESC LIMIT :limit OFFSET :offset", - ["offset" => $pageNumber * $requestsPerPage, "limit" => $requestsPerPage] + ["offset"=>$pageNumber * $requestsPerPage, "limit"=>$requestsPerPage] ); - $totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM note_request") / $requestsPerPage); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_request") / $requestsPerPage); $images = []; while ($row = $result->fetch()) { - $images[] = Image::by_id($row["image_id"]); + $images[] = [Image::by_id($row["image_id"])]; } $this->theme->display_note_requests($images, $pageNumber + 1, $totalPages); } - private function add_history(int $noteEnable, int $noteID, int $imageID, int $noteX1, int $noteY1, int $noteHeight, int $noteWidth, string $noteText): void + private function add_history($noteEnable, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText) { global $user, $database; - $reviewID = $database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = :note_id", ['note_id' => $noteID]); + $reviewID = $database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = :note_id", ['note_id'=>$noteID]); $reviewID = $reviewID + 1; $database->execute( @@ -430,12 +425,12 @@ class Notes extends Extension INSERT INTO note_histories (note_enable, note_id, review_id, image_id, user_id, user_ip, date, x1, y1, height, width, note) VALUES (:note_enable, :note_id, :review_id, :image_id, :user_id, :user_ip, now(), :x1, :y1, :height, :width, :note) ", - ['note_enable' => $noteEnable, 'note_id' => $noteID, 'review_id' => $reviewID, 'image_id' => $imageID, 'user_id' => $user->id, 'user_ip' => get_real_ip(), - 'x1' => $noteX1, 'y1' => $noteY1, 'height' => $noteHeight, 'width' => $noteWidth, 'note' => $noteText] + ['note_enable'=>$noteEnable, 'note_id'=>$noteID, 'review_id'=>$reviewID, 'image_id'=>$imageID, 'user_id'=>$user->id, 'user_ip'=>get_real_ip(), + 'x1'=>$noteX1, 'y1'=>$noteY1, 'height'=>$noteHeight, 'width'=>$noteWidth, 'note'=>$noteText] ); } - private function get_histories(PageRequestEvent $event): void + private function get_histories(PageRequestEvent $event) { global $config, $database; @@ -450,15 +445,15 @@ class Notes extends Extension "INNER JOIN users AS u ". "ON u.id = h.user_id ". "ORDER BY date DESC LIMIT :limit OFFSET :offset", - ['offset' => $pageNumber * $historiesPerPage, 'limit' => $historiesPerPage] + ['offset'=>$pageNumber * $historiesPerPage, 'limit'=>$historiesPerPage] ); - $totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM note_histories") / $historiesPerPage); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_histories") / $historiesPerPage); $this->theme->display_histories($histories, $pageNumber + 1, $totalPages); } - private function get_history(PageRequestEvent $event): void + private function get_history(PageRequestEvent $event) { global $config, $database; @@ -474,10 +469,10 @@ class Notes extends Extension "ON u.id = h.user_id ". "WHERE note_id = :note_id ". "ORDER BY date DESC LIMIT :limit OFFSET :offset", - ['note_id' => $noteID, 'offset' => $pageNumber * $historiesPerPage, 'limit' => $historiesPerPage] + ['note_id'=>$noteID, 'offset'=>$pageNumber * $historiesPerPage, 'limit'=>$historiesPerPage] ); - $totalPages = (int)ceil($database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = :note_id", ['note_id' => $noteID]) / $historiesPerPage); + $totalPages = ceil($database->get_one("SELECT COUNT(*) FROM note_histories WHERE note_id = :note_id", ['note_id'=>$noteID]) / $historiesPerPage); $this->theme->display_history($histories, $pageNumber + 1, $totalPages); } @@ -485,11 +480,11 @@ class Notes extends Extension /** * HERE GO BACK IN HISTORY AND SET THE OLD NOTE. IF WAS REMOVED WE RE-ADD IT. */ - private function revert_history(int $noteID, int $reviewID): void + private function revert_history(int $noteID, int $reviewID) { global $database; - $history = $database->get_row("SELECT * FROM note_histories WHERE note_id = :note_id AND review_id = :review_id", ['note_id' => $noteID, 'review_id' => $reviewID]); + $history = $database->get_row("SELECT * FROM note_histories WHERE note_id = :note_id AND review_id = :review_id", ['note_id'=>$noteID, 'review_id'=>$reviewID]); $noteEnable = $history['note_enable']; $noteID = $history['note_id']; @@ -504,7 +499,7 @@ class Notes extends Extension UPDATE notes SET enable = :enable, x1 = :x1, y1 = :y1, height = :height, width = :width, note = :note WHERE image_id = :image_id AND id = :id - ", ['enable' => 1, 'x1' => $noteX1, 'y1' => $noteY1, 'height' => $noteHeight, 'width' => $noteWidth, 'note' => $noteText, 'image_id' => $imageID, 'id' => $noteID]); + ", ['enable'=>1, 'x1'=>$noteX1, 'y1'=>$noteY1, 'height'=>$noteHeight, 'width'=>$noteWidth, 'note'=>$noteText, 'image_id'=>$imageID, 'id'=>$noteID]); $this->add_history($noteEnable, $noteID, $imageID, $noteX1, $noteY1, $noteHeight, $noteWidth, $noteText); } diff --git a/ext/notes/script.js b/ext/notes/script.js index ea2bb87c..7b5be054 100644 --- a/ext/notes/script.js +++ b/ext/notes/script.js @@ -1,274 +1,81 @@ -let notesContainer = null; -let noteImage = document.getElementById('main_image'); -let noteBeingEdited = null; -let dragStart = null; +/*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ document.addEventListener('DOMContentLoaded', () => { if(window.notes) { - if(noteImage.complete) { - renderNotes(); - } else { - noteImage.addEventListener('load', () => { - renderNotes(); - }); - } + $('#main_image').load(function(){ + $('#main_image').imgNotes({notes: window.notes}); - let resizeObserver = new ResizeObserver(entries => { - renderNotes(); + //Make sure notes are always shown + $('#main_image').off('mouseenter mouseleave'); }); - resizeObserver.observe(noteImage); } + + $('#cancelnote').click(function(){ + $('#main_image').imgAreaSelect({ hide: true }); + $('#noteform').hide(); + }); + + $('#EditCancelNote').click(function() { + $('#main_image').imgAreaSelect({ hide: true }); + $('#noteEditForm').hide(); + }); + + $('#addnote').click(function(){ + $('#noteEditForm').hide(); + $('#main_image').imgAreaSelect({ onSelectChange: showaddnote, x1: 120, y1: 90, x2: 280, y2: 210 }); + return false; + }); + + $('.note').click(function() { + $('#noteform').hide(); + var imgOffset = $('#main_image').offset(); + + var x1 = parseInt(this.style.left) - imgOffset.left; + var y1 = parseInt(this.style.top) - imgOffset.top; + var width = parseInt(this.style.width); + var height = parseInt(this.style.height); + var text = $(this).next('.notep').text().replace(/([^>]?)\\n{2}/g, '$1\\n'); + var id = $(this).next('.notep').next('.noteID').text(); + + $('#main_image').imgAreaSelect({ onSelectChange: showeditnote, x1: x1, y1: y1, x2: x1 + width, y2: y1 + height }); + setEditNoteData(x1, y1, width, height, text, id); + }); }); + +function showaddnote (img, area) { + var imgOffset = $(img).offset(); + var form_left = parseInt(imgOffset.left) + parseInt(area.x1); + var form_top = parseInt(imgOffset.top) + parseInt(area.y1) + parseInt(area.height)+5; -function renderNotes() { - // reset the DOM to empty - if(notesContainer) { - notesContainer.remove(); - } - - // check the image we're adding notes on top of - let br = noteImage.getBoundingClientRect(); - let scale = br.width / noteImage.getAttribute("data-width"); - - // render a container full of notes - notesContainer = document.createElement('div'); - notesContainer.className = 'notes-container'; - notesContainer.style.left = window.scrollX + br.left + 'px'; - notesContainer.style.top = window.scrollY + br.top + 'px'; - notesContainer.style.width = br.width + 'px'; - notesContainer.style.height = br.height + 'px'; - - // render each note - window.notes.forEach(note => { - let noteDiv = document.createElement('div'); - noteDiv.classList.add('note'); - noteDiv.style.left = note.x1 * scale + 'px'; - noteDiv.style.top = note.y1 * scale + 'px'; - noteDiv.style.width = note.width * scale + 'px'; - noteDiv.style.height = note.height * scale + 'px'; - let text = document.createElement('div'); - text.innerText = note.note; - noteDiv.addEventListener('click', (e) => { - noteBeingEdited = note.note_id; - renderNotes(); - }); - noteDiv.appendChild(text); - notesContainer.appendChild(noteDiv); - - // if the current note is being edited, render the editor - if(note.note_id == noteBeingEdited) { - let editor = renderEditor(noteDiv, note); - notesContainer.appendChild(editor); - } - }); - - noteImage.parentNode.appendChild(notesContainer); + $('#noteform').css({ left: form_left + 'px', top: form_top + 'px'}); + $('#noteform').show(); + $('#noteform').css('z-index', 10000); + $('#NoteX1').val(area.x1); + $('#NoteY1').val(area.y1); + $('#NoteHeight').val(area.height); + $('#NoteWidth').val(area.width); } -/** - * - * @param {HTMLElement} noteDiv - * @param {*} note - * @returns - */ -function renderEditor(noteDiv, note) { - // check the image we're adding notes on top of - let br = noteImage.getBoundingClientRect(); - let scale = br.width / noteImage.getAttribute("data-width"); +function showeditnote (img, area) { + var imgOffset = $(img).offset(); + var form_left = parseInt(imgOffset.left) + area.x1; + var form_top = parseInt(imgOffset.top) + area.y2; - // set the note itself into drag & resize mode - // NOTE: to avoid re-rendering the whole DOM every time the mouse - // moves, we directly edit the style of the noteDiv, and then when - // the mouse is released, we update the note object and re-render - noteDiv.classList.add('editing'); - noteDiv.addEventListener('mousedown', (e) => { - dragStart = { - x: e.pageX, - y: e.pageY, - mode: getArea(e.offsetX, e.offsetY, noteDiv.offsetWidth, noteDiv.offsetHeight), - }; - noteDiv.classList.add("dragging"); - }); - noteDiv.addEventListener('mousemove', (e) => { - if(dragStart) { - if(dragStart.mode == "c") { - noteDiv.style.left = (note.x1 * scale) + (e.pageX - dragStart.x) + 'px'; - noteDiv.style.top = (note.y1 * scale) + (e.pageY - dragStart.y) + 'px'; - } - if(dragStart.mode.indexOf("n") >= 0) { - noteDiv.style.top = (note.y1 * scale) + (e.pageY - dragStart.y) + 'px'; - noteDiv.style.height = (note.height * scale) - (e.pageY - dragStart.y) + 'px'; - } - if(dragStart.mode.indexOf("s") >= 0) { - noteDiv.style.height = (note.height * scale) + (e.pageY - dragStart.y) + 'px'; - } - if(dragStart.mode.indexOf("w") >= 0) { - noteDiv.style.left = (note.x1 * scale) + (e.pageX - dragStart.x) + 'px'; - noteDiv.style.width = (note.width * scale) - (e.pageX - dragStart.x) + 'px'; - } - if(dragStart.mode.indexOf("e") >= 0) { - noteDiv.style.width = (note.width * scale) + (e.pageX - dragStart.x) + 'px'; - } - } else { - let area = getArea(e.offsetX, e.offsetY, noteDiv.offsetWidth, noteDiv.offsetHeight); - if(area == "c") { - noteDiv.style.cursor = 'move'; - } else { - noteDiv.style.cursor = area + '-resize'; - } - } - }); - function _commit() { - noteDiv.classList.remove("dragging"); - dragStart = null; - note.x1 = noteDiv.offsetLeft / scale; - note.y1 = noteDiv.offsetTop / scale; - note.width = noteDiv.offsetWidth / scale; - note.height = noteDiv.offsetHeight / scale; - renderNotes(); - } - noteDiv.addEventListener('mouseup', _commit); - noteDiv.addEventListener('mouseleave', _commit); - - // add textarea / save / cancel / delete buttons - let editor = document.createElement('div'); - editor.classList.add('editor'); - editor.style.left = note.x1 * scale + 'px'; - editor.style.top = (note.y1 + note.height) * scale + 'px'; - - let textarea = document.createElement('textarea'); - textarea.value = note.note; - textarea.addEventListener('input', () => { - note.note = textarea.value; - }); - editor.appendChild(textarea); - - let save = document.createElement('button'); - save.innerText = 'Save'; - save.addEventListener('click', () => { - if(note.note_id == null) { - fetch('/note/create_note', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(note) - }).then(response => { - if(response.ok) { - return response.json(); - } else { - throw new Error('Failed to create note'); - } - }).then(data => { - note.note_id = data.note_id; - renderNotes(); - }).catch(error => { - alert(error); - }); - } else { - fetch('/note/update_note', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(note) - }).then(response => { - if(!response.ok) { - throw new Error('Failed to update note'); - } - }).catch(error => { - alert(error); - }); - } - noteBeingEdited = null; - renderNotes(); - }); - editor.appendChild(save); - - let cancel = document.createElement('button'); - cancel.innerText = 'Cancel'; - cancel.addEventListener('click', () => { - noteBeingEdited = null; - if(note.note_id == null) { - // delete the un-saved note - window.notes = window.notes.filter(n => n.note_id != null); - } - renderNotes(); - }); - editor.appendChild(cancel); - - if(window.notes_admin && note.note_id != null) { - let deleteNote = document.createElement('button'); - deleteNote.innerText = 'Delete'; - deleteNote.addEventListener('click', () => { - // TODO: delete note from server - fetch('/note/delete_note', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(note) - }).then(response => { - if(!response.ok) { - throw new Error('Failed to delete note'); - } - }).catch(error => { - alert(error); - }); - noteBeingEdited = null; - window.notes = window.notes.filter(n => n.note_id != note.note_id); - renderNotes(); - }); - editor.appendChild(deleteNote); - } - - return editor; + $('#noteEditForm').css({ left: form_left + 'px', top: form_top + 'px'}); + $('#noteEditForm').show(); + $('#noteEditForm').css('z-index', 10000); + $('#EditNoteX1').val(area.x1); + $('#EditNoteY1').val(area.y1); + $('#EditNoteHeight').val(area.height); + $('#EditNoteWidth').val(area.width); } -function addNewNote() { - if(window.notes.filter(note => note.note_id == null).length > 0) { - alert("Please save all notes before adding a new one."); - return; - } - window.notes.push( - { - x1: 10, - y1: 10, - width: 100, - height: 40, - note: "new note", - note_id: null, - image_id: window.notes_image_id, - } - ); - noteBeingEdited = null; - renderNotes(); -} - -function getArea(x, y, width, height) { - let border = 10; - - if(y < border) { - if(x < border) { - return "nw"; - } else if(x > width - border) { - return "ne"; - } else { - return "n"; - } - } else if(y > height - border) { - if(x < border) { - return "sw"; - } else if(x > width - border) { - return "se"; - } else { - return "s"; - } - } else if(x < border) { - return "w"; - } else if(x > width - border) { - return "e"; - } else { - return "c"; - } +function setEditNoteData(x1, y1, width, height, text, id) { + $('#EditNoteX1').val(x1); + $('#EditNoteY1').val(y1); + $('#EditNoteHeight').val(height); + $('#EditNoteWidth').val(width); + $('#EditNoteNote').text(text); + $('#EditNoteID').val(id); + $('#DeleteNoteNoteID').val(id); } diff --git a/ext/notes/style.css b/ext/notes/style.css index 11265937..ccdb7e97 100644 --- a/ext/notes/style.css +++ b/ext/notes/style.css @@ -1,56 +1,87 @@ -.notes-container { - position: absolute; -} +.note { + display: block; -.notes-container .note { - display: flex; - justify-content: center; - align-items: center; - color: black; background-color: #FFE; border: 1px dashed black; overflow: hidden; position: absolute; + z-index: 0; + + filter:alpha(opacity=50); + -moz-opacity:0.5; + -khtml-opacity: 0.5; opacity: 0.5; - z-index: 1; -} -.notes-container .note.editing { - opacity: 1; - border: 1px dashed red; - z-index: 2; -} -.notes-container .note.editing.dragging { - opacity: 0.5; - z-index: 2; -} -.notes-container .note:hover { - opacity: 1; - z-index: 3; } -.notes-container .editor { - display: grid; - color: black; - background-color: #EFE; - border: 1px dashed blue; +.notep { + display: none; + color: #412a21; + background-color: #fffdef; + border: #412a21 1px solid; + font-size: 8pt; + margin-top: 0; + padding: 2px; position: absolute; - grid-template-columns: 1fr 1fr; - grid-template-areas: - "text text" - "save cancel" - "delete delete"; - z-index: 4; + width: 175px; } -.notes-container .editor TEXTAREA { - grid-area: text; - // resize: none; + +#noteform, #noteEditForm { + display: none; + position: absolute; + width: 250px; } -.notes-container .editor BUTTON[value="Save"] { - grid-area: save; + +#noteform textarea, #noteEditForm textarea { + width: 100%; } -.notes-container .editor BUTTON[value="Cancel"] { - grid-area: cancel; + +/* + * imgAreaSelect default style + */ + +.imgareaselect-border1 { + background: url(border-v.gif) repeat-y left top; } -.notes-container .editor BUTTON[value="Delete"] { - grid-area: delete; + +.imgareaselect-border2 { + background: url(border-h.gif) repeat-x left top; +} + +.imgareaselect-border3 { + background: url(border-v.gif) repeat-y right top; +} + +.imgareaselect-border4 { + background: url(border-h.gif) repeat-x left bottom; +} + +.imgareaselect-border1, .imgareaselect-border2, +.imgareaselect-border3, .imgareaselect-border4 { + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-handle { + background-color: #fff; + border: solid 1px #000; + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-outer { + /*background-color: #000;*/ + filter: alpha(opacity=50); + opacity: 0.5; +} + +.imgareaselect-selection { +} + +/* Makes sure the note block is hidden */ +section#note_system { + height: 0; +} +section#note_system > .blockbody { + padding: 0; + border: 0; } diff --git a/ext/notes/theme.php b/ext/notes/theme.php index 58495d6c..9f6118fd 100644 --- a/ext/notes/theme.php +++ b/ext/notes/theme.php @@ -4,48 +4,46 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\INPUT; - -/** - * @phpstan-type NoteHistory array{image_id:int,note_id:int,review_id:int,user_name:string,note:string,date:string} - * @phpstan-type Note array{id:int,x1:int,y1:int,height:int,width:int,note:string} - */ class NotesTheme extends Themelet { - public function note_button(int $image_id): HTMLElement + public function note_button(int $image_id): string { - return SHM_SIMPLE_FORM("", INPUT(["type" => "button", "value" => "Add Note", "onclick" => "addNewNote()"])); + return ' + +
    + + +
    + '; } - public function request_button(int $image_id): HTMLElement + public function request_button(int $image_id): string { - return SHM_SIMPLE_FORM( - "note/add_request", - INPUT(["type" => "hidden", "name" => "image_id", "value" => $image_id]), - INPUT(["type" => "submit", "value" => "Add Note Request"]), - ); + return make_form(make_link("note/add_request")) . ' + + + + '; } - public function nuke_notes_button(int $image_id): HTMLElement + public function nuke_notes_button(int $image_id): string { - return SHM_SIMPLE_FORM( - "note/nuke_notes", - INPUT(["type" => "hidden", "name" => "image_id", "value" => $image_id]), - INPUT(["type" => "submit", "value" => "Nuke Notes", "onclick" => "return confirm_action('Are you sure?')"]), - ); + return make_form(make_link("note/nuke_notes")) . ' + + + + '; } - public function nuke_requests_button(int $image_id): HTMLElement + public function nuke_requests_button(int $image_id): string { - return SHM_SIMPLE_FORM( - "note/nuke_requests", - INPUT(["type" => "hidden", "name" => "image_id", "value" => $image_id]), - INPUT(["type" => "submit", "value" => "Nuke Requests", "onclick" => "return confirm_action('Are you sure?')"]), - ); + return make_form(make_link("note/nuke_requests")) . ' + + + + '; } public function search_notes_page(Page $page): void { //IN DEVELOPMENT, NOT FULLY WORKING - $html = '
    + $html = '
    '; @@ -56,38 +54,103 @@ class NotesTheme extends Themelet } // check action POST on form - /** - * @param Note[] $recovered_notes - */ public function display_note_system(Page $page, int $image_id, array $recovered_notes, bool $adminOptions): void { + $base_href = get_base_href(); + + $page->add_html_header(""); + $page->add_html_header(""); + $page->add_html_header(""); + $to_json = []; foreach ($recovered_notes as $note) { + $parsedNote = $note["note"]; + $parsedNote = str_replace("\n", "\\n", $parsedNote); + $parsedNote = str_replace("\r", "\\r", $parsedNote); + $to_json[] = [ - 'image_id' => $image_id, 'x1' => $note["x1"], 'y1' => $note["y1"], 'height' => $note["height"], 'width' => $note["width"], - 'note' => $note["note"], + 'note' => $parsedNote, 'note_id' => $note["id"], ]; } - $page->add_html_header(""); + + $html = ""; + + $html .= " +
    + ".make_form(make_link("note/add_note"))." + + + + + + + + + + + + + + +
    + +
    + + +
    +
    + ".make_form(make_link("note/edit_note"))." + + + + + + + + + + + + + + +
    + +
    + "; + + if ($adminOptions) { + $html .= " + ".make_form(make_link("note/delete_note"))." + + + + + + +
    + +"; + } + + $html .= "
    "; + + $page->add_block(new Block(null, $html, "main", 1, 'note_system')); } - /** - * @param array $images - */ - public function display_note_list(array $images, int $pageNumber, int $totalPages): void + + public function display_note_list($images, $pageNumber, $totalPages) { global $page; $pool_images = ''; - foreach ($images as $image) { + foreach ($images as $pair) { + $image = $pair[0]; + $thumb_html = $this->build_thumb_html($image); $pool_images .= ''. @@ -101,16 +164,16 @@ class NotesTheme extends Themelet $page->add_block(new Block("Notes", $pool_images, "main", 20)); } - /** - * @param array $images - */ - public function display_note_requests(array $images, int $pageNumber, int $totalPages): void + public function display_note_requests($images, $pageNumber, $totalPages) { global $page; $pool_images = ''; - foreach ($images as $image) { + foreach ($images as $pair) { + $image = $pair[0]; + $thumb_html = $this->build_thumb_html($image); + $pool_images .= ''. ' '.$thumb_html.''. ''; @@ -122,9 +185,6 @@ class NotesTheme extends Themelet $page->add_block(new Block("Note Requests", $pool_images, "main", 20)); } - /** - * @param NoteHistory[] $histories - */ private function get_history(array $histories): string { global $user; @@ -167,10 +227,7 @@ class NotesTheme extends Themelet return $html; } - /** - * @param NoteHistory[] $histories - */ - public function display_histories(array $histories, int $pageNumber, int $totalPages): void + public function display_histories($histories, $pageNumber, $totalPages) { global $page; @@ -183,10 +240,7 @@ class NotesTheme extends Themelet $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages); } - /** - * @param NoteHistory[] $histories - */ - public function display_history(array $histories, int $pageNumber, int $totalPages): void + public function display_history($histories, $pageNumber, $totalPages) { global $page; diff --git a/ext/numeric_score/info.php b/ext/numeric_score/info.php index 8c16b993..b3d8276a 100644 --- a/ext/numeric_score/info.php +++ b/ext/numeric_score/info.php @@ -14,5 +14,5 @@ class NumericScoreInfo extends ExtensionInfo public array $authors = self::SHISH_AUTHOR; public string $license = self::LICENSE_GPLV2; public string $description = "Allow users to score images"; - public ?string $documentation = "Each registered user may vote a post +1 or -1, the image's score is the sum of all votes."; + public ?string $documentation ="Each registered user may vote a post +1 or -1, the image's score is the sum of all votes."; } diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php index 35c6005c..fbdc4e3e 100644 --- a/ext/numeric_score/main.php +++ b/ext/numeric_score/main.php @@ -33,25 +33,22 @@ class NumericScoreVote public static function score(Image $post): int { global $database; - if ($post['score'] ?? null) { - return $post['score']; + if ($post->score ?? null) { + return $post->score; } return $database->get_one( "SELECT sum(score) FROM numeric_score_votes WHERE image_id=:image_id", - ['image_id' => $post->id] + ['image_id'=>$post->id] ) ?? 0; } - /** - * @return NumericScoreVote[] - */ #[Field(extends: "Post", type: "[NumericScoreVote!]!")] public static function votes(Image $post): array { global $database; $rows = $database->get_all( "SELECT * FROM numeric_score_votes WHERE image_id=:image_id", - ['image_id' => $post->id] + ['image_id'=>$post->id] ); $votes = []; foreach ($rows as $row) { @@ -70,7 +67,7 @@ class NumericScoreVote global $database, $user; return $database->get_one( "SELECT score FROM numeric_score_votes WHERE image_id=:image_id AND user_id=:user_id", - ['image_id' => $post->id, "user_id" => $user->id] + ['image_id'=>$post->id, "user_id"=>$user->id] ) ?? 0; } @@ -107,12 +104,7 @@ class NumericScore extends Extension /** @var NumericScoreTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void - { - Image::$prop_types["numeric_score"] = ImagePropType::INT; - } - - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $user; if ($user->can(Permissions::CREATE_VOTE)) { @@ -120,21 +112,22 @@ class NumericScore extends Extension } } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onUserPageBuilding(UserPageBuildingEvent $event) { global $user; if ($user->can(Permissions::EDIT_OTHER_VOTE)) { $this->theme->get_nuller($event->display_user); } - $n_up = Search::count_images(["upvoted_by={$event->display_user->name}"]); - $link_up = search_link(["upvoted_by={$event->display_user->name}"]); - $n_down = Search::count_images(["downvoted_by={$event->display_user->name}"]); - $link_down = search_link(["downvoted_by={$event->display_user->name}"]); + $u_name = url_escape($event->display_user->name); + $n_up = Image::count_images(["upvoted_by={$event->display_user->name}"]); + $link_up = make_link("post/list/upvoted_by=$u_name/1"); + $n_down = Image::count_images(["downvoted_by={$event->display_user->name}"]); + $link_down = make_link("post/list/downvoted_by=$u_name/1"); $event->add_stats("$n_up Upvotes / $n_down Downvotes"); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $database, $user, $page; @@ -145,7 +138,7 @@ class NumericScore extends Extension FROM numeric_score_votes JOIN users ON numeric_score_votes.user_id=users.id WHERE image_id=:image_id", - ['image_id' => $image_id] + ['image_id'=>$image_id] ); $html = ""; foreach ($x as $vote) { @@ -160,7 +153,7 @@ class NumericScore extends Extension if ($user->can(Permissions::CREATE_VOTE)) { $image_id = int_escape($_POST['image_id']); $score = int_escape($_POST['vote']); - if (($score == -1 || $score == 0 || $score == 1) && $image_id > 0) { + if (($score == -1 || $score == 0 || $score == 1) && $image_id>0) { send_event(new NumericScoreSetEvent($image_id, $user, $score)); } $page->set_mode(PageMode::REDIRECT); @@ -171,11 +164,11 @@ class NumericScore extends Extension $image_id = int_escape($_POST['image_id']); $database->execute( "DELETE FROM numeric_score_votes WHERE image_id=:image_id", - ['image_id' => $image_id] + ['image_id'=>$image_id] ); $database->execute( "UPDATE images SET numeric_score=0 WHERE id=:id", - ['id' => $image_id] + ['id'=>$image_id] ); $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("post/view/$image_id")); @@ -205,25 +198,25 @@ class NumericScore extends Extension $totaldate = $year."/".$month."/".$day; - $sql = "SELECT id FROM images WHERE EXTRACT(YEAR FROM posted) = :year"; + $sql = "SELECT id FROM images + WHERE EXTRACT(YEAR FROM posted) = :year + "; $args = ["limit" => $config->get_int(IndexConfig::IMAGES), "year" => $year]; if ($event->page_matches("popular_by_day")) { - $sql .= " AND EXTRACT(MONTH FROM posted) = :month AND EXTRACT(DAY FROM posted) = :day"; + $sql .= + "AND EXTRACT(MONTH FROM posted) = :month + AND EXTRACT(DAY FROM posted) = :day"; + $args = array_merge($args, ["month" => $month, "day" => $day]); - $current = date("F jS, Y", strtotime_ex($totaldate)). - $name = "day"; - $fmt = "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m\\&\\d\\a\\y\\=d"; + $dte = [$totaldate, date("F jS, Y", (strtotime($totaldate))), "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m\\&\\d\\a\\y\\=d", "day"]; } elseif ($event->page_matches("popular_by_month")) { - $sql .= " AND EXTRACT(MONTH FROM posted) = :month"; + $sql .= "AND EXTRACT(MONTH FROM posted) = :month"; + $args = array_merge($args, ["month" => $month]); - $current = date("F Y", strtotime_ex($totaldate)); - $name = "month"; - $fmt = "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m"; + $dte = [$totaldate, date("F Y", (strtotime($totaldate))), "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m", "month"]; } elseif ($event->page_matches("popular_by_year")) { - $current = "$year"; - $name = "year"; - $fmt = "\\y\\e\\a\\r\=Y"; + $dte = [$totaldate, $year, "\\y\\e\\a\\r\=Y", "year"]; } else { // this should never happen due to the fact that the page event is already matched against earlier. throw new \UnexpectedValueException("Error: Invalid page event."); @@ -232,35 +225,39 @@ class NumericScore extends Extension //filter images by score != 0 + date > limit to max images on one page > order from highest to lowest score - $ids = $database->get_col($sql, $args); - $images = Search::get_images($ids); - $this->theme->view_popular($images, $totaldate, $current, $name, $fmt); + $result = $database->get_col($sql, $args); + $images = []; + foreach ($result as $id) { + $images[] = Image::by_id((int)$id); + } + + $this->theme->view_popular($images, $dte); } } - public function onNumericScoreSet(NumericScoreSetEvent $event): void + public function onNumericScoreSet(NumericScoreSetEvent $event) { global $user; log_debug("numeric_score", "Rated >>{$event->image_id} as {$event->score}", "Rated Post"); $this->add_vote($event->image_id, $user->id, $event->score); } - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { global $database; $database->execute("DELETE FROM numeric_score_votes WHERE image_id=:id", ["id" => $event->image->id]); } - public function onUserDeletion(UserDeletionEvent $event): void + public function onUserDeletion(UserDeletionEvent $event) { $this->delete_votes_by($event->id); } - public function delete_votes_by(int $user_id): void + public function delete_votes_by(int $user_id) { global $database; - $image_ids = $database->get_col("SELECT image_id FROM numeric_score_votes WHERE user_id=:user_id", ['user_id' => $user_id]); + $image_ids = $database->get_col("SELECT image_id FROM numeric_score_votes WHERE user_id=:user_id", ['user_id'=>$user_id]); if (count($image_ids) == 0) { return; @@ -272,7 +269,7 @@ class NumericScore extends Extension $id_list = implode(",", $chunk); $database->execute( "DELETE FROM numeric_score_votes WHERE user_id=:user_id AND image_id IN (".$id_list.")", - ['user_id' => $user_id] + ['user_id'=>$user_id] ); $database->execute(" UPDATE images @@ -288,14 +285,14 @@ class NumericScore extends Extension } } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { - $event->replace('$score', (string)$event->image['numeric_score']); + $event->replace('$score', (string)$event->image->numeric_score); } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Numeric Score"; $block->body = $this->theme->get_help_html(); @@ -303,7 +300,7 @@ class NumericScore extends Extension } } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -323,7 +320,7 @@ class NumericScore extends Extension } $event->add_querylet(new Querylet( "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)", - ["ns_user_id" => $duser->id] + ["ns_user_id"=>$duser->id] )); } elseif (preg_match("/^downvoted_by[=|:](.*)$/i", $event->term, $matches)) { $duser = User::by_name($matches[1]); @@ -334,19 +331,19 @@ class NumericScore extends Extension } $event->add_querylet(new Querylet( "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)", - ["ns_user_id" => $duser->id] + ["ns_user_id"=>$duser->id] )); } elseif (preg_match("/^upvoted_by_id[=|:](\d+)$/i", $event->term, $matches)) { $iid = int_escape($matches[1]); $event->add_querylet(new Querylet( "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=1)", - ["ns_user_id" => $iid] + ["ns_user_id"=>$iid] )); } elseif (preg_match("/^downvoted_by_id[=|:](\d+)$/i", $event->term, $matches)) { $iid = int_escape($matches[1]); $event->add_querylet(new Querylet( "images.id in (SELECT image_id FROM numeric_score_votes WHERE user_id=:ns_user_id AND score=-1)", - ["ns_user_id" => $iid] + ["ns_user_id"=>$iid] )); } elseif (preg_match("/^order[=|:](?:numeric_)?(score)(?:_(desc|asc))?$/i", $event->term, $matches)) { $default_order_for_column = "DESC"; @@ -355,14 +352,14 @@ class NumericScore extends Extension } } - public function onTagTermCheck(TagTermCheckEvent $event): void + public function onTagTermCheck(TagTermCheckEvent $event) { if (preg_match("/^vote[=|:](up|down|remove)$/i", $event->term)) { $event->metatag = true; } } - public function onTagTermParse(TagTermParseEvent $event): void + public function onTagTermParse(TagTermParseEvent $event) { $matches = []; @@ -375,16 +372,16 @@ class NumericScore extends Extension } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "posts") { + if ($event->parent=="posts") { $event->add_nav_link("numeric_score_day", new Link('popular_by_day'), "Popular by Day"); $event->add_nav_link("numeric_score_month", new Link('popular_by_month'), "Popular by Month"); $event->add_nav_link("numeric_score_year", new Link('popular_by_year'), "Popular by Year"); } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -408,7 +405,7 @@ class NumericScore extends Extension } } - private function add_vote(int $image_id, int $user_id, int $score): void + private function add_vote(int $image_id, int $user_id, int $score) { global $database; $database->execute( diff --git a/ext/numeric_score/test.php b/ext/numeric_score/test.php index ab8819ed..44662acc 100644 --- a/ext/numeric_score/test.php +++ b/ext/numeric_score/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class NumericScoreTest extends ShimmiePHPUnitTestCase { - public function testNumericScore(): void + public function testNumericScore() { global $user; diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php index 64e94e54..6cbda2db 100644 --- a/ext/numeric_score/theme.php +++ b/ext/numeric_score/theme.php @@ -6,11 +6,14 @@ namespace Shimmie2; class NumericScoreTheme extends Themelet { - public function get_voter(Image $image): void + public function get_voter(Image $image) { global $user, $page; $i_image_id = $image->id; - $i_score = (int)$image['numeric_score']; + if (is_string($image->numeric_score)) { + $image->numeric_score = (int)$image->numeric_score; + } + $i_score = $image->numeric_score; $html = " Current Score: $i_score @@ -55,7 +58,7 @@ class NumericScoreTheme extends Themelet $page->add_block(new Block("Post Score", $html, "left", 20)); } - public function get_nuller(User $duser): void + public function get_nuller(User $duser) { global $user, $page; $html = " @@ -68,10 +71,7 @@ class NumericScoreTheme extends Themelet $page->add_block(new Block("Votes", $html, "main", 80)); } - /** - * @param Image[] $images - */ - public function view_popular(array $images, string $totaldate, string $current, string $name, string $fmt): void + public function view_popular($images, $dte) { global $page, $config; @@ -80,12 +80,12 @@ class NumericScoreTheme extends Themelet $pop_images .= $this->build_thumb_html($image)."\n"; } - $b_dte = make_link("popular_by_$name", date($fmt, strtotime_ex("-1 $name", strtotime_ex($totaldate)))); - $f_dte = make_link("popular_by_$name", date($fmt, strtotime_ex("+1 $name", strtotime_ex($totaldate)))); + $b_dte = make_link("popular_by_".$dte[3], date($dte[2], (strtotime('-1 '.$dte[3], strtotime($dte[0]))))); + $f_dte = make_link("popular_by_".$dte[3], date($dte[2], (strtotime('+1 '.$dte[3], strtotime($dte[0]))))); $html = "\n". "

    \n". - " « {$current} »\n". + " « {$dte[1]} »\n". "

    \n". "
    \n".$pop_images; diff --git a/ext/ouroboros_api/info.php b/ext/ouroboros_api/info.php index f63ea7d1..4c19d24a 100644 --- a/ext/ouroboros_api/info.php +++ b/ext/ouroboros_api/info.php @@ -10,7 +10,7 @@ class OuroborosAPIInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Ouroboros API"; - public array $authors = ["Diftraku" => "diftraku[at]derpy.me"]; + public array $authors = ["Diftraku"=>"diftraku[at]derpy.me"]; public string $description = "Ouroboros-like API for Shimmie"; public ?string $version = "0.2"; public ?string $documentation = diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php index 4bb79374..07ec46c2 100644 --- a/ext/ouroboros_api/main.php +++ b/ext/ouroboros_api/main.php @@ -22,7 +22,6 @@ class _SafeOuroborosImage * Post Meta */ public ?int $change = null; - /** @var array{n:int,s:int,json_class:string} */ public ?array $created_at = null; public ?int $id = null; public ?int $parent_id = null; @@ -69,18 +68,18 @@ class _SafeOuroborosImage // meta $this->change = intval($img->id); //DaFug is this even supposed to do? ChangeID? // Should be JSON specific, just strip this when converting to XML - $this->created_at = ['n' => 123456789, 's' => strtotime_ex($img->posted), 'json_class' => 'Time']; + $this->created_at = ['n' => 123456789, 's' => strtotime($img->posted), 'json_class' => 'Time']; $this->id = intval($img->id); $this->parent_id = null; - if (Extension::is_enabled(RatingsInfo::KEY) !== false) { + if (Extension::is_enabled(RatingsInfo::KEY)!== false) { // 'u' is not a "valid" rating - if ($img['rating'] == 's' || $img['rating'] == 'q' || $img['rating'] == 'e') { - $this->rating = $img['rating']; + if ($img->rating == 's' || $img->rating == 'q' || $img->rating == 'e') { + $this->rating = $img->rating; } } - if (Extension::is_enabled(NumericScoreInfo::KEY) !== false) { - $this->score = $img['numeric_score']; + if (Extension::is_enabled(NumericScoreInfo::KEY)!== false) { + $this->score = $img->numeric_score; } $this->source = $img->source; @@ -104,8 +103,7 @@ class _SafeOuroborosImage class OuroborosPost extends _SafeOuroborosImage { - /** @var array{tmp_name:string,name:string} */ - public ?array $file = null; + public array $file = []; public bool $is_rating_locked = false; public bool $is_note_locked = false; @@ -113,8 +111,6 @@ class OuroborosPost extends _SafeOuroborosImage * Initialize an OuroborosPost for creation * Mainly just acts as a wrapper and validation layer * @noinspection PhpMissingParentConstructorInspection - * - * @param array $post */ public function __construct(array $post) { @@ -139,19 +135,19 @@ class OuroborosPost extends _SafeOuroborosImage $this->rating = $post['rating']; } if (array_key_exists('source', $post)) { - $this->file_url = filter_var_ex( + $this->file_url = filter_var( urldecode($post['source']), FILTER_SANITIZE_URL ); } if (array_key_exists('sourceurl', $post)) { - $this->source = filter_var_ex( + $this->source = filter_var( urldecode($post['sourceurl']), FILTER_SANITIZE_URL ); } if (array_key_exists('description', $post)) { - $this->description = filter_var_ex( + $this->description = filter_var( $post['description'], FILTER_SANITIZE_STRING ); @@ -188,9 +184,6 @@ class _SafeOuroborosTag public string $name = ''; public int $type = 0; - /** - * @param array{id:int,tag:string,count:int} $tag - */ public function __construct(array $tag) { $this->count = $tag['count']; @@ -241,7 +234,7 @@ class OuroborosAPI extends Extension public const ERROR_POST_CREATE_DUPE = 'Duplicate'; public const OK_POST_CREATE_UPDATE = 'Updated'; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -260,7 +253,7 @@ class OuroborosAPI extends Extension if ($this->match('create')) { // Create if ($user->can(Permissions::CREATE_IMAGE)) { - $md5 = !empty($_REQUEST['md5']) ? filter_var_ex($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null; + $md5 = !empty($_REQUEST['md5']) ? filter_var($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null; $this->postCreate(new OuroborosPost($_REQUEST['post']), $md5); } else { $this->sendResponse(403, 'You cannot create new posts'); @@ -269,17 +262,17 @@ class OuroborosAPI extends Extension throw new SCoreException("update not implemented"); } elseif ($this->match('show')) { // Show - $id = !empty($_REQUEST['id']) ? (int)filter_var_ex($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) : null; + $id = !empty($_REQUEST['id']) ? filter_var($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) : null; $this->postShow($id); } elseif ($this->match('index') || $this->match('list')) { // List $limit = !empty($_REQUEST['limit']) ? intval( - filter_var_ex($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT) + filter_var($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT) ) : 45; $p = !empty($_REQUEST['page']) ? intval( - filter_var_ex($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT) + filter_var($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT) ) : 1; - $tags = !empty($_REQUEST['tags']) ? filter_var_ex($_REQUEST['tags'], FILTER_SANITIZE_STRING) : []; + $tags = !empty($_REQUEST['tags']) ? filter_var($_REQUEST['tags'], FILTER_SANITIZE_STRING) : []; if (is_string($tags)) { $tags = Tag::explode($tags); } @@ -288,23 +281,23 @@ class OuroborosAPI extends Extension } elseif ($event->page_matches('tag')) { if ($this->match('index') || $this->match('list')) { $limit = !empty($_REQUEST['limit']) ? intval( - filter_var_ex($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT) + filter_var($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT) ) : 50; $p = !empty($_REQUEST['page']) ? intval( - filter_var_ex($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT) + filter_var($_REQUEST['page'], FILTER_SANITIZE_NUMBER_INT) ) : 1; - $order = (!empty($_REQUEST['order']) && ($_REQUEST['order'] == 'date' || $_REQUEST['order'] == 'count' || $_REQUEST['order'] == 'name')) ? filter_var_ex( + $order = (!empty($_REQUEST['order']) && ($_REQUEST['order'] == 'date' || $_REQUEST['order'] == 'count' || $_REQUEST['order'] == 'name')) ? filter_var( $_REQUEST['order'], FILTER_SANITIZE_STRING ) : 'date'; $id = !empty($_REQUEST['id']) ? intval( - filter_var_ex($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) + filter_var($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) ) : null; $after_id = !empty($_REQUEST['after_id']) ? intval( - filter_var_ex($_REQUEST['after_id'], FILTER_SANITIZE_NUMBER_INT) + filter_var($_REQUEST['after_id'], FILTER_SANITIZE_NUMBER_INT) ) : null; - $name = !empty($_REQUEST['name']) ? filter_var_ex($_REQUEST['name'], FILTER_SANITIZE_STRING) : ''; - $name_pattern = !empty($_REQUEST['name_pattern']) ? filter_var_ex( + $name = !empty($_REQUEST['name']) ? filter_var($_REQUEST['name'], FILTER_SANITIZE_STRING) : ''; + $name_pattern = !empty($_REQUEST['name_pattern']) ? filter_var( $_REQUEST['name_pattern'], FILTER_SANITIZE_STRING ) : ''; @@ -326,9 +319,9 @@ class OuroborosAPI extends Extension /** * Wrapper for post creation */ - protected function postCreate(OuroborosPost $post, ?string $md5 = ''): void + protected function postCreate(OuroborosPost $post, ?string $md5 = '') { - global $config, $database; + global $config; $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); if (!empty($md5) && !($handler == ImageConfig::COLLISION_MERGE)) { $img = Image::by_hash($md5); @@ -338,24 +331,22 @@ class OuroborosAPI extends Extension } } $meta = []; - $meta['tags'] = Tag::explode($post->tags); + $meta['tags'] = is_array($post->tags) ? $post->tags : Tag::explode($post->tags); $meta['source'] = $post->source; - if (Extension::is_enabled(RatingsInfo::KEY) !== false) { + if (Extension::is_enabled(RatingsInfo::KEY)!== false) { $meta['rating'] = $post->rating; } // Check where we should try for the file - if (empty($post->file) && !empty($post->file_url) && filter_var_ex( + if (empty($post->file) && !empty($post->file_url) && filter_var( $post->file_url, FILTER_VALIDATE_URL ) !== false ) { // Transload from source - $meta['file'] = shm_tempnam('transload_' . $config->get_string(UploadConfig::TRANSLOAD_ENGINE)); + $meta['file'] = tempnam(sys_get_temp_dir(), 'shimmie_transload_' . $config->get_string(UploadConfig::TRANSLOAD_ENGINE)); $meta['filename'] = basename($post->file_url); - try { - fetch_url($post->file_url, $meta['file']); - } catch (FetchException $e) { - $this->sendResponse(500, "Transloading failed: $e"); + if (!fetch_url($post->file_url, $meta['file'])) { + $this->sendResponse(500, 'Transloading failed'); return; } $meta['hash'] = md5_file($meta['file']); @@ -374,7 +365,7 @@ class OuroborosAPI extends Extension if (!is_null($img)) { $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); if ($handler == ImageConfig::COLLISION_MERGE) { - $postTags = Tag::explode($post->tags); + $postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags); $merged = array_merge($postTags, $img->get_tag_array()); send_event(new TagSetEvent($img, $merged)); @@ -392,21 +383,27 @@ class OuroborosAPI extends Extension } $meta['extension'] = pathinfo($meta['filename'], PATHINFO_EXTENSION); try { - $image = $database->with_savepoint(function () use ($meta) { - $dae = send_event(new DataUploadEvent($meta['file'], $meta)); - return $dae->images[0]; - }); - $this->sendResponse(200, make_link('post/view/' . $image->id), true); + send_event(new DataUploadEvent($meta['file'], $meta)); + $image = Image::by_hash($meta['hash']); + if (!is_null($image)) { + $this->sendResponse(200, make_link('post/view/' . $image->id), true); + return; + } else { + // Fail, unsupported file? + $this->sendResponse(500, 'Unknown error'); + return; + } } catch (UploadException $e) { // Cleanup in case shit hit the fan $this->sendResponse(500, $e->getMessage()); + return; } } /** * Wrapper for getting a single post */ - protected function postShow(int $id = null): void + protected function postShow(int $id = null) { if (!is_null($id)) { $post = new _SafeOuroborosImage(Image::by_id($id)); @@ -418,12 +415,12 @@ class OuroborosAPI extends Extension /** * Wrapper for getting a list of posts - * @param string[] $tags + * #param string[] $tags */ - protected function postIndex(int $limit, int $page, array $tags): void + protected function postIndex(int $limit, int $page, array $tags) { $start = ($page - 1) * $limit; - $results = Search::find_images(max($start, 0), min($limit, 100), $tags); + $results = Image::find_images(max($start, 0), min($limit, 100), $tags); $posts = []; foreach ($results as $img) { if (!is_object($img)) { @@ -438,7 +435,7 @@ class OuroborosAPI extends Extension * Tag */ - protected function tagIndex(int $limit, int $page, string $order, int $id, int $after_id, string $name, string $name_pattern): void + protected function tagIndex(int $limit, int $page, string $order, int $id, int $after_id, string $name, string $name_pattern) { global $database, $config; $start = ($page - 1) * $limit; @@ -459,17 +456,20 @@ class OuroborosAPI extends Extension default: $tag_data = $database->get_all( " - SELECT id, tag, count - FROM tags - WHERE count >= :tags_min - ORDER BY count DESC, tag ASC LIMIT :start, :max_items - ", + SELECT id, tag, count + FROM tags + WHERE count >= :tags_min + ORDER BY count DESC, tag ASC LIMIT :start, :max_items + ", ['tags_min' => $config->get_int(TagListConfig::TAGS_MIN), 'start' => $start, 'max_items' => $limit] ); break; } $tags = []; foreach ($tag_data as $tag) { + if (!is_array($tag)) { + continue; + } $tags[] = new _SafeOuroborosTag($tag); } $this->sendData('tag', $tags, $start); @@ -482,7 +482,7 @@ class OuroborosAPI extends Extension /** * Sends a simple {success,reason} message to browser */ - private function sendResponse(int $code = 200, string $reason = '', bool $location = false): void + private function sendResponse(int $code = 200, string $reason = '', bool $location = false) { global $page; if ($code == 200) { @@ -514,7 +514,7 @@ class OuroborosAPI extends Extension $response['location'] = $response['reason']; unset($response['reason']); } - $response = json_encode_ex($response); + $response = json_encode($response); } elseif ($this->type == 'xml') { // Seriously, XML sucks... $xml = new \XMLWriter(); @@ -535,33 +535,32 @@ class OuroborosAPI extends Extension $page->set_data($response); } - /** - * @param list<_SafeOuroborosTag>|list<_SafeOuroborosImage> $data - */ - private function sendData(string $type = '', array $data = [], int $offset = 0): void + private function sendData(string $type = '', array $data = [], int $offset = 0) { global $page; $response = ''; if ($this->type == 'json') { - $response = json_encode_ex($data); + $response = json_encode($data); } elseif ($this->type == 'xml') { $xml = new \XMLWriter(); $xml->openMemory(); $xml->startDocument('1.0', 'utf-8'); - - $xml->startElement($type . 's'); - if ($type == 'post') { - $xml->writeAttribute('count', (string)count($data)); - $xml->writeAttribute('offset', (string)$offset); + if (array_key_exists(0, $data)) { + $xml->startElement($type . 's'); + if ($type == 'post') { + $xml->writeAttribute('count', (string)count($data)); + $xml->writeAttribute('offset', (string)$offset); + } + if ($type == 'tag') { + $xml->writeAttribute('type', 'array'); + } + foreach ($data as $item) { + $this->createItemXML($xml, $type, $item); + } + $xml->endElement(); + } else { + $this->createItemXML($xml, $type, $data); } - if ($type == 'tag') { - $xml->writeAttribute('type', 'array'); - } - foreach ($data as $item) { - $this->createItemXML($xml, $type, $item); - } - $xml->endElement(); - $xml->endDocument(); $response = $xml->outputMemory(true); unset($xml); @@ -569,10 +568,10 @@ class OuroborosAPI extends Extension $page->set_data($response); } - private function createItemXML(\XMLWriter $xml, string $type, _SafeOuroborosTag|_SafeOuroborosImage $item): void + private function createItemXML(\XMLWriter $xml, string $type, $item) { $xml->startElement($type); - foreach (json_decode(json_encode_ex($item)) as $key => $val) { + foreach ($item as $key => $val) { if ($key == 'created_at' && $type == 'post') { $xml->writeAttribute($key, $val['s']); } else { @@ -591,7 +590,7 @@ class OuroborosAPI extends Extension * Currently checks for either user & session in request or cookies * and initializes a global User */ - private function tryAuth(): void + private function tryAuth() { global $config, $user; diff --git a/ext/pm/main.php b/ext/pm/main.php index 4d8bdb4f..1755e839 100644 --- a/ext/pm/main.php +++ b/ext/pm/main.php @@ -70,9 +70,6 @@ class PM $this->is_read = $is_read; } - /** - * @param array $row - */ public static function from_row(array $row): PM { $pm = new PM( @@ -88,9 +85,6 @@ class PM return $pm; } - /** - * @return PM[]|null - */ #[Field(extends: "User", name: "private_messages", type: "[PrivateMessage!]")] public static function get_pms(User $duser): ?array { @@ -149,7 +143,7 @@ class PrivMsg extends Extension /** @var PrivMsgTheme */ protected Themelet $theme; - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -187,29 +181,29 @@ class PrivMsg extends Extension } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { global $user; - if ($event->parent === "user") { + if ($event->parent==="user") { if ($user->can(Permissions::READ_PM)) { $count = $this->count_pms($user); - $h_count = $count > 0 ? SPAN(["class" => 'unread'], "($count)") : ""; + $h_count = $count > 0 ? SPAN(["class"=>'unread'], "($count)") : ""; $event->add_nav_link("pm", new Link('user#private-messages'), emptyHTML("Private Messages", $h_count)); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::READ_PM)) { $count = $this->count_pms($user); - $h_count = $count > 0 ? SPAN(["class" => 'unread'], "($count)") : ""; + $h_count = $count > 0 ? SPAN(["class"=>'unread'], "($count)") : ""; $event->add_link(emptyHTML("Private Messages", $h_count), make_link("user", null, "private-messages")); } } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onUserPageBuilding(UserPageBuildingEvent $event) { global $page, $user; $duser = $event->display_user; @@ -218,13 +212,13 @@ class PrivMsg extends Extension if (!is_null($pms)) { $this->theme->display_pms($page, $pms); } - if ($user->can(Permissions::SEND_PM) && $user->id != $duser->id) { + if ($user->id != $duser->id) { $this->theme->display_composer($page, $user, $duser); } } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $cache, $database, $page, $user; if ($event->page_matches("pm")) { @@ -241,11 +235,7 @@ class PrivMsg extends Extension $database->execute("UPDATE private_message SET is_read=true WHERE id = :id", ["id" => $pm_id]); $cache->delete("pm-count-{$user->id}"); } - $pmo = PM::from_row($pm); - $this->theme->display_message($page, $from_user, $user, $pmo); - if($user->can(Permissions::SEND_PM)) { - $this->theme->display_composer($page, $user, $from_user, "Re: ".$pmo->subject); - } + $this->theme->display_message($page, $from_user, $user, PM::from_row($pm)); } else { $this->theme->display_permission_denied(); } @@ -289,7 +279,7 @@ class PrivMsg extends Extension } } - public function onSendPM(SendPMEvent $event): void + public function onSendPM(SendPMEvent $event) { global $cache, $database; $database->execute( @@ -305,19 +295,20 @@ class PrivMsg extends Extension log_info("pm", "Sent PM to User #{$event->pm->to_id}"); } - private function count_pms(User $user): int + private function count_pms(User $user) { - global $database; + global $cache, $database; - return cache_get_or_set( - "pm-count:{$user->id}", - fn () => $database->get_one(" - SELECT count(*) - FROM private_message - WHERE to_id = :to_id - AND is_read = :is_read - ", ["to_id" => $user->id, "is_read" => false]), - 600 - ); + $count = $cache->get("pm-count:{$user->id}"); + if (is_null($count)) { + $count = $database->get_one(" + SELECT count(*) + FROM private_message + WHERE to_id = :to_id + AND is_read = :is_read + ", ["to_id" => $user->id, "is_read" => false]); + $cache->set("pm-count:{$user->id}", $count, 600); + } + return $count; } } diff --git a/ext/pm/test.php b/ext/pm/test.php index a0a7069b..7ad5595d 100644 --- a/ext/pm/test.php +++ b/ext/pm/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class PrivMsgTest extends ShimmiePHPUnitTestCase { - public function testUserReadOwnMessage(): void + public function testUserReadOwnMessage() { // Send from admin to user $this->log_in_as_admin(); @@ -37,7 +37,7 @@ class PrivMsgTest extends ShimmiePHPUnitTestCase // $this->assert_text("No such PM"); } - public function testAdminReadOtherMessage(): void + public function testAdminReadOtherMessage() { // Send from admin to user $this->log_in_as_admin(); diff --git a/ext/pm/theme.php b/ext/pm/theme.php index 1b9d3924..283acace 100644 --- a/ext/pm/theme.php +++ b/ext/pm/theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class PrivMsgTheme extends Themelet { - /** - * @param PM[] $pms - */ - public function display_pms(Page $page, array $pms): void + public function display_pms(Page $page, $pms) { global $user; @@ -61,7 +58,7 @@ class PrivMsgTheme extends Themelet $page->add_block(new Block("Private Messages", $html, "main", 40, "private-messages")); } - public function display_composer(Page $page, User $from, User $to, string $subject = ""): void + public function display_composer(Page $page, User $from, User $to, $subject="") { global $user; $post_url = make_link("pm/send"); @@ -82,8 +79,9 @@ EOD; $page->add_block(new Block("Write a PM", $html, "main", 50)); } - public function display_message(Page $page, User $from, User $to, PM $pm): void + public function display_message(Page $page, User $from, User $to, PM $pm) { + $this->display_composer($page, $to, $from, "Re: ".$pm->subject); $page->set_title("Private Message"); $page->set_heading(html_escape($pm->subject)); $page->add_block(new NavBlock()); diff --git a/ext/pm_triggers/info.php b/ext/pm_triggers/info.php new file mode 100644 index 00000000..a11d3a5a --- /dev/null +++ b/ext/pm_triggers/info.php @@ -0,0 +1,18 @@ +send( + $event->image->owner_id, + "[System] A post you uploaded has been deleted", + "Post le gone~ (#{$event->image->id}, {$event->image->get_tag_list()})" + ); + } + + private function send($to_id, $subject, $body) + { + global $user; + send_event(new SendPMEvent(new PM( + $user->id, + get_real_ip(), + $to_id, + $subject, + $body + ))); + } +} diff --git a/ext/pools/info.php b/ext/pools/info.php index a817cc67..a99ad94d 100644 --- a/ext/pools/info.php +++ b/ext/pools/info.php @@ -10,7 +10,7 @@ class PoolsInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Pools System"; - public array $authors = ["Sein Kraft" => "mail@seinkraft.info", "jgen" => "jgen.tech@gmail.com", "Daku" => "admin@codeanimu.net"]; + public array $authors = ["Sein Kraft"=>"mail@seinkraft.info", "jgen"=>"jgen.tech@gmail.com", "Daku"=>"admin@codeanimu.net"]; public string $license = self::LICENSE_GPLV2; public string $description = "Allow users to create groups of images and order them."; public ?string $documentation = diff --git a/ext/pools/main.php b/ext/pools/main.php index 2be60771..f25bd1ff 100644 --- a/ext/pools/main.php +++ b/ext/pools/main.php @@ -26,12 +26,8 @@ class PoolCreationException extends SCoreException class PoolAddPostsEvent extends Event { public int $pool_id; - /** @var int[] */ public array $posts = []; - /** - * @param int[] $posts - */ public function __construct(int $pool_id, array $posts) { parent::__construct(); @@ -86,9 +82,6 @@ class Pool public string $date; public int $posts; - /** - * @param array $row - */ public function __construct(array $row) { $this->id = (int)$row['id']; @@ -101,24 +94,10 @@ class Pool $this->posts = (int)$row['posts']; } - /** - * @param array $row - */ public static function makePool(array $row): Pool { return new Pool($row); } - - public static function get_pool_id_by_title(string $poolTitle): ?int - { - global $database; - $row = $database->get_row("SELECT * FROM pools WHERE title=:title", ["title" => $poolTitle]); - if ($row != null) { - return $row['id']; - } else { - return null; - } - } } function _image_to_id(Image $image): int @@ -131,12 +110,10 @@ class Pools extends Extension /** @var PoolsTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; - Image::$prop_types["image_order"] = ImagePropType::INT; - // Set the defaults for the pools extension $config->set_default_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000); $config->set_default_int(PoolsConfig::IMAGES_PER_PAGE, 20); @@ -148,7 +125,7 @@ class Pools extends Extension $config->set_default_bool(PoolsConfig::AUTO_INCREMENT_ORDER, false); } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -158,10 +135,9 @@ class Pools extends Extension id SCORE_AIPK, user_id INTEGER NOT NULL, public BOOLEAN NOT NULL DEFAULT FALSE, - title VARCHAR(255) NOT NULL UNIQUE, + title VARCHAR(255) NOT NULL, description TEXT, date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - lastupdated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, posts INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE "); @@ -188,25 +164,20 @@ class Pools extends Extension log_info("pools", "extension installed"); } + if ($this->get_version("ext_pools_version") < 2) { + $database->execute("ALTER TABLE pools ADD UNIQUE INDEX (title);"); + $database->execute("ALTER TABLE pools ADD lastupdated TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;"); + + $this->set_version("ext_pools_version", 3); // skip 2 + } if ($this->get_version("ext_pools_version") < 4) { $database->standardise_boolean("pools", "public"); $this->set_version("ext_pools_version", 4); } - - if ($this->get_version("ext_pools_version") < 5) { - // earlier versions of the table-creation code added the lastupdated - // column non-deterministically, so let's check if it is there and - // add it if needed. - $cols = $database->raw_db()->describe("pools"); - if(!array_key_exists("lastupdated", $cols)) { - $database->execute("ALTER TABLE pools ADD COLUMN lastupdated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"); - } - $this->set_version("ext_pools_version", 5); - } } // Add a block to the Board Config / Setup - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Pools"); $sb->add_int_option(PoolsConfig::MAX_IMPORT_RESULTS, "Max results on import: "); @@ -219,14 +190,14 @@ class Pools extends Extension //$sb->add_bool_option(PoolsConfig::ADDER_ON_VIEW_IMAGE, "
    Show pool adder on image: "); } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("pool", new Link('pool/list'), "Pools"); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "pool") { + if ($event->parent=="pool") { $event->add_nav_link("pool_list", new Link('pool/list'), "List"); $event->add_nav_link("pool_new", new Link('pool/new'), "Create"); $event->add_nav_link("pool_updated", new Link('pool/updated'), "Changes"); @@ -234,26 +205,25 @@ class Pools extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $database, $page, $user; - if ($event->page_matches("pool/list")) { //index - if (isset($_GET['search']) and $_GET['search'] != null) { - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link('pool/list').'/'.$_GET['search'].'/'.strval($event->try_page_num(1))); - return; + if ($event->page_matches("pool")) { + $pool_id = 0; + $pool = []; + + // Check if we have pool id, since this is most often the case. + if (isset($_POST["pool_id"])) { + $pool_id = int_escape($_POST["pool_id"]); + $pool = $this->get_single_pool($pool_id); } - if (count($event->args) >= 4) { // Assume first 2 args are search and page num - $search = $event->get_arg(0); // Search is based on name comparison instead of tag search - $page_num = $event->try_page_num(1); - } else { - $search = ""; - $page_num = $event->try_page_num(0); - } - $this->list_pools($page, $page_num, $search); - } elseif ($event->page_matches("pool")) { + // What action are we trying to perform? switch ($event->get_arg(0)) { + case "list": //index + $this->list_pools($page, $event->try_page_num(1)); + break; + case "new": // Show form for new pools if (!$user->is_anonymous()) { $this->theme->new_pool_composer($page); @@ -298,9 +268,6 @@ class Pools extends Extension break; case "edit": // Edit the pool (remove images) - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if ($this->have_permission($user, $pool)) { $result = $database->execute("SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order ASC", ["pid" => $pool_id]); $images = []; @@ -315,9 +282,6 @@ class Pools extends Extension break; case "order": // Order the pool (view and change the order of images within the pool) - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if (isset($_POST["order_view"])) { if ($this->have_permission($user, $pool)) { $result = $database->execute( @@ -362,28 +326,28 @@ class Pools extends Extension } break; case "reverse": - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if ($this->have_permission($user, $pool)) { - $database->with_savepoint(function () use ($pool_id) { - global $database; - $result = $database->execute( - "SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order DESC", - ["pid" => $pool_id] - ); - $image_order = 1; + $result = $database->execute( + "SELECT image_id FROM pool_images WHERE pool_id=:pid ORDER BY image_order DESC", + ["pid" => $pool_id] + ); + $image_order = 1; + try { + $database->begin_transaction(); while ($row = $result->fetch()) { $database->execute( " - UPDATE pool_images - SET image_order=:ord - WHERE pool_id = :pid AND image_id = :iid", + UPDATE pool_images + SET image_order=:ord + WHERE pool_id = :pid AND image_id = :iid", ["ord" => $image_order, "pid" => $pool_id, "iid" => (int)$row['image_id']] ); $image_order = $image_order + 1; } - }); + $database->commit(); + } catch (\Exception $e) { + $database->rollback(); + } $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("pool/view/" . $pool_id)); } else { @@ -391,11 +355,8 @@ class Pools extends Extension } break; case "import": - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if ($this->have_permission($user, $pool)) { - $images = Search::find_images( + $images = Image::find_images( limit: $config->get_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000), tags: Tag::explode($_POST["pool_tag"]) ); @@ -406,9 +367,6 @@ class Pools extends Extension break; case "add_posts": - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if ($this->have_permission($user, $pool)) { $image_ids = array_map('intval', $_POST['check']); send_event(new PoolAddPostsEvent($pool_id, $image_ids)); @@ -420,9 +378,6 @@ class Pools extends Extension break; case "remove_posts": - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if ($this->have_permission($user, $pool)) { $images = ""; foreach ($_POST['check'] as $imageID) { @@ -446,12 +401,9 @@ class Pools extends Extension break; case "edit_description": - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if ($this->have_permission($user, $pool)) { $database->execute( - "UPDATE pools SET description=:dsc,lastupdated=CURRENT_TIMESTAMP WHERE id=:pid", + "UPDATE pools SET description=:dsc WHERE id=:pid", ["dsc" => $_POST['description'], "pid" => $pool_id] ); $page->set_mode(PageMode::REDIRECT); @@ -465,9 +417,6 @@ class Pools extends Extension case "nuke": // Completely remove the given pool. // -> Only admins and owners may do this - $pool_id = int_escape($_POST["pool_id"]); - $pool = $this->get_single_pool($pool_id); - if ($user->can(Permissions::POOLS_ADMIN) || $user->id == $pool->user_id) { send_event(new PoolDeletionEvent($pool_id)); $page->set_mode(PageMode::REDIRECT); @@ -480,7 +429,7 @@ class Pools extends Extension } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { $event->add_link("Pools", make_link("pool/list")); } @@ -490,7 +439,7 @@ class Pools extends Extension * image is currently a member of on a side panel, as well as a link * to the Next image in the pool. */ - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $config; @@ -513,7 +462,7 @@ class Pools extends Extension } } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $config, $database, $user; if ($config->get_bool(PoolsConfig::ADDER_ON_VIEW_IMAGE) && !$user->is_anonymous()) { @@ -524,19 +473,19 @@ class Pools extends Extension $pools = $database->get_pairs("SELECT id,title FROM pools WHERE user_id=:id ORDER BY title", ["id" => $user->id]); } if (count($pools) > 0) { - $event->add_part($this->theme->get_adder_html($event->image, $pools)); + $event->add_part((string)$this->theme->get_adder_html($event->image, $pools)); } } } - 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("Pools", $this->theme->get_help_html())); } } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -565,17 +514,16 @@ class Pools extends Extension $poolID = str_replace("_", " ", $matches[1]); $event->add_querylet(new Querylet("images.id IN (SELECT DISTINCT image_id FROM pool_images WHERE pool_id = $poolID)")); } - } - public function onTagTermCheck(TagTermCheckEvent $event): void + public function onTagTermCheck(TagTermCheckEvent $event) { if (preg_match("/^pool[=|:]([^:]*|lastcreated):?([0-9]*)$/i", $event->term)) { $event->metatag = true; } } - public function onTagTermParse(TagTermParseEvent $event): void + public function onTagTermParse(TagTermParseEvent $event) { $matches = []; if (preg_match("/^pool[=|:]([^:]*|lastcreated):?([0-9]*)$/i", $event->term, $matches)) { @@ -592,13 +540,13 @@ class Pools extends Extension } if ($pool && $this->have_permission($user, $pool)) { - $image_order = (int)($matches[2] ?: 0); + $image_order = ($matches[2] ?: 0); $this->add_post($pool->id, $event->image_id, true, $image_order); } } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $database; @@ -609,7 +557,7 @@ class Pools extends Extension $event->add_action("bulk_pool_add_new", "Create Pool", "", "", (string)$this->theme->get_bulk_pool_input($event->search_terms)); } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $user; @@ -659,7 +607,7 @@ class Pools extends Extension ); } - private function list_pools(Page $page, int $pageNumber, ?string $search): void + private function list_pools(Page $page, int $pageNumber) { global $config, $database; @@ -677,26 +625,21 @@ class Pools extends Extension $order_by = "ORDER BY p.posts DESC"; } - $where_clause = "WHERE LOWER(title) like '%%'"; - if ($search != null) { - $where_clause = "WHERE LOWER(title) like '%".strtolower($search)."%'"; - } - $pools = array_map([Pool::class, "makePool"], $database->get_all(" SELECT p.*, u.name as user_name FROM pools AS p INNER JOIN users AS u ON p.user_id = u.id - $where_clause $order_by LIMIT :l OFFSET :o ", ["l" => $poolsPerPage, "o" => $pageNumber * $poolsPerPage])); - $totalPages = (int)ceil((int)$database->get_one("SELECT COUNT(*) FROM pools ".$where_clause) / $poolsPerPage); - $this->theme->list_pools($page, $pools, $search, $pageNumber + 1, $totalPages); + $totalPages = (int)ceil((int)$database->get_one("SELECT COUNT(*) FROM pools") / $poolsPerPage); + + $this->theme->list_pools($page, $pools, $pageNumber + 1, $totalPages); } - public function onPoolCreation(PoolCreationEvent $event): void + public function onPoolCreation(PoolCreationEvent $event) { global $user, $database; @@ -744,7 +687,7 @@ class Pools extends Extension /** * Get all of the pool IDs that an image is in, given an image ID. - * @return int[] + * #return int[] */ private function get_pool_ids(int $imageID): array { @@ -766,7 +709,7 @@ class Pools extends Extension /** * HERE WE ADD CHECKED IMAGES FROM POOL AND UPDATE THE HISTORY */ - public function onPoolAddPosts(PoolAddPostsEvent $event): void + public function onPoolAddPosts(PoolAddPostsEvent $event) { global $database, $user; @@ -775,62 +718,65 @@ class Pools extends Extension return; } - $images = []; + $images = " "; foreach ($event->posts as $post_id) { if ($this->add_post($event->pool_id, $post_id, false)) { - $images[] = $post_id; + $images .= " " . $post_id; } } - if (count($images) > 0) { + if (!strlen($images) == 0) { $count = (int)$database->get_one( "SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid", ["pid" => $event->pool_id] ); - $this->add_history($event->pool_id, 1, implode(" ", $images), $count); + $this->add_history($event->pool_id, 1, $images, $count); } } /** * Gets the previous and next successive images from a pool, given a pool ID and an image ID. * - * @return array{prev:?int,next:?int} Array returning two elements (prev, next) in 1 dimension. Each returns ImageID or NULL if none. + * #return int[] Array returning two elements (prev, next) in 1 dimension. Each returns ImageID or NULL if none. */ - private function get_nav_posts(Pool $pool, int $imageID): array + private function get_nav_posts(Pool $pool, int $imageID): ?array { global $database; + if (empty($imageID)) { + return null; + } + return $database->get_row( " - SELECT ( - SELECT image_id - FROM pool_images - WHERE pool_id = :pid - AND image_order < ( - SELECT image_order - FROM pool_images - WHERE pool_id = :pid - AND image_id = :iid - LIMIT 1 - ) - ORDER BY image_order DESC LIMIT 1 - ) AS prev, - ( - SELECT image_id - FROM pool_images - WHERE pool_id = :pid - AND image_order > ( - SELECT image_order - FROM pool_images - WHERE pool_id = :pid - AND image_id = :iid - LIMIT 1 - ) - ORDER BY image_order ASC LIMIT 1 - ) AS next + SELECT ( + SELECT image_id + FROM pool_images + WHERE pool_id = :pid + AND image_order < ( + SELECT image_order + FROM pool_images + WHERE pool_id = :pid + AND image_id = :iid + LIMIT 1 + ) + ORDER BY image_order DESC LIMIT 1 + ) AS prev, + ( + SELECT image_id + FROM pool_images + WHERE pool_id = :pid + AND image_order > ( + SELECT image_order + FROM pool_images + WHERE pool_id = :pid + AND image_id = :iid + LIMIT 1 + ) + ORDER BY image_order ASC LIMIT 1 + ) AS next - LIMIT 1 - ", + LIMIT 1", ["pid" => $pool->id, "iid" => $imageID] ); } @@ -838,7 +784,7 @@ class Pools extends Extension /** * Retrieve all the images in a pool, given a pool ID. */ - private function get_posts(PageRequestEvent $event, int $poolID): void + private function get_posts(PageRequestEvent $event, int $poolID) { global $config, $user, $database; @@ -891,7 +837,7 @@ class Pools extends Extension /** * HERE WE NUKE ENTIRE POOL. WE REMOVE POOLS AND POSTS FROM REMOVED POOL AND HISTORIES ENTRIES FROM REMOVED POOL. */ - public function onPoolDeletion(PoolDeletionEvent $event): void + public function onPoolDeletion(PoolDeletionEvent $event) { global $user, $database; $poolID = $event->pool_id; @@ -909,7 +855,7 @@ class Pools extends Extension * * $action Action=1 (one) MEANS ADDED, Action=0 (zero) MEANS REMOVED */ - private function add_history(int $poolID, int $action, string $images, int $count): void + private function add_history(int $poolID, int $action, string $images, int $count) { global $user, $database; @@ -921,7 +867,7 @@ class Pools extends Extension ); } - private function get_history(int $pageNumber): void + private function get_history(int $pageNumber) { global $config, $database; @@ -947,7 +893,7 @@ class Pools extends Extension /** * HERE GO BACK IN HISTORY AND ADD OR REMOVE POSTS TO POOL. */ - private function revert_history(int $historyID): void + private function revert_history(int $historyID) { global $database; $status = $database->get_all("SELECT * FROM pool_history WHERE id=:hid", ["hid" => $historyID]); @@ -1033,11 +979,11 @@ class Pools extends Extension return true; } - private function update_count(int $pool_id): void + private function update_count(int $pool_id) { global $database; $database->execute( - "UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid),lastupdated=CURRENT_TIMESTAMP WHERE id=:pid", + "UPDATE pools SET posts=(SELECT COUNT(*) FROM pool_images WHERE pool_id=:pid) WHERE id=:pid", ["pid" => $pool_id] ); } diff --git a/ext/pools/script.js b/ext/pools/script.js index 19b8ea9a..16119d32 100644 --- a/ext/pools/script.js +++ b/ext/pools/script.js @@ -3,7 +3,7 @@ document.addEventListener('DOMContentLoaded', () => { $('#order_pool').change(function(){ var val = $("#order_pool option:selected").val(); - shm_cookie_set("shm_ui-order-pool", val); //FIXME: This won't play nice if COOKIE_PREFIX is not "shm_". + Cookies.set("shm_ui-order-pool", val, {path: '/', expires: 365}); //FIXME: This won't play nice if COOKIE_PREFIX is not "shm_". window.location.href = ''; }); }); diff --git a/ext/pools/test.php b/ext/pools/test.php index c4c878a8..67da7b48 100644 --- a/ext/pools/test.php +++ b/ext/pools/test.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Shimmie2; -use PHPUnit\Framework\Attributes\Depends; - class PoolsTest extends ShimmiePHPUnitTestCase { public function setUp(): void @@ -20,7 +18,7 @@ class PoolsTest extends ShimmiePHPUnitTestCase } } - public function testAnon(): void + public function testAnon() { $this->log_out(); @@ -31,9 +29,6 @@ class PoolsTest extends ShimmiePHPUnitTestCase $this->assert_title("Error"); } - /** - * @return array{0: int, 1: array{0: int, 1: int}} - */ public function testCreate(): array { $this->log_in_as_user(); @@ -55,8 +50,8 @@ class PoolsTest extends ShimmiePHPUnitTestCase return [$pool_id, [$image_id_1, $image_id_2]]; } - #[Depends('testCreate')] - public function testOnViewImage(): void + /** @depends testCreate */ + public function testOnViewImage($args) { [$pool_id, $image_ids] = $this->testCreate(); @@ -69,8 +64,8 @@ class PoolsTest extends ShimmiePHPUnitTestCase $this->assert_text("Pool"); } - #[Depends('testCreate')] - public function testSearch(): void + /** @depends testCreate */ + public function testSearch($args) { [$pool_id, $image_ids] = $this->testCreate(); @@ -81,16 +76,16 @@ class PoolsTest extends ShimmiePHPUnitTestCase $this->assert_text("Pool"); } - #[Depends('testCreate')] - public function testList(): void + /** @depends testCreate */ + public function testList($args) { $this->testCreate(); $this->get_page("pool/list"); $this->assert_text("Pool"); } - #[Depends('testCreate')] - public function testView(): void + /** @depends testCreate */ + public function testView($args) { [$pool_id, $image_ids] = $this->testCreate(); @@ -98,8 +93,8 @@ class PoolsTest extends ShimmiePHPUnitTestCase $this->assert_text("Pool"); } - #[Depends('testCreate')] - public function testHistory(): void + /** @depends testCreate */ + public function testHistory($args) { [$pool_id, $image_ids] = $this->testCreate(); @@ -107,8 +102,8 @@ class PoolsTest extends ShimmiePHPUnitTestCase $this->assert_text("Pool"); } - #[Depends('testCreate')] - public function testImport(): void + /** @depends testCreate */ + public function testImport($args) { [$pool_id, $image_ids] = $this->testCreate(); @@ -119,11 +114,8 @@ class PoolsTest extends ShimmiePHPUnitTestCase $this->assert_text("Pool"); } - /** - * @return array{0: int, 1: array{0: int, 1:int}} - */ - #[Depends('testCreate')] - public function testRemovePosts(): array + /** @depends testCreate */ + public function testRemovePosts($args): array { [$pool_id, $image_ids] = $this->testCreate(); @@ -136,10 +128,10 @@ class PoolsTest extends ShimmiePHPUnitTestCase return [$pool_id, $image_ids]; } - #[Depends('testRemovePosts')] - public function testAddPosts(): void + /** @depends testRemovePosts */ + public function testAddPosts($args) { - [$pool_id, $image_ids] = $this->testRemovePosts(); + [$pool_id, $image_ids] = $this->testRemovePosts(null); $page = $this->post_page("pool/add_posts", [ "pool_id" => $pool_id, @@ -148,11 +140,8 @@ class PoolsTest extends ShimmiePHPUnitTestCase $this->assertEquals(PageMode::REDIRECT, $page->mode); } - /** - * @return array{0: int, 1: array{0: int, 1:int}} - */ - #[Depends('testCreate')] - public function testEditDescription(): array + /** @depends testCreate */ + public function testEditDescription($args): array { [$pool_id, $image_ids] = $this->testCreate(); @@ -165,7 +154,7 @@ class PoolsTest extends ShimmiePHPUnitTestCase return [$pool_id, $image_ids]; } - public function testNuke(): void + public function testNuke() { $this->log_in_as_user(); $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); diff --git a/ext/pools/theme.php b/ext/pools/theme.php index 551e533c..cbe04adc 100644 --- a/ext/pools/theme.php +++ b/ext/pools/theme.php @@ -10,18 +10,13 @@ use function MicroHTML\emptyHTML; use function MicroHTML\rawHTML; use function MicroHTML\{A,BR,DIV,INPUT,P,SCRIPT,SPAN,TABLE,TBODY,TD,TEXTAREA,TH,THEAD,TR}; -/** - * @phpstan-type PoolHistory array{id:int,pool_id:int,title:string,user_name:string,action:string,images:string,count:int,date:string} - */ class PoolsTheme extends Themelet { /** * Adds a block to the panel with information on the pool(s) the image is in. * $navIDs = Multidimensional array containing pool id, info & nav IDs. - * - * @param array $navIDs */ - public function pool_info(array $navIDs): void + public function pool_info(array $navIDs) { global $page; @@ -30,7 +25,7 @@ class PoolsTheme extends Themelet foreach ($navIDs as $poolID => $poolInfo) { $div = DIV(SHM_A("pool/view/" . $poolID, $poolInfo["info"]->title)); - if(!empty($poolInfo["nav"])) { + if (!empty($poolInfo["nav"])) { if (!empty($poolInfo["nav"]["prev"])) { $div->appendChild(SHM_A("post/view/" . $poolInfo["nav"]["prev"], "Prev", class: "pools_prev_img")); } @@ -47,25 +42,20 @@ class PoolsTheme extends Themelet } } - /** - * @param array $pools - */ public function get_adder_html(Image $image, array $pools): HTMLElement { return SHM_SIMPLE_FORM( "pool/add_post", SHM_SELECT("pool_id", $pools), - INPUT(["type" => "hidden", "name" => "image_id", "value" => $image->id]), + INPUT(["type"=>"hidden", "name"=>"image_id", "value"=>$image->id]), SHM_SUBMIT("Add Post to Pool") ); } /** * HERE WE SHOWS THE LIST OF POOLS. - * - * @param Pool[] $pools */ - public function list_pools(Page $page, array $pools, string $search, int $pageNumber, int $totalPages): void + public function list_pools(Page $page, array $pools, int $pageNumber, int $totalPages) { // Build up the list of pools. $pool_rows = []; @@ -74,7 +64,7 @@ class PoolsTheme extends Themelet $user_link = SHM_A("user/" . url_escape($pool->user_name), $pool->user_name); $pool_rows[] = TR( - TD(["class" => "left"], $pool_link), + TD(["class"=>"left"], $pool_link), TD($user_link), TD($pool->posts), TD($pool->public ? "Yes" : "No") @@ -82,43 +72,40 @@ class PoolsTheme extends Themelet } $table = TABLE( - ["id" => "poolsList", "class" => "zebra"], + ["id"=>"poolsList", "class"=>"zebra"], THEAD(TR(TH("Name"), TH("Creator"), TH("Posts"), TH("Public"))), TBODY(...$pool_rows) ); $order_arr = ['created' => 'Recently created', 'updated' => 'Last updated', 'name' => 'Name', 'count' => 'Post Count']; $order_selected = $page->get_cookie('ui-order-pool'); - $order_sel = SHM_SELECT("order_pool", $order_arr, selected_options: [$order_selected], attrs: ["id" => "order_pool"]); + $order_sel = SHM_SELECT("order_pool", $order_arr, selected_options: [$order_selected], attrs: ["id"=>"order_pool"]); $this->display_top(null, "Pools"); $page->add_block(new Block("Order By", $order_sel, "left", 15)); $page->add_block(new Block("Pools", $table, position: 10)); - if ($search != "" and !str_starts_with($search, '/')) { - $search = '/'.$search; - } - $this->display_paginator($page, "pool/list".$search, null, $pageNumber, $totalPages); + $this->display_paginator($page, "pool/list", null, $pageNumber, $totalPages); } /* * HERE WE DISPLAY THE NEW POOL COMPOSER */ - public function new_pool_composer(Page $page): void + public function new_pool_composer(Page $page) { $form = SHM_SIMPLE_FORM("pool/create", TABLE( - TR(TD("Title:"), TD(INPUT(["type" => "text", "name" => "title"]))), - TR(TD("Public?:"), TD(INPUT(["type" => "checkbox", "name" => "public", "value" => "Y", "checked" => "checked"]))), - TR(TD("Description:"), TD(TEXTAREA(["name" => "description"]))), - TR(TD(["colspan" => "2"], SHM_SUBMIT("Create"))) + TR(TD("Title:"), TD(INPUT(["type"=>"text", "name"=>"title"]))), + TR(TD("Public?:"), TD(INPUT(["type"=>"checkbox", "name"=>"public", "value"=>"Y", "checked"=>"checked"]))), + TR(TD("Description:"), TD(TEXTAREA(["name"=>"description"]))), + TR(TD(["colspan"=>"2"], SHM_SUBMIT("Create"))) )); $this->display_top(null, "Create Pool"); $page->add_block(new Block("Create Pool", $form, position: 20)); } - private function display_top(?Pool $pool, string $heading, bool $check_all = false): void + private function display_top(?Pool $pool, string $heading, bool $check_all = false) { global $page, $user; @@ -133,15 +120,8 @@ class PoolsTheme extends Themelet SHM_A("pool/updated", "Pool Changes") ); - $search = "
    - - - - "; - $page->add_block(new NavBlock()); $page->add_block(new Block("Pool Navigation", $poolnav, "left", 10)); - $page->add_block(new Block("Search", $search, "left", 10)); if (!is_null($pool)) { if ($pool->public || $user->can(Permissions::POOLS_ADMIN)) {// IF THE POOL IS PUBLIC OR IS ADMIN SHOW EDIT PANEL @@ -156,21 +136,19 @@ class PoolsTheme extends Themelet /** * HERE WE DISPLAY THE POOL WITH TITLE DESCRIPTION AND IMAGES WITH PAGINATION. - * - * @param Image[] $images */ - public function view_pool(Pool $pool, array $images, int $pageNumber, int $totalPages): void + public function view_pool(Pool $pool, array $images, int $pageNumber, int $totalPages) { global $page; $this->display_top($pool, "Pool: " . html_escape($pool->title)); - $image_list = DIV(["class" => "shm-image-list"]); + $pool_images = emptyHTML(); foreach ($images as $image) { - $image_list->appendChild($this->build_thumb_html($image)); + $pool_images->appendChild($this->build_thumb_html($image)); } - $page->add_block(new Block("Viewing Posts", $image_list, "main", 30)); + $page->add_block(new Block("Viewing Posts", $pool_images, "main", 30)); $this->display_paginator($page, "pool/view/" . $pool->id, null, $pageNumber, $totalPages); } @@ -178,13 +156,13 @@ class PoolsTheme extends Themelet /** * HERE WE DISPLAY THE POOL OPTIONS ON SIDEBAR BUT WE HIDE REMOVE OPTION IF THE USER IS NOT THE OWNER OR ADMIN. */ - public function sidebar_options(Page $page, Pool $pool, bool $check_all): void + public function sidebar_options(Page $page, Pool $pool, bool $check_all) { global $user; // This could become a SHM_INPUT function that also accepts 'type' and other attributes. - $_hidden = function (string $name, $value) { - return INPUT(["type" => "hidden", "name" => $name, "value" => $value]); + $_hidden=function (string $name, $value) { + return INPUT(["type"=>"hidden", "name"=>$name, "value"=>$value]); }; $_input_id = $_hidden("pool_id", $pool->id); @@ -192,38 +170,38 @@ class PoolsTheme extends Themelet $editor = emptyHTML( SHM_SIMPLE_FORM( "pool/import", - INPUT(["type" => "text", "name" => "pool_tag", "id" => "edit_pool_tag", "placeholder" => "Please enter a tag"]), + INPUT(["type"=>"text", "name"=>"pool_tag", "id"=>"edit_pool_tag", "placeholder"=>"Please enter a tag"]), $_input_id, - SHM_SUBMIT("Import", ["name" => "edit", "id" => "edit_pool_import_btn"]) + SHM_SUBMIT("Import", ["name"=>"edit", "id"=>"edit_pool_import_btn"]) ), SHM_SIMPLE_FORM( "pool/edit", $_hidden("edit_pool", "yes"), $_input_id, - SHM_SUBMIT("Edit Pool", ["name" => "edit", "id" => "edit_pool_btn"]), + SHM_SUBMIT("Edit Pool", ["name"=>"edit", "id"=>"edit_pool_btn"]), ), SHM_SIMPLE_FORM( "pool/order", $_hidden("order_view", "yes"), $_input_id, - SHM_SUBMIT("Order Pool", ["name" => "edit", "id" => "edit_pool_order_btn"]) + SHM_SUBMIT("Order Pool", ["name"=>"edit", "id"=>"edit_pool_order_btn"]) ), SHM_SIMPLE_FORM( "pool/reverse", $_hidden("reverse_view", "yes"), $_input_id, - SHM_SUBMIT("Reverse Order", ["name" => "edit", "id" => "reverse_pool_order_btn"]) + SHM_SUBMIT("Reverse Order", ["name"=>"edit", "id"=>"reverse_pool_order_btn"]) ), SHM_SIMPLE_FORM( - "post/list/pool_id=" . $pool->id . "/1", - SHM_SUBMIT("Post/List View", ["name" => "edit", "id" => $pool->id]) + "pool/list/pool_id%3A" . $pool->id . "/1", + SHM_SUBMIT("Post/List View", ["name"=>"edit", "id"=>"postlist_pool_btn"]) ) ); if ($user->id == $pool->user_id || $user->can(Permissions::POOLS_ADMIN)) { $editor->appendChild( SCRIPT( - ["type" => "text/javascript"], + ["type"=>"text/javascript"], rawHTML("") ), - INPUT(["type" => "button", "name" => "CheckAll", "value" => "Check All", "onclick" => "setAll(true)"]), - INPUT(["type" => "button", "name" => "UnCheckAll", "value" => "Uncheck All", "onclick" => "setAll(false)"]) + INPUT(["type"=>"button", "name"=>"CheckAll", "value"=>"Check All", "onclick"=>"setAll(true)"]), + INPUT(["type"=>"button", "name"=>"UnCheckAll", "value"=>"Uncheck All", "onclick"=>"setAll(false)"]) ); } @@ -258,16 +236,14 @@ class PoolsTheme extends Themelet /** * HERE WE DISPLAY THE RESULT OF THE SEARCH ON IMPORT. - * - * @param Image[] $images */ - public function pool_result(Page $page, array $images, Pool $pool): void + public function pool_result(Page $page, array $images, Pool $pool) { $this->display_top($pool, "Importing Posts", true); $import = emptyHTML( SCRIPT( - ["type" => "text/javascript"], + ["type"=>"text/javascript"], rawHTML(" function confirm_action() { return confirm('Are you sure you want to add selected posts to this pool?'); @@ -276,18 +252,16 @@ class PoolsTheme extends Themelet ); $form = SHM_FORM("pool/add_posts", name: "checks"); - $image_list = DIV(["class" => "shm-image-list"]); foreach ($images as $image) { - $image_list->appendChild( - SPAN(["class" => "thumb"], $this->build_thumb_html($image), BR(), INPUT(["type" => "checkbox", "name" => "check[]", "value" => $image->id])), + $form->appendChild( + SPAN(["class"=>"thumb"], $this->build_thumb_html($image), BR(), INPUT(["type"=>"checkbox", "name"=>"check[]", "value"=>$image->id])), ); } - $form->appendChild($image_list); $form->appendChild( BR(), - SHM_SUBMIT("Add Selected", ["name" => "edit", "id" => "edit_pool_add_btn", "onclick" => "return confirm_action()"]), - INPUT(["type" => "hidden", "name" => "pool_id", "value" => $pool->id]) + SHM_SUBMIT("Add Selected", ["name"=>"edit", "id"=>"edit_pool_add_btn", "onclick"=>"return confirm_action()"]), + INPUT(["type"=>"hidden", "name"=>"pool_id", "value"=>$pool->id]) ); $import->appendChild($form); @@ -299,28 +273,24 @@ class PoolsTheme extends Themelet /** * HERE WE DISPLAY THE POOL ORDERER. * WE LIST ALL IMAGES ON POOL WITHOUT PAGINATION AND WITH A TEXT INPUT TO SET A NUMBER AND CHANGE THE ORDER - * - * @param Image[] $images */ - public function edit_order(Page $page, Pool $pool, array $images): void + public function edit_order(Page $page, Pool $pool, array $images) { $this->display_top($pool, "Sorting Pool"); $form = SHM_FORM("pool/order", name: "checks"); - $image_list = DIV(["class" => "shm-image-list"]); - foreach ($images as $i => $image) { - $image_list->appendChild(SPAN( - ["class" => "thumb"], + foreach ($images as $i=>$image) { + $form->appendChild(SPAN( + ["class"=>"thumb"], $this->build_thumb_html($image), - INPUT(["type" => "number", "name" => "imgs[$i][]", "value" => $image['image_order'], "style" => "max-width: 50px;"]), - INPUT(["type" => "hidden", "name" => "imgs[$i][]", "value" => $image->id]) + INPUT(["type"=>"number", "name"=>"imgs[$i][]", "value"=>$image->image_order, "style"=>"max-width: 50px;"]), + INPUT(["type"=>"hidden", "name"=>"imgs[$i][]", "value"=>$image->id]) )); } - $form->appendChild($image_list); $form->appendChild( - INPUT(["type" => "hidden", "name" => "pool_id", "value" => $pool->id]), - SHM_SUBMIT("Order", ["name" => "edit", "id" => "edit_pool_order"]) + INPUT(["type"=>"hidden", "name"=>"pool_id", "value"=>$pool->id]), + SHM_SUBMIT("Order", ["name"=>"edit", "id"=>"edit_pool_order"]) ); $page->add_block(new Block("Sorting Posts", $form, position: 30)); @@ -331,36 +301,32 @@ class PoolsTheme extends Themelet * * WE LIST ALL IMAGES ON POOL WITHOUT PAGINATION AND WITH * A CHECKBOX TO SELECT WHICH IMAGE WE WANT TO REMOVE - * - * @param Image[] $images */ - public function edit_pool(Page $page, Pool $pool, array $images): void + public function edit_pool(Page $page, Pool $pool, array $images) { - $_input_id = INPUT(["type" => "hidden", "name" => "pool_id", "value" => $pool->id]); + $_input_id = INPUT(["type"=>"hidden", "name"=>"pool_id", "value"=>$pool->id]); $desc_form = SHM_SIMPLE_FORM( "pool/edit/description", - TEXTAREA(["name" => "description"], $pool->description), + TEXTAREA(["name"=>"description"], $pool->description), BR(), $_input_id, SHM_SUBMIT("Change Description") ); $images_form = SHM_FORM("pool/remove_posts", name: "checks"); - $image_list = DIV(["class" => "shm-image-list"]); foreach ($images as $image) { - $image_list->appendChild(SPAN( - ["class" => "thumb"], + $images_form->appendChild(SPAN( + ["class"=>"thumb"], $this->build_thumb_html($image), - INPUT(["type" => "checkbox", "name" => "check[]", "value" => $image->id]) + INPUT(["type"=>"checkbox", "name"=>"check[]", "value"=>$image->id]) )); } - $images_form->appendChild($image_list); $images_form->appendChild( BR(), $_input_id, - SHM_SUBMIT("Remove Selected", ["name" => "edit", "id" => "edit_pool_remove_sel"]) + SHM_SUBMIT("Remove Selected", ["name"=>"edit", "id"=>"edit_pool_remove_sel"]) ); $pool->description = ""; //This is a rough fix to avoid showing the description twice. @@ -371,15 +337,13 @@ class PoolsTheme extends Themelet /** * HERE WE DISPLAY THE HISTORY LIST. - * - * @param PoolHistory[] $histories */ - public function show_history(array $histories, int $pageNumber, int $totalPages): void + public function show_history(array $histories, int $pageNumber, int $totalPages) { global $page; $table = TABLE( - ["id" => "poolsList", "class" => "zebra"], + ["id"=>"poolsList", "class"=>"zebra"], THEAD(TR(TH("Pool"), TH("Post Count"), TH("Changes"), TH("Updater"), TH("Date"), TH("Action"))) ); @@ -406,7 +370,7 @@ class PoolsTheme extends Themelet } $body[] = TR( - TD(["class" => "left"], $pool_link), + TD(["class"=>"left"], $pool_link), TD($history["count"]), TD($image_links), TD($user_link), @@ -423,26 +387,20 @@ class PoolsTheme extends Themelet $this->display_paginator($page, "pool/updated", null, $pageNumber, $totalPages); } - /** - * @param array $options - */ public function get_bulk_pool_selector(array $options): HTMLElement { return SHM_SELECT("bulk_pool_select", $options, required: true, empty_option: true); } - /** - * @param string[] $search_terms - */ public function get_bulk_pool_input(array $search_terms): HTMLElement { return INPUT( [ - "type" => "text", - "name" => "bulk_pool_new", - "placeholder" => "New Pool", - "required" => "", - "value" => Tag::implode($search_terms) + "type"=>"text", + "name"=>"bulk_pool_new", + "placeholder"=>"New Pool", + "required"=>"", + "value"=>implode(" ", $search_terms) ] ); } diff --git a/ext/post_peek/info.php b/ext/post_peek/info.php index 285b73a3..fa08270f 100644 --- a/ext/post_peek/info.php +++ b/ext/post_peek/info.php @@ -11,7 +11,7 @@ class PostPeekInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Post Peek"; public string $url = self::SHIMMIE_URL; - public array $authors = ["Matthew Barbour" => "matthew@darkholme.net"]; + public array $authors = ["Matthew Barbour"]; public string $license = self::LICENSE_WTFPL; public string $description = "Peek at posts"; } diff --git a/ext/post_titles/info.php b/ext/post_titles/info.php index a217ff87..1c1c87b0 100644 --- a/ext/post_titles/info.php +++ b/ext/post_titles/info.php @@ -10,7 +10,7 @@ class PostTitlesInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Post Titles"; - 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 = "Add titles to media posts"; } diff --git a/ext/post_titles/main.php b/ext/post_titles/main.php index d8a66fc3..ff3ef241 100644 --- a/ext/post_titles/main.php +++ b/ext/post_titles/main.php @@ -17,16 +17,15 @@ class PostTitles extends Extension return 60; } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool(PostTitlesConfig::DEFAULT_TO_FILENAME, false); $config->set_default_bool(PostTitlesConfig::SHOW_IN_WINDOW_TITLE, false); - Image::$prop_types["title"] = ImagePropType::STRING; } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -36,7 +35,7 @@ class PostTitles extends Extension } } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $config, $page; @@ -45,14 +44,14 @@ class PostTitles extends Extension } } - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { global $user; $event->add_part($this->theme->get_title_set_html(self::get_title($event->image), $user->can(Permissions::EDIT_IMAGE_TITLE)), 10); } - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { global $user; @@ -62,12 +61,12 @@ class PostTitles extends Extension } } - public function onPostTitleSet(PostTitleSetEvent $event): void + public function onPostTitleSet(PostTitleSetEvent $event) { $this->set_title($event->image->id, $event->title); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Post Titles"); $sb->start_table(); @@ -76,22 +75,21 @@ class PostTitles extends Extension $sb->end_table(); } - public function onBulkExport(BulkExportEvent $event): void + public function onBulkExport(BulkExportEvent $event) { - $event->fields["title"] = $event->image['title']; + $event->fields["title"] = $event->image->title; } - - public function onBulkImport(BulkImportEvent $event): void + public function onBulkImport(BulkImportEvent $event) { - if (array_key_exists("title", $event->fields) && $event->fields['title'] != null) { + if (array_key_exists("title", $event->fields) && $event->fields['title']!=null) { $this->set_title($event->image->id, $event->fields['title']); } } - private function set_title(int $image_id, string $title): void + private function set_title(int $image_id, string $title) { global $database; - $database->execute("UPDATE images SET title=:title WHERE id=:id", ['title' => $title, 'id' => $image_id]); + $database->execute("UPDATE images SET title=:title WHERE id=:id", ['title'=>$title, 'id'=>$image_id]); log_info("post_titles", "Title for >>{$image_id} set to: ".$title); } @@ -99,7 +97,7 @@ class PostTitles extends Extension { global $config; - $title = $image['title'] ?? ""; + $title = $image->title??""; if (empty($title) && $config->get_bool(PostTitlesConfig::DEFAULT_TO_FILENAME)) { $info = pathinfo($image->filename); if (array_key_exists("extension", $info)) { diff --git a/ext/post_titles/theme.php b/ext/post_titles/theme.php index f468fea2..aa3c9d35 100644 --- a/ext/post_titles/theme.php +++ b/ext/post_titles/theme.php @@ -4,18 +4,23 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\{INPUT}; - class PostTitlesTheme extends Themelet { - public function get_title_set_html(string $title, bool $can_set): HTMLElement + public function get_title_set_html(string $title, bool $can_set): string { - return SHM_POST_INFO( - "Title", - $title, - $can_set ? INPUT(["type" => "text", "name" => "post_title", "value" => $title]) : null - ); + $html = " +
    + + + + "; + return $html; } } diff --git a/ext/private_image/info.php b/ext/private_image/info.php index 3c34e446..2196f19e 100644 --- a/ext/private_image/info.php +++ b/ext/private_image/info.php @@ -10,7 +10,7 @@ class PrivateImageInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Private Post"; - 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 = "Allows users to mark images as private, which prevents other users from seeing them."; } diff --git a/ext/private_image/main.php b/ext/private_image/main.php index 81d01836..88f06d35 100644 --- a/ext/private_image/main.php +++ b/ext/private_image/main.php @@ -16,18 +16,18 @@ class PrivateImage extends Extension /** @var PrivateImageTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { - Image::$prop_types["private"] = ImagePropType::BOOL; + Image::$bool_props[] = "private"; } - public function onInitUserConfig(InitUserConfigEvent $event): void + public function onInitUserConfig(InitUserConfigEvent $event) { $event->user_config->set_default_bool(PrivateImageConfig::USER_SET_DEFAULT, false); $event->user_config->set_default_bool(PrivateImageConfig::USER_VIEW_DEFAULT, true); } - public function onUserOptionsBuilding(UserOptionsBuildingEvent $event): void + public function onUserOptionsBuilding(UserOptionsBuildingEvent $event) { global $user; $sb = $event->panel->create_new_block("Private Posts"); @@ -39,7 +39,7 @@ class PrivateImage extends Extension $sb->end_table(); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user, $user_config; @@ -53,10 +53,10 @@ class PrivateImage extends Extension throw new SCoreException("Can not make image private: No valid Post ID given."); } $image = Image::by_id($image_id); - if ($image == null) { + if ($image==null) { throw new SCoreException("Post not found."); } - if ($image->owner_id != $user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { + if ($image->owner_id!=$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { throw new SCoreException("Cannot set another user's image to private."); } @@ -75,10 +75,10 @@ class PrivateImage extends Extension throw new SCoreException("Can not make image public: No valid Post ID given."); } $image = Image::by_id($image_id); - if ($image == null) { + if ($image==null) { throw new SCoreException("Post not found."); } - if ($image->owner_id != $user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { + if ($image->owner_id!=$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { throw new SCoreException("Cannot set another user's image to public."); } @@ -113,19 +113,19 @@ class PrivateImage extends Extension } } } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $user, $page; - if ($event->image['private'] === true && $event->image->owner_id != $user->id && !$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { + if ($event->image->private===true && $event->image->owner_id!=$user->id && !$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link()); + $page->set_redirect(make_link("post/list")); } } public const SEARCH_REGEXP = "/^private:(yes|no|any)/"; - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { global $user, $user_config; $show_private = $user_config->get_bool(PrivateImageConfig::USER_VIEW_DEFAULT); @@ -137,12 +137,12 @@ class PrivateImage extends Extension $event->add_querylet( new Querylet( "private != :true OR owner_id = :private_owner_id", - ["private_owner_id" => $user->id, "true" => true] + ["private_owner_id"=>$user->id, "true"=>true] ) ); } else { $event->add_querylet( - new Querylet("private != :true", ["true" => true]) + new Querylet("private != :true", ["true"=>true]) ); } } @@ -180,9 +180,9 @@ class PrivateImage extends Extension } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Private Posts"; $block->body = $this->theme->get_help_html(); @@ -190,9 +190,7 @@ class PrivateImage extends Extension } } - /** - * @param string[] $context - */ + private function no_private_query(array $context): bool { foreach ($context as $term) { @@ -203,35 +201,35 @@ class PrivateImage extends Extension return true; } - public static function privatize_image(int $image_id): void + public static function privatize_image($image_id) { global $database; $database->execute( "UPDATE images SET private = :true WHERE id = :id AND private = :false", - ["id" => $image_id, "true" => true, "false" => false] + ["id"=>$image_id, "true"=>true, "false"=>false] ); } - public static function publicize_image(int $image_id): void + public static function publicize_image($image_id) { global $database; $database->execute( "UPDATE images SET private = :false WHERE id = :id AND private = :true", - ["id" => $image_id, "true" => true, "false" => false] + ["id"=>$image_id, "true"=>true, "false"=>false] ); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; - if (($user->can(Permissions::SET_PRIVATE_IMAGE) && $user->id == $event->image->owner_id) || $user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { + if (($user->can(Permissions::SET_PRIVATE_IMAGE) && $user->id==$event->image->owner_id) || $user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { $event->add_part($this->theme->get_image_admin_html($event->image)); } } - public function onImageAddition(ImageAdditionEvent $event): void + public function onImageAddition(ImageAdditionEvent $event) { global $user, $user_config; if ($user_config->get_bool(PrivateImageConfig::USER_SET_DEFAULT) && $user->can(Permissions::SET_PRIVATE_IMAGE)) { @@ -239,7 +237,7 @@ class PrivateImage extends Extension } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -249,7 +247,7 @@ class PrivateImage extends Extension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $page, $user; @@ -258,7 +256,7 @@ class PrivateImage extends Extension if ($user->can(Permissions::SET_PRIVATE_IMAGE)) { $total = 0; foreach ($event->items as $image) { - if ($image->owner_id == $user->id || + if ($image->owner_id==$user->id || $user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { self::privatize_image($image->id); $total++; @@ -270,7 +268,7 @@ class PrivateImage extends Extension case "bulk_publicize_image": $total = 0; foreach ($event->items as $image) { - if ($image->owner_id == $user->id || + if ($image->owner_id==$user->id || $user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) { self::publicize_image($image->id); $total++; @@ -280,7 +278,7 @@ class PrivateImage extends Extension break; } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; diff --git a/ext/private_image/theme.php b/ext/private_image/theme.php index 9e8fe687..a06a2239 100644 --- a/ext/private_image/theme.php +++ b/ext/private_image/theme.php @@ -8,23 +8,23 @@ use function MicroHTML\INPUT; class PrivateImageTheme extends Themelet { - public function get_image_admin_html(Image $image): \MicroHTML\HTMLElement + public function get_image_admin_html(Image $image): string { - if ($image['private'] === false) { + if ($image->private===false) { $html = SHM_SIMPLE_FORM( 'privatize_image/'.$image->id, - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image->id]), + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]), SHM_SUBMIT("Make Private") ); } else { $html = SHM_SIMPLE_FORM( 'publicize_image/'.$image->id, - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image->id]), + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]), SHM_SUBMIT("Make Public") ); } - return $html; + return (string)$html; } public function get_help_html(): string diff --git a/ext/qr_code/info.php b/ext/qr_code/info.php index dac6bf0d..c43b3fde 100644 --- a/ext/qr_code/info.php +++ b/ext/qr_code/info.php @@ -11,7 +11,7 @@ class QRImageInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "QR Codes"; 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 = "Shows a QR Code for downloading a post to cell phones"; public ?string $documentation = diff --git a/ext/qr_code/main.php b/ext/qr_code/main.php index f9ceafb4..22468c8c 100644 --- a/ext/qr_code/main.php +++ b/ext/qr_code/main.php @@ -9,7 +9,7 @@ class QRImage extends Extension /** @var QRImageTheme */ protected Themelet $theme; - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { $this->theme->links_block(make_http(make_link('image/'.$event->image->id.'.'.$event->image->get_ext()))); } diff --git a/ext/qr_code/theme.php b/ext/qr_code/theme.php index 4579880c..857eb89d 100644 --- a/ext/qr_code/theme.php +++ b/ext/qr_code/theme.php @@ -6,7 +6,7 @@ namespace Shimmie2; class QRImageTheme extends Themelet { - public function links_block(string $link): void + public function links_block(string $link) { global $page; diff --git a/ext/random_image/main.php b/ext/random_image/main.php index 0e43ae91..2733708c 100644 --- a/ext/random_image/main.php +++ b/ext/random_image/main.php @@ -9,7 +9,7 @@ class RandomImage extends Extension /** @var RandomImageTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page; @@ -25,7 +25,10 @@ class RandomImage extends Extension } $image = Image::by_random($search_terms); if (!$image) { - throw new SCoreException("Couldn't find any posts randomly"); + throw new SCoreException( + "Couldn't find any posts randomly", + Tag::implode($search_terms) + ); } if ($action === "download") { @@ -40,13 +43,13 @@ class RandomImage extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Random Post"); $sb->add_bool_option("show_random_block", "Show Random Block: "); } - public function onPostListBuilding(PostListBuildingEvent $event): void + public function onPostListBuilding(PostListBuildingEvent $event) { global $config, $page; if ($config->get_bool("show_random_block")) { @@ -57,9 +60,9 @@ class RandomImage extends Extension } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "posts") { + if ($event->parent=="posts") { $event->add_nav_link("posts_random", new Link('random_image/view'), "Random Post"); } } diff --git a/ext/random_image/test.php b/ext/random_image/test.php index 44a00077..d39391e6 100644 --- a/ext/random_image/test.php +++ b/ext/random_image/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class RandomImageTest extends ShimmiePHPUnitTestCase { - public function testRandom(): void + public function testRandom() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); @@ -23,7 +23,7 @@ class RandomImageTest extends ShimmiePHPUnitTestCase # FIXME: assert($raw == file(blah.jpg)) } - public function testPostListBlock(): void + public function testPostListBlock() { global $config; diff --git a/ext/random_image/theme.php b/ext/random_image/theme.php index 198598da..144e0717 100644 --- a/ext/random_image/theme.php +++ b/ext/random_image/theme.php @@ -10,7 +10,7 @@ use function MicroHTML\IMG; class RandomImageTheme extends Themelet { - public function display_random(Page $page, Image $image): void + public function display_random(Page $page, Image $image) { $page->add_block(new Block("Random Post", $this->build_random_html($image), "left", 8)); } @@ -20,16 +20,16 @@ class RandomImageTheme extends Themelet $tsize = get_thumbnail_size($image->width, $image->height); return (string)DIV( - ["style" => "text-align: center;"], + ["style"=>"text-align: center;"], A( - ["href" => make_link("post/view/{$image->id}", $query)], + ["href"=>make_link("post/view/{$image->id}", $query)], IMG([ - "id" => "thumb_rand_{$image->id}", - "title" => $image->get_tooltip(), - "alt" => $image->get_tooltip(), - "class" => 'highlighted', - "style" => "max-height: {$tsize[1]}px; max-width: 100%;", - "src" => $image->get_thumb_link() + "id"=>"thumb_rand_{$image->id}", + "title"=>$image->get_tooltip(), + "alt"=>$image->get_tooltip(), + "class"=>'highlighted', + "style"=>"max-height: {$tsize[1]}px; max-width: 100%;", + "src"=>$image->get_thumb_link() ]) ) ); diff --git a/ext/random_list/info.php b/ext/random_list/info.php index 8a52e22b..38ec94df 100644 --- a/ext/random_list/info.php +++ b/ext/random_list/info.php @@ -11,7 +11,7 @@ class RandomListInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Random List"; public string $url = "http://www.drudexsoftware.com"; - public array $authors = ["Drudex Software" => "support@drudexsoftware.com"]; + public array $authors = ["Drudex Software"=>"support@drudexsoftware.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Allows displaying a page with random posts"; public ?string $documentation = diff --git a/ext/random_list/main.php b/ext/random_list/main.php index 693ba43d..18b077d1 100644 --- a/ext/random_list/main.php +++ b/ext/random_list/main.php @@ -9,7 +9,7 @@ class RandomList extends Extension /** @var RandomListTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page; @@ -53,13 +53,13 @@ class RandomList extends Extension } } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int("random_images_list_count", 12); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Random Posts List"); @@ -70,9 +70,9 @@ class RandomList extends Extension ); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "posts") { + if ($event->parent=="posts") { $event->add_nav_link("posts_random", new Link('random'), "Shuffle"); } } diff --git a/ext/random_list/theme.php b/ext/random_list/theme.php index a16eeae4..0eac68f7 100644 --- a/ext/random_list/theme.php +++ b/ext/random_list/theme.php @@ -6,21 +6,20 @@ namespace Shimmie2; class RandomListTheme extends Themelet { - /** @var string[] */ protected array $search_terms; /** - * @param string[] $search_terms + * #param string[] $search_terms */ - public function set_page(array $search_terms): void + public function set_page(array $search_terms) { $this->search_terms = $search_terms; } /** - * @param Image[] $images + * #param Image[] $images */ - public function display_page(Page $page, array $images): void + public function display_page(Page $page, array $images) { $page->title = "Random Posts"; @@ -43,17 +42,14 @@ class RandomListTheme extends Themelet $page->add_block(new Block("Navigation", $nav, "left", 0)); } - /** - * @param string[] $search_terms - */ protected function build_navigation(array $search_terms): string { $h_search_string = html_escape(Tag::implode($search_terms)); $h_search_link = make_link("random"); $h_search = "

    - - + + "; diff --git a/ext/rating/main.php b/ext/rating/main.php index c7e54720..eab9ae0e 100644 --- a/ext/rating/main.php +++ b/ext/rating/main.php @@ -4,10 +4,14 @@ declare(strict_types=1); namespace Shimmie2; +/** +* @global ImageRating[] $_shm_ratings +*/ +global $_shm_ratings; +$_shm_ratings = []; + class ImageRating { - /** @var array */ - public static array $known_ratings = []; public string $name; public string $code; public string $search_term; @@ -15,7 +19,7 @@ class ImageRating public function __construct(string $code, string $name, string $search_term, int $order) { - assert(strlen($code) == 1, "Rating code must be exactly one character"); + assert(strlen($code)==1, "Rating code must be exactly one character"); $this->name = $name; $this->code = $code; @@ -26,13 +30,14 @@ class ImageRating function add_rating(ImageRating $rating): void { - if ($rating->code == "?" && array_key_exists("?", ImageRating::$known_ratings)) { + global $_shm_ratings; + if ($rating->code == "?" && array_key_exists("?", $_shm_ratings)) { throw new \RuntimeException("? is a reserved rating code that cannot be overridden"); } if ($rating->code != "?" && in_array(strtolower($rating->search_term), Ratings::UNRATED_KEYWORDS)) { throw new \RuntimeException("$rating->search_term is a reserved search term"); } - ImageRating::$known_ratings[$rating->code] = $rating; + $_shm_ratings[$rating->code] = $rating; } add_rating(new ImageRating("?", "Unrated", "unrated", 99999)); @@ -50,8 +55,9 @@ class RatingSetEvent extends Event public function __construct(Image $image, string $rating) { parent::__construct(); + global $_shm_ratings; - assert(in_array($rating, array_keys(ImageRating::$known_ratings))); + assert(in_array($rating, array_keys($_shm_ratings))); $this->image = $image; $this->rating = $rating; @@ -73,26 +79,24 @@ class Ratings extends Extension private string $search_regexp; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { - global $config; + global $config, $_shm_user_classes, $_shm_ratings; - $codes = implode("", array_keys(ImageRating::$known_ratings)); + $codes = implode("", array_keys($_shm_ratings)); $search_terms = []; - foreach (ImageRating::$known_ratings as $key => $rating) { + foreach ($_shm_ratings as $key => $rating) { $search_terms[] = $rating->search_term; } $this->search_regexp = "/^rating[=|:](?:(\*|[" . $codes . "]+)|(" . implode("|", $search_terms) . "|".implode("|", self::UNRATED_KEYWORDS)."))$/D"; - foreach (array_keys(UserClass::$known_classes) as $key) { + foreach (array_keys($_shm_user_classes) as $key) { if ($key == "base" || $key == "hellbanned") { continue; } - $config->set_default_array("ext_rating_" . $key . "_privs", array_keys(ImageRating::$known_ratings)); + $config->set_default_array("ext_rating_" . $key . "_privs", array_keys($_shm_ratings)); } - - Image::$prop_types["rating"] = ImagePropType::STRING; } private function check_permissions(Image $image): bool @@ -100,18 +104,18 @@ class Ratings extends Extension global $user; $user_view_level = Ratings::get_user_class_privs($user); - if (!in_array($image['rating'], $user_view_level)) { + if (!in_array($image->rating, $user_view_level)) { return false; } return true; } - public function onInitUserConfig(InitUserConfigEvent $event): void + public function onInitUserConfig(InitUserConfigEvent $event) { $event->user_config->set_default_array(RatingsConfig::USER_DEFAULTS, self::get_user_class_privs($event->user)); } - public function onImageDownloading(ImageDownloadingEvent $event): void + public function onImageDownloading(ImageDownloadingEvent $event) { /** * Deny images upon insufficient permissions. @@ -121,25 +125,27 @@ class Ratings extends Extension } } - public function onUserOptionsBuilding(UserOptionsBuildingEvent $event): void + public function onUserOptionsBuilding(UserOptionsBuildingEvent $event) { - global $user; + global $user, $_shm_ratings; $levels = self::get_user_class_privs($user); $options = []; foreach ($levels as $level) { - $options[ImageRating::$known_ratings[$level]->name] = $level; + $options[$_shm_ratings[$level]->name] = $level; } $sb = $event->panel->create_new_block("Default Rating Filter"); $sb->start_table(); - $sb->add_multichoice_option(RatingsConfig::USER_DEFAULTS, $options, "Default Ratings: ", true); + $sb->add_multichoice_option(RatingsConfig::USER_DEFAULTS, $options, "Output Log Level: ", true); $sb->end_table(); $sb->add_label("This controls the default rating search results will be filtered by, and nothing else. To override in your search results, add rating:* to your search."); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { + global $_shm_user_classes; + $ratings = self::get_sorted_ratings(); $options = []; @@ -149,7 +155,7 @@ class Ratings extends Extension $sb = $event->panel->create_new_block("Post Rating Visibility"); $sb->start_table(); - foreach (array_keys(UserClass::$known_classes) as $key) { + foreach (array_keys($_shm_user_classes) as $key) { if ($key == "base" || $key == "hellbanned") { continue; } @@ -158,14 +164,7 @@ class Ratings extends Extension $sb->end_table(); } - public function onImageAddition(ImageAdditionEvent $event): void - { - if(!empty($event->metadata['rating'])) { - send_event(new RatingSetEvent($event->image, $event->metadata['rating'])); - } - } - - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $page; /** @@ -173,15 +172,15 @@ class Ratings extends Extension **/ 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 onBulkExport(BulkExportEvent $event): void + public function onBulkExport(BulkExportEvent $event) { - $event->fields["rating"] = $event->image['rating']; + $event->fields["rating"] = $event->image->rating; } - public function onBulkImport(BulkImportEvent $event): void + public function onBulkImport(BulkImportEvent $event) { if (array_key_exists("rating", $event->fields) && $event->fields['rating'] !== null @@ -190,30 +189,30 @@ class Ratings extends Extension } } - public function onRatingSet(RatingSetEvent $event): void + public function onRatingSet(RatingSetEvent $event) { - if (empty($event->image['rating'])) { + if (empty($event->image->rating)) { $old_rating = ""; } else { - $old_rating = $event->image['rating']; + $old_rating = $event->image->rating; } $this->set_rating($event->image->id, $event->rating, $old_rating); } - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { global $user; $event->add_part( - $this->theme->get_rater_html( + (string)$this->theme->get_rater_html( $event->image->id, - $event->image['rating'], + $event->image->rating, $user->can(Permissions::EDIT_IMAGE_RATING) ), 80 ); } - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { global $user; if ($user->can(Permissions::EDIT_IMAGE_RATING) && isset($_POST["rating"])) { @@ -224,22 +223,20 @@ class Ratings extends Extension } } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { - if(!is_null($event->image['rating'])) { - $event->replace('$rating', $this->rating_to_human($event->image['rating'])); - } + $event->replace('$rating', $this->rating_to_human($event->image->rating)); } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $ratings = self::get_sorted_ratings(); $event->add_block(new Block("Ratings", $this->theme->get_help_html($ratings))); } } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { global $user; @@ -256,7 +253,7 @@ class Ratings extends Extension if (preg_match($this->search_regexp, strtolower($event->term), $matches)) { $ratings = $matches[1] ? $matches[1] : $matches[2][0]; - if (count($matches) > 2 && in_array($matches[2], self::UNRATED_KEYWORDS)) { + if (count($matches)>2&&in_array($matches[2], self::UNRATED_KEYWORDS)) { $ratings = "?"; } @@ -271,14 +268,14 @@ class Ratings extends Extension } } - public function onTagTermCheck(TagTermCheckEvent $event): void + public function onTagTermCheck(TagTermCheckEvent $event) { if (preg_match($this->search_regexp, $event->term)) { $event->metatag = true; } } - public function onTagTermParse(TagTermParseEvent $event): void + public function onTagTermParse(TagTermParseEvent $event) { global $user; $matches = []; @@ -286,7 +283,7 @@ class Ratings extends Extension if (preg_match($this->search_regexp, strtolower($event->term), $matches)) { $ratings = $matches[1] ? $matches[1] : $matches[2][0]; - if (count($matches) > 2 && in_array($matches[2], self::UNRATED_KEYWORDS)) { + if (count($matches)>2&&in_array($matches[2], self::UNRATED_KEYWORDS)) { $ratings = "?"; } @@ -298,16 +295,15 @@ class Ratings extends Extension } } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { - global $database; + global $database, $_shm_ratings; $results = $database->get_col("SELECT DISTINCT rating FROM images ORDER BY rating"); $original_values = []; foreach ($results as $result) { - assert(is_string($result)); - if (array_key_exists($result, ImageRating::$known_ratings)) { - $original_values[$result] = ImageRating::$known_ratings[$result]->name; + if (array_key_exists($result, $_shm_ratings)) { + $original_values[$result] = $_shm_ratings[$result]->name; } else { $original_values[$result] = $result; } @@ -316,7 +312,7 @@ class Ratings extends Extension $this->theme->display_form($original_values); } - public function onAdminAction(AdminActionEvent $event): void + public function onAdminAction(AdminActionEvent $event) { global $database, $user; $action = $event->action; @@ -333,14 +329,14 @@ class Ratings extends Extension $new = $_POST["rating_new"]; if ($user->can(Permissions::BULK_EDIT_IMAGE_RATING)) { - $database->execute("UPDATE images SET rating = :new WHERE rating = :old", ["new" => $new, "old" => $old ]); + $database->execute("UPDATE images SET rating = :new WHERE rating = :old", ["new"=>$new, "old"=>$old ]); } break; } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -349,7 +345,7 @@ class Ratings extends Extension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $page, $user; @@ -371,7 +367,7 @@ class Ratings extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $user, $page; @@ -381,7 +377,7 @@ class Ratings extends Extension } else { $n = 0; while (true) { - $images = Search::find_images($n, 100, Tag::explode($_POST["query"])); + $images = Image::find_images($n, 100, Tag::explode($_POST["query"])); if (count($images) == 0) { break; } @@ -399,28 +395,23 @@ class Ratings extends Extension # on image_tags.tag_id = tags.id where tags.tag = :tag); # ", ['rating'=>$_POST["rating"], 'tag'=>$_POST["tag"]]); $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link()); + $page->set_redirect(make_link("post/list")); } } } - /** - * @return ImageRating[] - */ public static function get_sorted_ratings(): array { - $ratings = array_values(ImageRating::$known_ratings); + global $_shm_ratings; + + $ratings = array_values($_shm_ratings); usort($ratings, function ($a, $b) { return $a->order <=> $b->order; }); return $ratings; } - /** - * @param ImageRating[]|null $ratings - * @return array - */ - public static function get_ratings_dict(array $ratings = null): array + public static function get_ratings_dict(array $ratings=null): array { if (!isset($ratings)) { $ratings = self::get_sorted_ratings(); @@ -435,11 +426,6 @@ class Ratings extends Extension ); } - /** - * Figure out which ratings a user is allowed to see - * - * @return string[] - */ public static function get_user_class_privs(User $user): array { global $config; @@ -447,12 +433,6 @@ class Ratings extends Extension return $config->get_array("ext_rating_".$user->class->name."_privs"); } - /** - * Figure out which ratings a user would like to see by default - * (Which will be a subset of what they are allowed to see) - * - * @return string[] - */ public static function get_user_default_ratings(): array { global $user_config, $user; @@ -463,16 +443,13 @@ class Ratings extends Extension return array_intersect($available, $selected); } - /** - * @param string[] $privs - */ public static function privs_to_sql(array $privs): string { $arr = []; foreach ($privs as $i) { $arr[] = "'" . $i . "'"; } - if (sizeof($arr) == 0) { + if (sizeof($arr)==0) { return "' '"; } return join(', ', $arr); @@ -480,19 +457,23 @@ class Ratings extends Extension public static function rating_to_human(string $rating): string { - if (array_key_exists($rating, ImageRating::$known_ratings)) { - return ImageRating::$known_ratings[$rating]->name; + global $_shm_ratings; + + if (array_key_exists($rating, $_shm_ratings)) { + return $_shm_ratings[$rating]->name; } return "Unknown"; } public static function rating_is_valid(string $rating): bool { - return in_array($rating, array_keys(ImageRating::$known_ratings)); + global $_shm_ratings; + + return in_array($rating, array_keys($_shm_ratings)); } /** - * @param string[] $context + * #param string[] $context */ private function no_rating_query(array $context): bool { @@ -504,7 +485,7 @@ class Ratings extends Extension return true; } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database, $config; @@ -558,17 +539,17 @@ class Ratings extends Extension $database->set_timeout(null); // These updates can take a little bit - $database->execute("UPDATE images SET rating = :new WHERE rating = :old", ["new" => '?', "old" => 'u' ]); + $database->execute("UPDATE images SET rating = :new WHERE rating = :old", ["new"=>'?', "old"=>'u' ]); $this->set_version(RatingsConfig::VERSION, 4); } } - private function set_rating(int $image_id, string $rating, string $old_rating): void + private function set_rating(int $image_id, string $rating, string $old_rating) { global $database; if ($old_rating != $rating) { - $database->execute("UPDATE images SET rating=:rating WHERE id=:id", ['rating' => $rating, 'id' => $image_id]); + $database->execute("UPDATE images SET rating=:rating WHERE id=:id", ['rating'=>$rating, 'id'=>$image_id]); log_info("rating", "Rating for >>{$image_id} set to: ".$this->rating_to_human($rating)); } } diff --git a/ext/rating/test.php b/ext/rating/test.php index 7d4f744d..e9b73473 100644 --- a/ext/rating/test.php +++ b/ext/rating/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class RatingsTest extends ShimmiePHPUnitTestCase { - public function testRatingSafe(): void + public function testRatingSafe() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); @@ -25,7 +25,7 @@ class RatingsTest extends ShimmiePHPUnitTestCase $this->assert_search_results(["rating=q"], []); } - public function testRatingExplicit(): void + public function testRatingExplicit() { global $config; $config->set_array("ext_rating_anonymous_privs", ["s", "q"]); @@ -38,79 +38,4 @@ class RatingsTest extends ShimmiePHPUnitTestCase $this->log_out(); $this->assert_search_results(["pbx"], []); } - - public function testUserConfig(): void - { - global $config, $user_config; - - // post a safe image and an explicit image - $this->log_in_as_user(); - $image_id_e = $this->post_image("tests/bedroom_workshop.jpg", "pbx"); - $image_e = Image::by_id($image_id_e); - send_event(new RatingSetEvent($image_e, "e")); - $image_id_s = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $image_s = Image::by_id($image_id_s); - send_event(new RatingSetEvent($image_s, "s")); - - // user is allowed to see all - $config->set_array("ext_rating_user_privs", ["s", "q", "e"]); - - // user prefers safe-only by default - $user_config->set_array(RatingsConfig::USER_DEFAULTS, ["s"]); - - // search with no tags should return only safe image - $this->assert_search_results([], [$image_id_s]); - - // specifying a rating should return only that rating - $this->assert_search_results(["rating=e"], [$image_id_e]); - $this->assert_search_results(["rating=s"], [$image_id_s]); - - // If user prefers to see all images, going to the safe image - // and clicking next should show the explicit image - $user_config->set_array(RatingsConfig::USER_DEFAULTS, ["s", "q", "e"]); - $this->assertEquals($image_s->get_next()->id, $image_id_e); - - // If the user prefers to see only safe images by default, then - // going to the safe image and clicking next should not show - // the explicit image (See bug #984) - $user_config->set_array(RatingsConfig::USER_DEFAULTS, ["s"]); - $this->assertEquals($image_s->get_next(), null); - } - - public function testCountImages(): void - { - global $config, $user_config; - - $this->log_in_as_user(); - - $image_id_s = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); - $image_s = Image::by_id($image_id_s); - send_event(new RatingSetEvent($image_s, "s")); - $image_id_q = $this->post_image("tests/favicon.png", "favicon"); - $image_q = Image::by_id($image_id_q); - send_event(new RatingSetEvent($image_q, "q")); - $image_id_e = $this->post_image("tests/bedroom_workshop.jpg", "bedroom"); - $image_e = Image::by_id($image_id_e); - send_event(new RatingSetEvent($image_e, "e")); - - $config->set_array("ext_rating_user_privs", ["s", "q"]); - $user_config->set_array(RatingsConfig::USER_DEFAULTS, ["s"]); - - $this->assertEquals(1, Search::count_images(["rating=s"]), "UserClass has access to safe, show safe"); - $this->assertEquals(2, Search::count_images(["rating=*"]), "UserClass has access to s/q - if user asks for everything, show those two but hide e"); - $this->assertEquals(1, Search::count_images(), "If search doesn't specify anything, check the user defaults"); - } - - // reset the user config to defaults at the end of every test so - // that it doesn't mess with other unrelated tests - public function tearDown(): void - { - global $config, $user_config; - $config->set_array("ext_rating_user_privs", ["?", "s", "q", "e"]); - - $this->log_in_as_user(); - $user_config->set_array(RatingsConfig::USER_DEFAULTS, ["?", "s", "q", "e"]); - - parent::tearDown(); - } } diff --git a/ext/rating/theme.php b/ext/rating/theme.php index 72f6efb1..929a5288 100644 --- a/ext/rating/theme.php +++ b/ext/rating/theme.php @@ -7,14 +7,10 @@ namespace Shimmie2; use MicroHTML\HTMLElement; use function MicroHTML\emptyHTML; -use function MicroHTML\{A,P,TABLE,TD,TH,TR}; +use function MicroHTML\{P,SPAN,TABLE,TD,TH,TR}; class RatingsTheme extends Themelet { - /** - * @param array $ratings - * @param string[] $selected_options - */ public function get_selection_rater_html(string $name = "rating", array $ratings = [], array $selected_options = []): HTMLElement { return SHM_SELECT($name, !empty($ratings) ? $ratings : Ratings::get_ratings_dict(), required: true, selected_options: $selected_options); @@ -22,33 +18,38 @@ class RatingsTheme extends Themelet public function get_rater_html(int $image_id, string $rating, bool $can_rate): HTMLElement { - return SHM_POST_INFO( - "Rating", - A(["href" => search_link(["rating=$rating"])], Ratings::rating_to_human($rating)), - $can_rate ? $this->get_selection_rater_html("rating", selected_options: [$rating]) : null - ); + $human_rating = Ratings::rating_to_human($rating); + + $html = TR(TH("Rating")); + + if ($can_rate) { + $selector = $this->get_selection_rater_html(selected_options: [$rating]); + + $html->appendChild(TD( + SPAN(["class"=>"view"], $human_rating), + SPAN(["class"=>"edit"], $selector) + )); + } else { + $html->appendChild(TD($human_rating)); + } + + return $html; } - /** - * @param array $current_ratings - */ - public function display_form(array $current_ratings): void + public function display_form(array $current_ratings) { global $page; $table = TABLE( - ["class" => "form"], + ["class"=>"form"], TR(TH("Change"), TD($this->get_selection_rater_html("rating_old", $current_ratings))), TR(TH("To"), TD($this->get_selection_rater_html("rating_new"))), - TR(TD(["colspan" => "2"], SHM_SUBMIT("Update"))) + TR(TD(["colspan"=>"2"], SHM_SUBMIT("Update"))) ); $page->add_block(new Block("Update Ratings", SHM_SIMPLE_FORM("admin/update_ratings", $table))); } - /** - * @param ImageRating[] $ratings - */ public function get_help_html(array $ratings): HTMLElement { $rating_rows = [TR(TH("Name"), TH("Search Term"), TH("Abbreviation"))]; diff --git a/ext/regen_thumb/main.php b/ext/regen_thumb/main.php index 8c93604d..b66389ee 100644 --- a/ext/regen_thumb/main.php +++ b/ext/regen_thumb/main.php @@ -12,12 +12,12 @@ class RegenThumb extends Extension public function regenerate_thumbnail(Image $image, bool $force = true): bool { global $cache; - $event = send_event(new ThumbnailGenerationEvent($image, $force)); + $event = send_event(new ThumbnailGenerationEvent($image->hash, $image->get_mime(), $force)); $cache->delete("thumb-block:{$image->id}"); return $event->generated; } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -30,18 +30,18 @@ class RegenThumb extends Extension } if ($event->page_matches("regen_thumb/mass") && $user->can(Permissions::DELETE_IMAGE) && isset($_POST['tags'])) { $tags = Tag::explode(strtolower($_POST['tags']), false); - $images = Search::find_images(limit: 10000, tags: $tags); + $images = Image::find_images(limit: 10000, tags: $tags); foreach ($images as $image) { $this->regenerate_thumbnail($image); } $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link()); + $page->set_redirect(make_link("post/list")); } } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::DELETE_IMAGE)) { @@ -49,7 +49,7 @@ class RegenThumb extends Extension } } - // public function onPostListBuilding(PostListBuildingEvent $event): void + // public function onPostListBuilding(PostListBuildingEvent $event) // { // global $user; // if ($user->can(UserAbilities::DELETE_IMAGE) && !empty($event->search_terms)) { @@ -57,7 +57,7 @@ class RegenThumb extends Extension // } // } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -66,7 +66,7 @@ class RegenThumb extends Extension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $page, $user; @@ -75,8 +75,8 @@ class RegenThumb extends Extension if ($user->can(Permissions::DELETE_IMAGE)) { $force = true; if (isset($_POST["bulk_regen_thumb_missing_only"]) - && $_POST["bulk_regen_thumb_missing_only"] == "true") { - $force = false; + &&$_POST["bulk_regen_thumb_missing_only"]=="true") { + $force=false; } $total = 0; @@ -91,45 +91,45 @@ class RegenThumb extends Extension } } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_admin_block(); } - public function onAdminAction(AdminActionEvent $event): void + public function onAdminAction(AdminActionEvent $event) { global $page; switch ($event->action) { case "regen_thumbs": $event->redirect = true; $force = false; - if (isset($_POST["regen_thumb_force"]) && $_POST["regen_thumb_force"] == "true") { - $force = true; + if (isset($_POST["regen_thumb_force"])&&$_POST["regen_thumb_force"]=="true") { + $force=true; } $limit = 1000; - if (isset($_POST["regen_thumb_limit"]) && is_numeric($_POST["regen_thumb_limit"])) { - $limit = intval($_POST["regen_thumb_limit"]); + if (isset($_POST["regen_thumb_limit"])&&is_numeric($_POST["regen_thumb_limit"])) { + $limit=intval($_POST["regen_thumb_limit"]); } $mime = ""; if (isset($_POST["regen_thumb_mime"])) { $mime = $_POST["regen_thumb_mime"]; } - $images = Search::find_images(tags: ["mime=" . $mime]); + $images = $this->get_images($mime); $i = 0; foreach ($images as $image) { if (!$force) { - $path = warehouse_path(Image::THUMBNAIL_DIR, $image->hash, false); + $path = warehouse_path(Image::THUMBNAIL_DIR, $image["hash"], false); if (file_exists($path)) { continue; } } - $event = send_event(new ThumbnailGenerationEvent($image, $force)); + $event = send_event(new ThumbnailGenerationEvent($image["hash"], $image["mime"], $force)); if ($event->generated) { $i++; } - if ($i >= $limit) { + if ($i>=$limit) { break; } } @@ -138,12 +138,12 @@ class RegenThumb extends Extension case "delete_thumbs": $event->redirect = true; - if (isset($_POST["delete_thumb_mime"]) && $_POST["delete_thumb_mime"] != "") { - $images = Search::find_images(tags: ["mime=" . $_POST["delete_thumb_mime"]]); + if (isset($_POST["delete_thumb_mime"])&&$_POST["delete_thumb_mime"]!="") { + $images = $this->get_images($_POST["delete_thumb_mime"]); $i = 0; foreach ($images as $image) { - $outname = $image->get_thumb_filename(); + $outname = warehouse_path(Image::THUMBNAIL_DIR, $image["hash"]); if (file_exists($outname)) { unlink($outname); $i++; @@ -152,7 +152,7 @@ class RegenThumb extends Extension $page->flash("Deleted $i thumbnails for ".$_POST["delete_thumb_mime"]." images"); } else { $dir = "data/thumbs/"; - deltree($dir); + $this->remove_dir_recursively($dir); $page->flash("Deleted all thumbnails"); } @@ -160,4 +160,36 @@ class RegenThumb extends Extension break; } } + + public function get_images(string $mime = null): array + { + global $database; + + $query = "SELECT hash, mime FROM images"; + $args = []; + if (!empty($mime)) { + $query .= " WHERE mime = :mime"; + $args["mime"] = $mime; + } + + return $database->get_all($query, $args); + } + + public function remove_dir_recursively($dir) + { + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (filetype($dir."/".$object) == "dir") { + $this->remove_dir_recursively($dir."/".$object); + } else { + unlink($dir."/".$object); + } + } + } + reset($objects); + rmdir($dir); + } + } } diff --git a/ext/regen_thumb/test.php b/ext/regen_thumb/test.php index 2eef060c..44f93fd1 100644 --- a/ext/regen_thumb/test.php +++ b/ext/regen_thumb/test.php @@ -6,13 +6,13 @@ namespace Shimmie2; class RegenThumbTest extends ShimmiePHPUnitTestCase { - public function testRegenThumb(): void + public function testRegenThumb() { $this->log_in_as_admin(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); $this->get_page("post/view/$image_id"); - $this->post_page("regen_thumb/one", ['image_id' => $image_id]); + $this->post_page("regen_thumb/one", ['image_id'=>$image_id]); $this->assert_title("Thumbnail Regenerated"); # FIXME: test that the thumb's modified time has been updated diff --git a/ext/regen_thumb/theme.php b/ext/regen_thumb/theme.php index e6f4a16f..cf05e5c3 100644 --- a/ext/regen_thumb/theme.php +++ b/ext/regen_thumb/theme.php @@ -11,11 +11,11 @@ class RegenThumbTheme extends Themelet /** * Show a form which offers to regenerate the thumb of an image with ID #$image_id */ - public function get_buttons_html(int $image_id): \MicroHTML\HTMLElement + public function get_buttons_html(int $image_id): string { - return SHM_SIMPLE_FORM( + return (string)SHM_SIMPLE_FORM( "regen_thumb/one", - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image_id]), + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image_id]), SHM_SUBMIT('Regenerate Thumbnail') ); } @@ -23,7 +23,7 @@ class RegenThumbTheme extends Themelet /** * Show a link to the new thumbnail. */ - public function display_results(Page $page, Image $image): void + public function display_results(Page $page, Image $image) { $page->set_title("Thumbnail Regenerated"); $page->set_heading("Thumbnail Regenerated"); diff --git a/ext/relationships/info.php b/ext/relationships/info.php index 38776024..f7a8e344 100644 --- a/ext/relationships/info.php +++ b/ext/relationships/info.php @@ -10,7 +10,7 @@ class RelationshipsInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Post Relationships"; - public array $authors = ["Angus Johnston" => "admin@codeanimu.net"]; + public array $authors = ["Angus Johnston"=>"admin@codeanimu.net"]; public string $license = self::LICENSE_GPLV2; public string $description = "Allow posts to have relationships (parent/child)."; } diff --git a/ext/relationships/main.php b/ext/relationships/main.php index fd12a064..e6684cb8 100644 --- a/ext/relationships/main.php +++ b/ext/relationships/main.php @@ -25,13 +25,13 @@ class Relationships extends Extension public const NAME = "Relationships"; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { - Image::$prop_types["parent_id"] = ImagePropType::INT; - Image::$prop_types["has_children"] = ImagePropType::BOOL; + Image::$bool_props[] = "has_children"; + Image::$int_props[] = "parent_id"; } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -52,7 +52,7 @@ class Relationships extends Extension } } - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { global $user; if ($user->can(Permissions::EDIT_IMAGE_RELATIONSHIPS)) { @@ -66,12 +66,12 @@ class Relationships extends Extension } } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { $this->theme->relationship_info($event->image); } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -85,17 +85,17 @@ class Relationships extends Extension $not = ($parentID == "any" ? "NOT" : ""); $event->add_querylet(new Querylet("images.parent_id IS $not NULL")); } else { - $event->add_querylet(new Querylet("images.parent_id = :pid", ["pid" => $parentID])); + $event->add_querylet(new Querylet("images.parent_id = :pid", ["pid"=>$parentID])); } } elseif (preg_match("/^child[=|:](any|none)$/", $event->term, $matches)) { $not = ($matches[1] == "any" ? "=" : "!="); - $event->add_querylet(new Querylet("images.has_children $not :true", ["true" => true])); + $event->add_querylet(new Querylet("images.has_children $not :true", ["true"=>true])); } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Relationships"; $block->body = $this->theme->get_help_html(); @@ -103,14 +103,14 @@ class Relationships extends Extension } } - public function onTagTermCheck(TagTermCheckEvent $event): void + public function onTagTermCheck(TagTermCheckEvent $event) { if (preg_match("/^(parent|child)[=|:](.*)$/i", $event->term)) { $event->metatag = true; } } - public function onTagTermParse(TagTermParseEvent $event): void + public function onTagTermParse(TagTermParseEvent $event) { $matches = []; @@ -127,29 +127,29 @@ class Relationships extends Extension } } - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { $event->add_part($this->theme->get_parent_editor_html($event->image), 45); } - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { global $database; - if (bool_escape($event->image['has_children'])) { - $database->execute("UPDATE images SET parent_id = NULL WHERE parent_id = :iid", ["iid" => $event->image->id]); + if (bool_escape($event->image->has_children)) { + $database->execute("UPDATE images SET parent_id = NULL WHERE parent_id = :iid", ["iid"=>$event->image->id]); } - if ($event->image['parent_id'] !== null) { - $this->set_has_children($event->image['parent_id']); + if ($event->image->parent_id !== null) { + $this->set_has_children($event->image->parent_id); } } - public function onImageRelationshipSet(ImageRelationshipSetEvent $event): void + public function onImageRelationshipSet(ImageRelationshipSetEvent $event) { global $database; - $old_parent = $database->get_one("SELECT parent_id FROM images WHERE id = :cid", ["cid" => $event->child_id]); + $old_parent = $database->get_one("SELECT parent_id FROM images WHERE id = :cid", ["cid"=>$event->child_id]); if (!is_null($old_parent)) { $old_parent = (int)$old_parent; } @@ -162,23 +162,20 @@ class Relationships extends Extension } $database->execute("UPDATE images SET parent_id = :pid WHERE id = :cid", ["pid" => $event->parent_id, "cid" => $event->child_id]); - $database->execute("UPDATE images SET has_children = :true WHERE id = :pid", ["pid" => $event->parent_id, "true" => true]); + $database->execute("UPDATE images SET has_children = :true WHERE id = :pid", ["pid" => $event->parent_id, "true"=>true]); - if ($old_parent != null) { + if ($old_parent!=null) { $this->set_has_children($old_parent); } } - /** - * @return Image[] - */ public static function get_children(Image $image, int $omit = null): array { global $database; - $results = $database->get_all_iterable("SELECT * FROM images WHERE parent_id = :pid ", ["pid" => $image->id]); + $results = $database->get_all_iterable("SELECT * FROM images WHERE parent_id = :pid ", ["pid"=>$image->id]); $output = []; foreach ($results as $result) { - if ($result["id"] == $omit) { + if ($result["id"]==$omit) { continue; } $output[] = new Image($result); @@ -186,18 +183,18 @@ class Relationships extends Extension return $output; } - private function remove_parent(int $imageID): void + private function remove_parent(int $imageID) { global $database; - $parentID = $database->get_one("SELECT parent_id FROM images WHERE id = :iid", ["iid" => $imageID]); + $parentID = $database->get_one("SELECT parent_id FROM images WHERE id = :iid", ["iid"=>$imageID]); if ($parentID) { - $database->execute("UPDATE images SET parent_id = NULL WHERE id = :iid", ["iid" => $imageID]); + $database->execute("UPDATE images SET parent_id = NULL WHERE id = :iid", ["iid"=>$imageID]); $this->set_has_children((int)$parentID); } } - private function set_has_children(int $parent_id): void + private function set_has_children(int $parent_id) { global $database; @@ -210,11 +207,11 @@ class Relationships extends Extension $children = $database->get_one( "SELECT COUNT(*) FROM images WHERE parent_id=:pid", - ["pid" => $parent_id] + ["pid"=>$parent_id] ); $database->execute( "UPDATE images SET has_children = :has_children WHERE id = :pid", - ["has_children" => $children > 0, "pid" => $parent_id] + ["has_children"=>$children>0, "pid"=>$parent_id] ); } } diff --git a/ext/relationships/test.php b/ext/relationships/test.php index cfaa82ae..88c54315 100644 --- a/ext/relationships/test.php +++ b/ext/relationships/test.php @@ -4,17 +4,12 @@ declare(strict_types=1); namespace Shimmie2; -use PHPUnit\Framework\Attributes\Depends; - class RelationshipsTest extends ShimmiePHPUnitTestCase { //================================================================= // Set by box //================================================================= - /** - * @return array{0: Image, 1: Image, 2: Image} - */ public function testNoParent(): array { $this->log_in_as_user(); @@ -23,73 +18,73 @@ class RelationshipsTest extends ShimmiePHPUnitTestCase $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "pbx"); $image_id_3 = $this->post_image("tests/favicon.png", "pbx"); - $image_1 = null_throws(Image::by_id($image_id_1)); - $image_2 = null_throws(Image::by_id($image_id_2)); - $image_3 = null_throws(Image::by_id($image_id_3)); + $image_1 = Image::by_id($image_id_1); + $image_2 = Image::by_id($image_id_2); + $image_3 = Image::by_id($image_id_3); - $this->assertNull($image_1['parent_id']); - $this->assertNull($image_2['parent_id']); - $this->assertNull($image_3['parent_id']); - $this->assertFalse($image_1['has_children']); - $this->assertFalse($image_2['has_children']); - $this->assertFalse($image_3['has_children']); + $this->assertNull($image_1->parent_id); + $this->assertNull($image_2->parent_id); + $this->assertNull($image_3->parent_id); + $this->assertFalse($image_1->has_children); + $this->assertFalse($image_2->has_children); + $this->assertFalse($image_3->has_children); return [$image_1, $image_2, $image_3]; } /** - * @return array{0:Image, 1:Image, 2:Image} + * @depends testNoParent */ - #[Depends('testNoParent')] - public function testSetParent(): array + public function testSetParent($imgs): array { [$image_1, $image_2, $image_3] = $this->testNoParent(); send_event(new ImageRelationshipSetEvent($image_2->id, $image_1->id)); // refresh data from database - $image_1 = null_throws(Image::by_id($image_1->id)); - $image_2 = null_throws(Image::by_id($image_2->id)); - $image_3 = null_throws(Image::by_id($image_3->id)); + $image_1 = Image::by_id($image_1->id); + $image_2 = Image::by_id($image_2->id); + $image_3 = Image::by_id($image_3->id); - $this->assertNull($image_1['parent_id']); - $this->assertEquals($image_1->id, $image_2['parent_id']); - $this->assertNull($image_3['parent_id']); - $this->assertTrue($image_1['has_children']); - $this->assertFalse($image_2['has_children']); - $this->assertFalse($image_3['has_children']); + $this->assertNull($image_1->parent_id); + $this->assertEquals($image_1->id, $image_2->parent_id); + $this->assertNull($image_3->parent_id); + $this->assertTrue($image_1->has_children); + $this->assertFalse($image_2->has_children); + $this->assertFalse($image_3->has_children); return [$image_1, $image_2, $image_3]; } /** - * @return array{0:Image, 1:Image, 2:Image} + * @depends testSetParent */ - #[Depends('testSetParent')] - public function testChangeParent(): array + public function testChangeParent($imgs): array { - [$image_1, $image_2, $image_3] = $this->testSetParent(); + [$image_1, $image_2, $image_3] = $this->testSetParent(null); send_event(new ImageRelationshipSetEvent($image_2->id, $image_3->id)); // refresh data from database - $image_1 = null_throws(Image::by_id($image_1->id)); - $image_2 = null_throws(Image::by_id($image_2->id)); - $image_3 = null_throws(Image::by_id($image_3->id)); + $image_1 = Image::by_id($image_1->id); + $image_2 = Image::by_id($image_2->id); + $image_3 = Image::by_id($image_3->id); - $this->assertNull($image_1['parent_id']); - $this->assertEquals($image_3->id, $image_2['parent_id']); - $this->assertNull($image_3['parent_id']); - $this->assertFalse($image_2['has_children']); - $this->assertFalse($image_2['has_children']); - $this->assertTrue($image_3['has_children']); + $this->assertNull($image_1->parent_id); + $this->assertEquals($image_3->id, $image_2->parent_id); + $this->assertNull($image_3->parent_id); + $this->assertFalse($image_2->has_children); + $this->assertFalse($image_2->has_children); + $this->assertTrue($image_3->has_children); return [$image_1, $image_2, $image_3]; } - #[Depends('testSetParent')] - public function testSearch(): void + /** + * @depends testSetParent + */ + public function testSearch($imgs) { - [$image_1, $image_2, $image_3] = $this->testSetParent(); + [$image_1, $image_2, $image_3] = $this->testSetParent(null); $this->assert_search_results(["parent:any"], [$image_2->id]); $this->assert_search_results(["parent:none"], [$image_3->id, $image_1->id]); @@ -99,38 +94,37 @@ class RelationshipsTest extends ShimmiePHPUnitTestCase $this->assert_search_results(["child:none"], [$image_3->id, $image_2->id]); } - #[Depends('testChangeParent')] - public function testRemoveParent(): void + /** + * @depends testChangeParent + */ + public function testRemoveParent($imgs) { - [$image_1, $image_2, $image_3] = $this->testChangeParent(); + [$image_1, $image_2, $image_3] = $this->testChangeParent(null); global $database; $database->execute( "UPDATE images SET parent_id=NULL, has_children=:false", - ["false" => false] + ["false"=>false] ); // FIXME: send_event(new ImageRelationshipSetEvent($image_2->id, null)); // refresh data from database - $image_1 = null_throws(Image::by_id($image_1->id)); - $image_2 = null_throws(Image::by_id($image_2->id)); - $image_3 = null_throws(Image::by_id($image_3->id)); + $image_1 = Image::by_id($image_1->id); + $image_2 = Image::by_id($image_2->id); + $image_3 = Image::by_id($image_3->id); - $this->assertNull($image_1['parent_id']); - $this->assertNull($image_2['parent_id']); - $this->assertNull($image_3['parent_id']); - $this->assertFalse($image_2['has_children']); - $this->assertFalse($image_2['has_children']); - $this->assertFalse($image_3['has_children']); + $this->assertNull($image_1->parent_id); + $this->assertNull($image_2->parent_id); + $this->assertNull($image_3->parent_id); + $this->assertFalse($image_2->has_children); + $this->assertFalse($image_2->has_children); + $this->assertFalse($image_3->has_children); } //================================================================= // Set by tag //================================================================= - /** - * @return array{0:Image, 1:Image, 2:Image} - */ public function testSetParentByTagBase(): array { $this->log_in_as_user(); @@ -138,79 +132,80 @@ class RelationshipsTest extends ShimmiePHPUnitTestCase $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "pbx"); $image_id_3 = $this->post_image("tests/favicon.png", "pbx"); - $image_1 = null_throws(Image::by_id($image_id_1)); - $image_2 = null_throws(Image::by_id($image_id_2)); - $image_3 = null_throws(Image::by_id($image_id_3)); + $image_1 = Image::by_id($image_id_1); + $image_2 = Image::by_id($image_id_2); + $image_3 = Image::by_id($image_id_3); - $this->assertNull($image_1['parent_id']); - $this->assertNull($image_2['parent_id']); - $this->assertNull($image_3['parent_id']); - $this->assertFalse($image_1['has_children']); - $this->assertFalse($image_2['has_children']); - $this->assertFalse($image_3['has_children']); + $this->assertNull($image_1->parent_id); + $this->assertNull($image_2->parent_id); + $this->assertNull($image_3->parent_id); + $this->assertFalse($image_1->has_children); + $this->assertFalse($image_2->has_children); + $this->assertFalse($image_3->has_children); return [$image_1, $image_2, $image_3]; } /** - * @return array{0:Image, 1:Image, 2:Image} + * @depends testSetParentByTagBase */ - #[Depends('testSetParentByTagBase')] - public function testSetParentByTag(): array + public function testSetParentByTag($imgs): array { [$image_1, $image_2, $image_3] = $this->testSetParentByTagBase(); send_event(new TagSetEvent($image_2, ["pbx", "parent:{$image_1->id}"])); // refresh data from database - $image_1 = null_throws(Image::by_id($image_1->id)); - $image_2 = null_throws(Image::by_id($image_2->id)); - $image_3 = null_throws(Image::by_id($image_3->id)); + $image_1 = Image::by_id($image_1->id); + $image_2 = Image::by_id($image_2->id); + $image_3 = Image::by_id($image_3->id); $this->assertEquals(["pbx"], $image_2->get_tag_array()); - $this->assertNull($image_1['parent_id']); - $this->assertEquals($image_1->id, $image_2['parent_id']); - $this->assertNull($image_3['parent_id']); - $this->assertTrue($image_1['has_children']); - $this->assertFalse($image_2['has_children']); - $this->assertFalse($image_3['has_children']); + $this->assertNull($image_1->parent_id); + $this->assertEquals($image_1->id, $image_2->parent_id); + $this->assertNull($image_3->parent_id); + $this->assertTrue($image_1->has_children); + $this->assertFalse($image_2->has_children); + $this->assertFalse($image_3->has_children); return [$image_1, $image_2, $image_3]; } /** - * @return array{0:Image, 1:Image, 2:Image} + * @depends testSetParentByTag */ - #[Depends('testSetParentByTag')] - public function testSetChildByTag(): array + public function testSetChildByTag($imgs): array { - [$image_1, $image_2, $image_3] = $this->testSetParentByTag(); + [$image_1, $image_2, $image_3] = $this->testSetParentByTag(null); send_event(new TagSetEvent($image_3, ["pbx", "child:{$image_1->id}"])); // refresh data from database - $image_1 = null_throws(Image::by_id($image_1->id)); - $image_2 = null_throws(Image::by_id($image_2->id)); - $image_3 = null_throws(Image::by_id($image_3->id)); + $image_1 = Image::by_id($image_1->id); + $image_2 = Image::by_id($image_2->id); + $image_3 = Image::by_id($image_3->id); $this->assertEquals(["pbx"], $image_3->get_tag_array()); - $this->assertEquals($image_3->id, $image_1['parent_id']); - $this->assertEquals($image_1->id, $image_2['parent_id']); - $this->assertNull($image_3['parent_id']); - $this->assertTrue($image_1['has_children']); - $this->assertFalse($image_2['has_children']); - $this->assertTrue($image_3['has_children']); + $this->assertEquals($image_3->id, $image_1->parent_id); + $this->assertEquals($image_1->id, $image_2->parent_id); + $this->assertNull($image_3->parent_id); + $this->assertTrue($image_1->has_children); + $this->assertFalse($image_2->has_children); + $this->assertTrue($image_3->has_children); return [$image_1, $image_2, $image_3]; } - #[Depends('testSetChildByTag')] - public function testRemoveParentByTag(): void + /** + * @depends testSetChildByTag + */ + public function testRemoveParentByTag($imgs) { - [$image_1, $image_2, $image_3] = $this->testSetChildByTag(); + [$image_1, $image_2, $image_3] = $this->testSetChildByTag(null); + assert(!is_null($image_3)); // check parent is set - $this->assertEquals($image_2['parent_id'], $image_1->id); + $this->assertEquals($image_2->parent_id, $image_1->id); // un-set it send_event(new TagSetEvent($image_2, ["pbx", "parent:none"])); @@ -220,6 +215,6 @@ class RelationshipsTest extends ShimmiePHPUnitTestCase // check it was unset $this->assertEquals(["pbx"], $image_2->get_tag_array()); - $this->assertNull($image_2['parent_id']); + $this->assertNull($image_2->parent_id); } } diff --git a/ext/relationships/theme.php b/ext/relationships/theme.php index d2a61a1b..96e73cde 100644 --- a/ext/relationships/theme.php +++ b/ext/relationships/theme.php @@ -4,25 +4,21 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\{TR, TH, TD, emptyHTML, DIV, INPUT}; - class RelationshipsTheme extends Themelet { - public function relationship_info(Image $image): void + public function relationship_info(Image $image) { global $page, $database; - if ($image['parent_id'] !== null) { - $a = "parent post"; + if ($image->parent_id !== null) { + $a = "parent post"; $page->add_block(new Block(null, "This post belongs to a $a.", "main", 5, "ImageHasParent")); } - if (bool_escape($image['has_children'])) { - $ids = $database->get_col("SELECT id FROM images WHERE parent_id = :iid", ["iid" => $image->id]); + if (bool_escape($image->has_children)) { + $ids = $database->get_col("SELECT id FROM images WHERE parent_id = :iid", ["iid"=>$image->id]); - $html = "This post has ".(count($ids) > 1 ? "child posts" : "a child post").""; + $html = "This post has ".(count($ids) > 1 ? "child posts" : "a child post").""; $html .= " (post "; foreach ($ids as $id) { $html .= "#{$id}, "; @@ -33,15 +29,26 @@ class RelationshipsTheme extends Themelet } } - public function get_parent_editor_html(Image $image): HTMLElement + public function get_parent_editor_html(Image $image): string { global $user; - return SHM_POST_INFO( - "Parent", - strval($image['parent_id']) ?: "None", - !$user->is_anonymous() ? INPUT(["type" => "number", "name" => "tag_edit__parent", "value" => $image['parent_id']]) : null - ); + $h_parent_id = $image->parent_id; + $s_parent_id = $h_parent_id ?: "None"; + + $html = "

    \n". + " \n". + " \n"; + return $html; } diff --git a/ext/replace_file/info.php b/ext/replace_file/info.php deleted file mode 100644 index 86430f7a..00000000 --- a/ext/replace_file/info.php +++ /dev/null @@ -1,20 +0,0 @@ -page_matches("replace")) { - if (!$user->can(Permissions::REPLACE_IMAGE)) { - $this->theme->display_error(403, "Error", "{$user->name} doesn't have permission to replace images"); - return; - } - - $image_id = int_escape($event->get_arg(0)); - $image = Image::by_id($image_id); - if (is_null($image)) { - throw new UploadException("Can not replace Post: No post with ID $image_id"); - } - - if($event->method == "GET") { - $this->theme->display_replace_page($page, $image_id); - } elseif($event->method == "POST") { - if (!empty($_POST["url"])) { - $tmp_filename = shm_tempnam("transload"); - fetch_url($_POST["url"], $tmp_filename); - send_event(new ImageReplaceEvent($image, $tmp_filename)); - } elseif (count($_FILES) > 0) { - send_event(new ImageReplaceEvent($image, $_FILES["data"]['tmp_name'])); - } - if(!empty($_POST["source"])) { - send_event(new SourceSetEvent($image, $_POST["source"])); - } - $cache->delete("thumb-block:{$image_id}"); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/$image_id")); - } - } - } - - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void - { - global $user; - - /* In the future, could perhaps allow users to replace images that they own as well... */ - if ($user->can(Permissions::REPLACE_IMAGE)) { - $event->add_part($this->theme->get_replace_html($event->image->id)); - } - } - - public function onImageReplace(ImageReplaceEvent $event): void - { - $image = $event->image; - - $duplicate = Image::by_hash($event->new_hash); - if (!is_null($duplicate) && $duplicate->id != $image->id) { - throw new ImageReplaceException("A different post >>{$duplicate->id} already has hash {$duplicate->hash}"); - } - - $image->remove_image_only(); // Actually delete the old image file from disk - - $target = warehouse_path(Image::IMAGE_DIR, $event->new_hash); - if (!@copy($event->tmp_filename, $target)) { - $errors = error_get_last(); - throw new ImageReplaceException( - "Failed to copy file from uploads ({$event->tmp_filename}) to archive ($target): ". - "{$errors['type']} / {$errors['message']}" - ); - } - unlink($event->tmp_filename); - - // update metadata and save metadata to DB - $event->image->hash = $event->new_hash; - $event->image->filesize = filesize_ex($target); - $event->image->set_mime(MimeType::get_for_file($target)); - send_event(new MediaCheckPropertiesEvent($image)); - $image->save_to_db(); - - send_event(new ThumbnailGenerationEvent($image)); - - log_info("image", "Replaced >>{$image->id} {$event->old_hash} with {$event->new_hash}"); - } -} diff --git a/ext/replace_file/test.php b/ext/replace_file/test.php deleted file mode 100644 index aecd08be..00000000 --- a/ext/replace_file/test.php +++ /dev/null @@ -1,68 +0,0 @@ -log_in_as_admin(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - $this->get_page("replace/$image_id"); - $this->assert_title("Replace File"); - } - public function testReplace(): void - { - global $database; - $this->log_in_as_admin(); - - // upload an image - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - - // check that the image is original - $image = Image::by_id($image_id); - $old_hash = md5_file_ex("tests/pbx_screenshot.jpg"); - //$this->assertEquals("pbx_screenshot.jpg", $image->filename); - $this->assertEquals("image/jpeg", $image->get_mime()); - $this->assertEquals(19774, $image->filesize); - $this->assertEquals(640, $image->width); - $this->assertEquals($old_hash, $image->hash); - - // replace it - // create a copy because the file is deleted after upload - $tmpfile = shm_tempnam("test"); - copy("tests/favicon.png", $tmpfile); - $new_hash = md5_file_ex($tmpfile); - $_FILES = [ - 'data' => [ - 'name' => 'favicon.png', - 'type' => 'image/png', - 'tmp_name' => $tmpfile, - 'error' => 0, - 'size' => 246, - ] - ]; - $page = $this->post_page("replace/$image_id"); - $this->assert_response(302); - $this->assertEquals("/test/post/view/$image_id", $page->redirect); - - // check that there's still one image - $this->assertEquals(1, $database->get_one("SELECT COUNT(*) FROM images")); - - // check that the image was replaced - $image = Image::by_id($image_id); - // $this->assertEquals("favicon.png", $image->filename); // TODO should we update filename? - $this->assertEquals("image/png", $image->get_mime()); - $this->assertEquals(246, $image->filesize); - $this->assertEquals(16, $image->width); - $this->assertEquals(md5_file("tests/favicon.png"), $image->hash); - - // check that new files exist and old files don't - $this->assertFalse(file_exists(warehouse_path(Image::IMAGE_DIR, $old_hash))); - $this->assertFalse(file_exists(warehouse_path(Image::THUMBNAIL_DIR, $old_hash))); - $this->assertTrue(file_exists(warehouse_path(Image::IMAGE_DIR, $new_hash))); - $this->assertTrue(file_exists(warehouse_path(Image::THUMBNAIL_DIR, $new_hash))); - } -} diff --git a/ext/replace_file/theme.php b/ext/replace_file/theme.php deleted file mode 100644 index 8fbbb4ca..00000000 --- a/ext/replace_file/theme.php +++ /dev/null @@ -1,80 +0,0 @@ -get_string(UploadConfig::TRANSLOAD_ENGINE, "none") != "none"); - $accept = $this->get_accept(); - - $max_size = $config->get_int(UploadConfig::SIZE); - $max_kb = to_shorthand_int($max_size); - - $image = Image::by_id($image_id); - $thumbnail = $this->build_thumb_html($image); - - $form = SHM_FORM("replace/".$image_id, "POST", true); - $form->appendChild(emptyHTML( - TABLE( - ["id" => "large_upload_form", "class" => "form"], - TR( - TD("File"), - TD(INPUT(["name" => "data", "type" => "file", "accept" => $accept])) - ), - $tl_enabled ? TR( - TD("or URL"), - TD(INPUT(["name" => "url", "type" => "text", "value" => @$_GET['url']])) - ) : null, - TR(TD("Source"), TD(["colspan" => 3], INPUT(["name" => "source", "type" => "text"]))), - TR(TD(["colspan" => 4], INPUT(["id" => "uploadbutton", "type" => "submit", "value" => "Post"]))), - ) - )); - - $html = emptyHTML( - P( - "Replacing Post ID $image_id", - BR(), - "Please note: You will have to refresh the post page, or empty your browser cache." - ), - $thumbnail, - BR(), - $form, - $max_size > 0 ? SMALL("(Max file size is $max_kb)") : null, - ); - - $page->set_title("Replace File"); - $page->set_heading("Replace File"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Upload Replacement File", $html, "main", 20)); - } - - /** - * Display link to replace the image - */ - public function get_replace_html(int $image_id): \MicroHTML\HTMLElement - { - $form = SHM_FORM("replace/$image_id", "GET"); - $form->appendChild(INPUT(["type" => 'submit', "value" => 'Replace'])); - return $form; - } - - protected function get_accept(): string - { - return ".".join(",.", DataHandlerExtension::get_all_supported_exts()); - } -} diff --git a/ext/report_image/info.php b/ext/report_image/info.php index 1cf3439d..3a60a0f6 100644 --- a/ext/report_image/info.php +++ b/ext/report_image/info.php @@ -11,7 +11,7 @@ class ReportImageInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Report Posts"; public string $url = "http://atravelinggeek.com/"; - public array $authors = ["ATravelingGeek" => "atg@atravelinggeek.com"]; + public array $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Report posts as dupes/illegal/etc"; public ?string $version = "0.3a"; diff --git a/ext/report_image/main.php b/ext/report_image/main.php index 146963a1..81ee53d8 100644 --- a/ext/report_image/main.php +++ b/ext/report_image/main.php @@ -40,15 +40,12 @@ class ImageReport } } -/** - * @phpstan-type Report array{id: int, image: Image, reason: string, reporter_name: string} - */ class ReportImage extends Extension { /** @var ReportImageTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("image_report")) { @@ -85,26 +82,26 @@ class ReportImage extends Extension } } - public function onAddReportedImage(AddReportedImageEvent $event): void + public function onAddReportedImage(AddReportedImageEvent $event) { global $cache, $database; log_info("report_image", "Adding report of >>{$event->report->image_id} with reason '{$event->report->reason}'"); $database->execute( "INSERT INTO image_reports(image_id, reporter_id, reason) VALUES (:image_id, :reporter_id, :reason)", - ['image_id' => $event->report->image_id, 'reporter_id' => $event->report->user_id, 'reason' => $event->report->reason] + ['image_id'=>$event->report->image_id, 'reporter_id'=>$event->report->user_id, 'reason'=>$event->report->reason] ); $cache->delete("image-report-count"); } - public function onRemoveReportedImage(RemoveReportedImageEvent $event): void + public function onRemoveReportedImage(RemoveReportedImageEvent $event) { global $cache, $database; - $database->execute("DELETE FROM image_reports WHERE id = :id", ["id" => $event->id]); + $database->execute("DELETE FROM image_reports WHERE id = :id", ["id"=>$event->id]); $cache->delete("image-report-count"); } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onUserPageBuilding(UserPageBuildingEvent $event) { global $user; if ($user->can(Permissions::VIEW_IMAGE_REPORT)) { @@ -112,7 +109,7 @@ class ReportImage extends Extension } } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $user; if ($user->can(Permissions::CREATE_IMAGE_REPORT)) { @@ -122,10 +119,10 @@ class ReportImage 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::VIEW_IMAGE_REPORT)) { $count = $this->count_reported_images(); $h_count = $count > 0 ? " ($count)" : ""; @@ -135,7 +132,7 @@ class ReportImage extends Extension } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::VIEW_IMAGE_REPORT)) { @@ -145,19 +142,19 @@ class ReportImage extends Extension } } - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { global $cache, $database; - $database->execute("DELETE FROM image_reports WHERE image_id = :image_id", ["image_id" => $event->image->id]); + $database->execute("DELETE FROM image_reports WHERE image_id = :image_id", ["image_id"=>$event->image->id]); $cache->delete("image-report-count"); } - public function onUserDeletion(UserDeletionEvent $event): void + public function onUserDeletion(UserDeletionEvent $event) { $this->delete_reports_by($event->id); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Post Reports"); @@ -170,14 +167,14 @@ class ReportImage extends Extension $sb->add_choice_option("report_image_publicity", $opts, "Show publicly: "); } - public function delete_reports_by(int $user_id): void + public function delete_reports_by(int $user_id) { global $cache, $database; - $database->execute("DELETE FROM image_reports WHERE reporter_id=:reporter_id", ['reporter_id' => $user_id]); + $database->execute("DELETE FROM image_reports WHERE reporter_id=:reporter_id", ['reporter_id'=>$user_id]); $cache->delete("image-report-count"); } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -195,7 +192,7 @@ class ReportImage extends Extension } /** - * @return ImageReport[] + * #return ImageReport[] */ public function get_reports(Image $image): array { @@ -215,9 +212,6 @@ class ReportImage extends Extension return $reps; } - /** - * @return array - */ public function get_reported_images(): array { global $database; @@ -248,12 +242,14 @@ class ReportImage extends Extension public function count_reported_images(): int { - global $database; + global $cache, $database; - return (int)cache_get_or_set( - "image-report-count", - fn () => $database->get_one("SELECT count(*) FROM image_reports"), - 600 - ); + $count = $cache->get("image-report-count"); + if (is_null($count)) { + $count = $database->get_one("SELECT count(*) FROM image_reports"); + $cache->set("image-report-count", $count, 600); + } + + return (int)$count; } } diff --git a/ext/report_image/style.css b/ext/report_image/style.css index ad5f881c..7fd0e56a 100644 --- a/ext/report_image/style.css +++ b/ext/report_image/style.css @@ -3,6 +3,3 @@ overflow-wrap: break-word; word-wrap: break-word; } -#reportedimage .formstretch INPUT { - width: 100%; -} \ No newline at end of file diff --git a/ext/report_image/test.php b/ext/report_image/test.php index 4d3f7d03..57996eaf 100644 --- a/ext/report_image/test.php +++ b/ext/report_image/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class ReportImageTest extends ShimmiePHPUnitTestCase { - public function testReportImage(): void + public function testReportImage() { global $config, $database, $user; diff --git a/ext/report_image/theme.php b/ext/report_image/theme.php index 3560a7e6..82dd33c2 100644 --- a/ext/report_image/theme.php +++ b/ext/report_image/theme.php @@ -6,15 +6,9 @@ namespace Shimmie2; use function MicroHTML\INPUT; -/** - * @phpstan-type Report array{id: int, image: Image, reason: string, reporter_name: string} - */ class ReportImageTheme extends Themelet { - /** - * @param array $reports - */ - public function display_reported_images(Page $page, array $reports): void + public function display_reported_images(Page $page, array $reports) { global $config, $user; @@ -29,19 +23,19 @@ class ReportImageTheme extends Themelet $iabbe = send_event(new ImageAdminBlockBuildingEvent($image, $user, "report")); ksort($iabbe->parts); - $actions = join("", $iabbe->parts); + $actions = join("
    ", $iabbe->parts); $h_reportedimages .= "
    - "; @@ -62,9 +56,9 @@ class ReportImageTheme extends Themelet } /** - * @param ImageReport[] $reports + * #param ImageReport[] $reports */ - public function display_image_banner(Image $image, array $reports): void + public function display_image_banner(Image $image, array $reports) { global $config, $page; @@ -97,12 +91,12 @@ class ReportImageTheme extends Themelet $page->add_block(new Block("Report Post", $html, "left")); } - public function get_nuller(User $duser): void + public function get_nuller(User $duser) { global $page; $html = (string)SHM_SIMPLE_FORM( "image_report/remove_reports_by", - INPUT(["type" => 'hidden', "name" => 'user_id', "value" => $duser->id]), + INPUT(["type"=>'hidden', "name"=>'user_id', "value"=>$duser->id]), SHM_SUBMIT('Delete all reports by this user') ); $page->add_block(new Block("Reports", $html, "main", 80)); diff --git a/ext/res_limit/main.php b/ext/res_limit/main.php index 62a3b2ce..69c61f33 100644 --- a/ext/res_limit/main.php +++ b/ext/res_limit/main.php @@ -11,15 +11,14 @@ class ResolutionLimit extends Extension return 40; } // early, to veto ImageUploadEvent - public function onImageAddition(ImageAdditionEvent $event): void + public function onImageAddition(ImageAdditionEvent $event) { global $config; $min_w = $config->get_int("upload_min_width", -1); $min_h = $config->get_int("upload_min_height", -1); $max_w = $config->get_int("upload_max_width", -1); $max_h = $config->get_int("upload_max_height", -1); - $rs = $config->get_string("upload_ratios", ""); - $ratios = trim($rs) ? explode(" ", $rs) : []; + $ratios = explode(" ", $config->get_string("upload_ratios", "")); $image = $event->image; @@ -55,13 +54,13 @@ class ResolutionLimit extends Extension if ($valids > 0 && !$ok) { throw new UploadException( "Post needs to be in one of these ratios: ". - $config->get_string("upload_ratios", "") + html_escape($config->get_string("upload_ratios", "")) ); } } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Resolution Limits"); diff --git a/ext/res_limit/test.php b/ext/res_limit/test.php index 618a15ff..7bb9ee49 100644 --- a/ext/res_limit/test.php +++ b/ext/res_limit/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class ResolutionLimitTest extends ShimmiePHPUnitTestCase { - public function testResLimitOK(): void + public function testResLimitOK() { global $config; $config->set_int("upload_min_height", 0); @@ -23,7 +23,7 @@ class ResolutionLimitTest extends ShimmiePHPUnitTestCase $this->assert_no_text("ratio"); } - public function testResLimitSmall(): void + public function testResLimitSmall() { global $config; $config->set_int("upload_min_height", 900); @@ -33,13 +33,15 @@ class ResolutionLimitTest extends ShimmiePHPUnitTestCase $config->set_string("upload_ratios", "4:3 16:9"); $this->log_in_as_user(); - $e = $this->assertException(UploadException::class, function () { + try { $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - }); - $this->assertEquals("Post too small", $e->getMessage()); + $this->fail("Invalid-size image was allowed"); + } catch (UploadException $e) { + $this->assertEquals("Post too small", $e->getMessage()); + } } - public function testResLimitLarge(): void + public function testResLimitLarge() { global $config; $config->set_int("upload_min_height", 0); @@ -48,13 +50,15 @@ class ResolutionLimitTest extends ShimmiePHPUnitTestCase $config->set_int("upload_max_width", 100); $config->set_string("upload_ratios", "4:3 16:9"); - $e = $this->assertException(UploadException::class, function () { + try { $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - }); - $this->assertEquals("Post too large", $e->getMessage()); + $this->fail("Invalid-size image was allowed"); + } catch (UploadException $e) { + $this->assertEquals("Post too large", $e->getMessage()); + } } - public function testResLimitRatio(): void + public function testResLimitRatio() { global $config; $config->set_int("upload_min_height", -1); @@ -63,10 +67,12 @@ class ResolutionLimitTest extends ShimmiePHPUnitTestCase $config->set_int("upload_max_width", -1); $config->set_string("upload_ratios", "16:9"); - $e = $this->assertException(UploadException::class, function () { + try { $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - }); - $this->assertEquals("Post needs to be in one of these ratios: 16:9", $e->getMessage()); + $this->fail("Invalid-size image was allowed"); + } catch (UploadException $e) { + $this->assertEquals("Post needs to be in one of these ratios: 16:9", $e->getMessage()); + } } # reset to defaults, otherwise this can interfere with diff --git a/ext/resize/info.php b/ext/resize/info.php index 5b2bdfd9..b7703588 100644 --- a/ext/resize/info.php +++ b/ext/resize/info.php @@ -16,7 +16,7 @@ class ResizeImageInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Resize Post"; - public array $authors = ["jgen" => "jgen.tech@gmail.com"]; + public array $authors = ["jgen"=>"jgen.tech@gmail.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "This extension allows admins to resize images."; public ?string $version = "0.1"; diff --git a/ext/resize/main.php b/ext/resize/main.php index 83f4beac..543c2437 100644 --- a/ext/resize/main.php +++ b/ext/resize/main.php @@ -31,7 +31,7 @@ class ResizeImage extends Extension } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool(ResizeConfig::ENABLED, true); @@ -42,7 +42,7 @@ class ResizeImage extends Extension $config->set_default_int(ResizeConfig::DEFAULT_HEIGHT, 0); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user, $config; if ($user->can(Permissions::EDIT_FILES) && $config->get_bool(ResizeConfig::ENABLED) @@ -52,7 +52,7 @@ class ResizeImage extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Image Resize"); $sb->start_table(); @@ -73,13 +73,14 @@ class ResizeImage extends Extension $sb->end_table(); } - public function onDataUpload(DataUploadEvent $event): void + public function onDataUpload(DataUploadEvent $event) { global $config, $page; + $image_obj = Image::by_id($event->image_id); + if ($config->get_bool(ResizeConfig::UPLOAD) == true && $this->can_resize_mime($event->mime)) { - $image_obj = $event->images[0]; $width = $height = 0; if ($config->get_int(ResizeConfig::DEFAULT_WIDTH) !== 0) { @@ -94,7 +95,7 @@ class ResizeImage extends Extension if (($fh = @fopen($image_filename, 'rb'))) { //check if gif is animated (via https://www.php.net/manual/en/function.imagecreatefromgif.php#104473) while (!feof($fh) && $isanigif < 2) { - $chunk = false_throws(fread($fh, 1024 * 100)); + $chunk = fread($fh, 1024 * 100); $isanigif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches); } } @@ -108,16 +109,16 @@ class ResizeImage extends Extension //Need to generate thumbnail again... //This only seems to be an issue if one of the sizes was set to 0. - $image_obj = Image::by_id($image_obj->id); //Must be a better way to grab the new hash than setting this again.. - send_event(new ThumbnailGenerationEvent($image_obj, true)); + $image_obj = Image::by_id($event->image_id); //Must be a better way to grab the new hash than setting this again.. + send_event(new ThumbnailGenerationEvent($image_obj->hash, $image_obj->get_mime(), true)); - log_info("resize", ">>{$image_obj->id} has been resized to: ".$width."x".$height); + log_info("resize", ">>{$event->image_id} has been resized to: ".$width."x".$height); //TODO: Notify user that image has been resized. } } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -164,7 +165,7 @@ class ResizeImage extends Extension } } - public function onImageDownloading(ImageDownloadingEvent $event): void + public function onImageDownloading(ImageDownloadingEvent $event) { global $config, $user; @@ -185,8 +186,8 @@ class ResizeImage extends Extension [$new_width, $new_height] = get_scaled_by_aspect_ratio($event->image->width, $event->image->height, $max_width, $max_height); - if ($new_width !== $event->image->width || $new_height !== $event->image->height) { - $tmp_filename = shm_tempnam('resize'); + if ($new_width!==$event->image->width || $new_height !==$event->image->height) { + $tmp_filename = tempnam(sys_get_temp_dir(), 'shimmie_resize'); if (empty($tmp_filename)) { throw new ImageResizeException("Unable to save temporary image file."); } @@ -200,7 +201,7 @@ class ResizeImage extends Extension $new_height )); - if ($event->file_modified === true && $event->path != $event->image->get_image_filename()) { + if ($event->file_modified===true&&$event->path!=$event->image->get_image_filename()) { // This means that we're dealing with a temp file that will need cleaned up unlink($event->path); } @@ -211,7 +212,7 @@ class ResizeImage extends Extension } } - private function can_resize_mime(string $mime): bool + private function can_resize_mime($mime): bool { global $config; $engine = $config->get_string(ResizeConfig::ENGINE); @@ -222,7 +223,7 @@ class ResizeImage extends Extension // Private functions /* ----------------------------- */ - private function resize_image(Image $image_obj, int $width, int $height): void + private function resize_image(Image $image_obj, int $width, int $height) { global $config; @@ -240,7 +241,7 @@ class ResizeImage extends Extension $hash = $image_obj->hash; $image_filename = warehouse_path(Image::IMAGE_DIR, $hash); - $info = false_throws(getimagesize($image_filename)); + $info = getimagesize($image_filename); if (($image_obj->width != $info[0]) || ($image_obj->height != $info[1])) { throw new ImageResizeException("The current image size does not match what is set in the database! - Aborting Resize."); } @@ -248,7 +249,7 @@ class ResizeImage extends Extension list($new_height, $new_width) = $this->calc_new_size($image_obj, $width, $height); /* Temp storage while we resize */ - $tmp_filename = shm_tempnam('resize'); + $tmp_filename = tempnam(sys_get_temp_dir(), 'shimmie_resize'); if (empty($tmp_filename)) { throw new ImageResizeException("Unable to save temporary image file."); } @@ -263,13 +264,29 @@ class ResizeImage extends Extension Media::RESIZE_TYPE_STRETCH )); - send_event(new ImageReplaceEvent($image_obj, $tmp_filename)); + $new_image = new Image(); + $new_image->hash = md5_file($tmp_filename); + $new_image->filesize = filesize($tmp_filename); + $new_image->filename = 'resized-'.$image_obj->filename; + $new_image->width = $new_width; + $new_image->height = $new_height; - log_info("resize", "Resized >>{$image_obj->id} - New hash: {$image_obj->hash}"); + /* Move the new image into the main storage location */ + $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash); + if (!@copy($tmp_filename, $target)) { + throw new ImageResizeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); + } + + /* Remove temporary file */ + @unlink($tmp_filename); + + send_event(new ImageReplaceEvent($image_obj->id, $new_image)); + + log_info("resize", "Resized >>{$image_obj->id} - New hash: {$new_image->hash}"); } /** - * @return int[] + * #return int[] */ private function calc_new_size(Image $image_obj, int $width, int $height): array { @@ -288,8 +305,8 @@ class ResizeImage extends Extension $factor = min($width / $image_obj->width, $height / $image_obj->height); } - $new_width = (int)round($image_obj->width * $factor); - $new_height = (int)round($image_obj->height * $factor); + $new_width = round($image_obj->width * $factor); + $new_height = round($image_obj->height * $factor); return [$new_height, $new_width]; } } diff --git a/ext/resize/theme.php b/ext/resize/theme.php index 04900c96..44937b1f 100644 --- a/ext/resize/theme.php +++ b/ext/resize/theme.php @@ -4,14 +4,12 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\{rawHTML}; - class ResizeImageTheme extends Themelet { /* * Display a link to resize an image */ - public function get_resize_html(Image $image): \MicroHTML\HTMLElement + public function get_resize_html(Image $image): string { global $config; @@ -25,7 +23,7 @@ class ResizeImageTheme extends Themelet $default_height = $image->height; } - $html = rawHTML(" + $html = " ".make_form(make_link("resize/{$image->id}"), 'POST')." @@ -35,12 +33,12 @@ class ResizeImageTheme extends Themelet

    - "); + "; return $html; } - public function display_resize_error(Page $page, string $title, string $message): void + public function display_resize_error(Page $page, string $title, string $message) { $page->set_title("Resize Image"); $page->set_heading("Resize Image"); diff --git a/ext/reverse_search_links/icons/ascii2d.ico b/ext/reverse_search_links/icons/ascii2d.ico deleted file mode 100644 index 29e6fd12..00000000 Binary files a/ext/reverse_search_links/icons/ascii2d.ico and /dev/null differ diff --git a/ext/reverse_search_links/icons/saucenao.ico b/ext/reverse_search_links/icons/saucenao.ico deleted file mode 100644 index 128dde64..00000000 Binary files a/ext/reverse_search_links/icons/saucenao.ico and /dev/null differ diff --git a/ext/reverse_search_links/icons/tineye.ico b/ext/reverse_search_links/icons/tineye.ico deleted file mode 100644 index 184b8793..00000000 Binary files a/ext/reverse_search_links/icons/tineye.ico and /dev/null differ diff --git a/ext/reverse_search_links/icons/trace.moe.ico b/ext/reverse_search_links/icons/trace.moe.ico deleted file mode 100644 index ff23d4f1..00000000 Binary files a/ext/reverse_search_links/icons/trace.moe.ico and /dev/null differ diff --git a/ext/reverse_search_links/icons/yandex.ico b/ext/reverse_search_links/icons/yandex.ico deleted file mode 100644 index 984e71c0..00000000 Binary files a/ext/reverse_search_links/icons/yandex.ico and /dev/null differ diff --git a/ext/reverse_search_links/info.php b/ext/reverse_search_links/info.php deleted file mode 100644 index 15f6164b..00000000 --- a/ext/reverse_search_links/info.php +++ /dev/null @@ -1,18 +0,0 @@ - 'joe@thisisjoes.site']; - public string $license = self::LICENSE_GPLV2; - public string $description = "Provides reverse search links for images."; - public ?string $documentation = "Click on an icon in the 'Reverse Image Search' block to search for the image using the corresponding service. This may be useful to find the original source or author of an image.
    - Options for which services to show and the position and priority of the block are available for admins on the config page."; -} diff --git a/ext/reverse_search_links/main.php b/ext/reverse_search_links/main.php deleted file mode 100644 index 92fcfe8d..00000000 --- a/ext/reverse_search_links/main.php +++ /dev/null @@ -1,56 +0,0 @@ -image->get_mime(), $supported_types)) { - $this->theme->reverse_search_block($page, $event->image); - } - } - - - /** - * Supported reverse search services - * - * @var string[] - */ - protected array $SERVICES = [ - 'SauceNAO', - 'TinEye', - 'trace.moe', - 'ascii2d', - 'Yandex' - ]; - - /** - * Set default config values - */ - public function onInitExt(InitExtEvent $event): void - { - global $config; - $config->set_default_array( - ReverseSearchLinksConfig::ENABLED_SERVICES, - ['SauceNAO', 'TinEye', 'trace.moe', 'ascii2d', 'Yandex'] - ); - } -} diff --git a/ext/reverse_search_links/style.css b/ext/reverse_search_links/style.css deleted file mode 100644 index 36cab20a..00000000 --- a/ext/reverse_search_links/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.reverse_image_link { - padding: 0 5px; -} diff --git a/ext/reverse_search_links/theme.php b/ext/reverse_search_links/theme.php deleted file mode 100644 index dfff6ea7..00000000 --- a/ext/reverse_search_links/theme.php +++ /dev/null @@ -1,34 +0,0 @@ - 'https://saucenao.com/search.php?url=' . url_escape(make_http($image->get_thumb_link())), - 'TinEye' => 'https://www.tineye.com/search/?url=' . url_escape(make_http($image->get_thumb_link())), - 'trace.moe' => 'https://trace.moe/?auto&url=' . url_escape(make_http($image->get_thumb_link())), - 'ascii2d' => 'https://ascii2d.net/search/url/' . url_escape(make_http($image->get_thumb_link())), - 'Yandex' => 'https://yandex.com/images/search?rpt=imageview&url=' . url_escape(make_http($image->get_thumb_link())) - ]; - - // only generate links for enabled reverse search services - $enabled_services = $config->get_array(ReverseSearchLinksConfig::ENABLED_SERVICES); - - $html = ""; - foreach($links as $name => $link) { - if (in_array($name, $enabled_services)) { - $icon_link = make_link("/ext/reverse_search_links/icons/" . strtolower($name) . ".ico"); - $html .= "$name icon"; - } - } - - $page->add_block(new Block("Reverse Image Search", $html, "main", 20)); - } -} diff --git a/ext/rotate/info.php b/ext/rotate/info.php index 66ae6281..51c530d4 100644 --- a/ext/rotate/info.php +++ b/ext/rotate/info.php @@ -16,7 +16,7 @@ class RotateImageInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Rotate Image"; - public array $authors = ["jgen" => "jgen.tech@gmail.com","Agasa" => "hiroshiagasa@gmail.com"]; + public array $authors = ["jgen"=>"jgen.tech@gmail.com","Agasa"=>"hiroshiagasa@gmail.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Allows admins to rotate images."; } diff --git a/ext/rotate/main.php b/ext/rotate/main.php index 723be202..f8dcfa45 100644 --- a/ext/rotate/main.php +++ b/ext/rotate/main.php @@ -23,14 +23,14 @@ class RotateImage extends Extension public const SUPPORTED_MIME = [MimeType::JPEG, MimeType::PNG, MimeType::GIF, MimeType::WEBP]; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool('rotate_enabled', true); $config->set_default_int('rotate_default_deg', 180); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user, $config; if ($user->can(Permissions::EDIT_FILES) && $config->get_bool("rotate_enabled") @@ -40,7 +40,7 @@ class RotateImage extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Image Rotate"); $sb->add_bool_option("rotate_enabled", "Allow rotating images: "); @@ -49,7 +49,7 @@ class RotateImage extends Extension $sb->add_label(" deg"); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -96,7 +96,7 @@ class RotateImage extends Extension // Private functions /* ----------------------------- */ - private function rotate_image(int $image_id, int $deg): void + private function rotate_image(int $image_id, int $deg) { if (($deg <= -360) || ($deg >= 360)) { throw new ImageRotateException("Invalid options for rotation angle. ($deg)"); @@ -104,13 +104,16 @@ class RotateImage extends Extension $image_obj = Image::by_id($image_id); $hash = $image_obj->hash; + if (is_null($hash)) { + throw new ImageRotateException("Post does not have a hash associated with it."); + } $image_filename = warehouse_path(Image::IMAGE_DIR, $hash); - if (file_exists($image_filename) === false) { + if (file_exists($image_filename)===false) { throw new ImageRotateException("$image_filename does not exist."); } - $info = false_throws(getimagesize($image_filename)); + $info = getimagesize($image_filename); $memory_use = Media::calc_memory_use($info); $memory_limit = get_memory_limit(); @@ -121,7 +124,7 @@ class RotateImage extends Extension /* Attempt to load the image */ - $image = imagecreatefromstring(file_get_contents_ex($image_filename)); + $image = imagecreatefromstring(file_get_contents($image_filename)); if ($image == false) { throw new ImageRotateException("Could not load image: ".$image_filename); } @@ -133,17 +136,17 @@ class RotateImage extends Extension $background_color = imagecolorallocatealpha($image, 0, 0, 0, 127); break; } - if ($background_color === false) { + if ($background_color===false) { throw new ImageRotateException("Unable to allocate transparent color"); } $image_rotated = imagerotate($image, $deg, $background_color); - if ($image_rotated === false) { + if ($image_rotated===false) { throw new ImageRotateException("Image rotate failed"); } /* Temp storage while we rotate */ - $tmp_filename = shm_tempnam('rotate'); + $tmp_filename = tempnam(ini_get('upload_tmp_dir'), 'shimmie_rotate'); if (empty($tmp_filename)) { throw new ImageRotateException("Unable to save temporary image file."); } @@ -169,19 +172,31 @@ class RotateImage extends Extension throw new ImageRotateException("Unsupported image type."); } - if ($result === false) { + if ($result===false) { throw new ImageRotateException("Could not save image: ".$tmp_filename); } - $new_hash = md5_file_ex($tmp_filename); + list($new_width, $new_height) = getimagesize($tmp_filename); + + $new_image = new Image(); + $new_image->hash = md5_file($tmp_filename); + $new_image->filesize = filesize($tmp_filename); + $new_image->filename = 'rotated-'.$image_obj->filename; + $new_image->width = $new_width; + $new_image->height = $new_height; + $new_image->posted = $image_obj->posted; + /* Move the new image into the main storage location */ - $target = warehouse_path(Image::IMAGE_DIR, $new_hash); + $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash); if (!@copy($tmp_filename, $target)) { throw new ImageRotateException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); } - send_event(new ImageReplaceEvent($image_obj, $tmp_filename)); + /* Remove temporary file */ + @unlink($tmp_filename); - log_info("rotate", "Rotated >>{$image_id} - New hash: {$new_hash}"); + send_event(new ImageReplaceEvent($image_id, $new_image)); + + log_info("rotate", "Rotated >>{$image_id} - New hash: {$new_image->hash}"); } } diff --git a/ext/rotate/theme.php b/ext/rotate/theme.php index 703006ea..a8a08a26 100644 --- a/ext/rotate/theme.php +++ b/ext/rotate/theme.php @@ -11,20 +11,20 @@ class RotateImageTheme extends Themelet /** * Display a link to rotate an image. */ - public function get_rotate_html(int $image_id): \MicroHTML\HTMLElement + public function get_rotate_html(int $image_id): string { - return SHM_SIMPLE_FORM( + return (string)SHM_SIMPLE_FORM( 'rotate/'.$image_id, - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $image_id]), - INPUT(["type" => 'number', "name" => 'rotate_deg', "id" => "rotate_deg", "placeholder" => "Rotation degrees"]), - INPUT(["type" => 'submit', "value" => 'Rotate', "id" => "rotatebutton"]), + INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image_id]), + INPUT(["type"=>'number', "name"=>'rotate_deg', "id"=>"rotate_deg", "placeholder"=>"Rotation degrees"]), + INPUT(["type"=>'submit', "value"=>'Rotate', "id"=>"rotatebutton"]), ); } /** * Display the error. */ - public function display_rotate_error(Page $page, string $title, string $message): void + public function display_rotate_error(Page $page, string $title, string $message) { $page->set_title("Rotate Image"); $page->set_heading("Rotate Image"); diff --git a/ext/rss_comments/main.php b/ext/rss_comments/main.php index 6b487ff0..5afadf6b 100644 --- a/ext/rss_comments/main.php +++ b/ext/rss_comments/main.php @@ -6,7 +6,7 @@ namespace Shimmie2; class RSSComments extends Extension { - public function onPostListBuilding(PostListBuildingEvent $event): void + public function onPostListBuilding(PostListBuildingEvent $event) { global $config, $page; $title = $config->get_string(SetupConfig::TITLE); @@ -15,7 +15,7 @@ class RSSComments extends Extension "title=\"$title - Comments\" href=\"".make_link("rss/comments")."\" />"); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $database, $page; if ($event->page_matches("rss/comments")) { @@ -40,7 +40,7 @@ class RSSComments extends Extension $comment_id = $comment['comment_id']; $link = make_http(make_link("post/view/$image_id")); $owner = html_escape($comment['user_name']); - $posted = date(DATE_RSS, strtotime_ex($comment['posted'])); + $posted = date(DATE_RSS, strtotime($comment['posted'])); $comment = html_escape($comment['comment']); $content = html_escape("$owner: $comment"); @@ -75,9 +75,9 @@ EOD; } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "comment") { + if ($event->parent=="comment") { $event->add_nav_link("comment_rss", new Link('rss/comments'), "Feed"); } } diff --git a/ext/rss_comments/test.php b/ext/rss_comments/test.php index 868f143c..c3ce163e 100644 --- a/ext/rss_comments/test.php +++ b/ext/rss_comments/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class RSSCommentsTest extends ShimmiePHPUnitTestCase { - public function testImageFeed(): void + public function testImageFeed() { global $user; $this->log_in_as_user(); diff --git a/ext/rss_images/main.php b/ext/rss_images/main.php index a61096cc..5cccc51c 100644 --- a/ext/rss_images/main.php +++ b/ext/rss_images/main.php @@ -6,13 +6,13 @@ namespace Shimmie2; class RSSImages extends Extension { - public function onPostListBuilding(PostListBuildingEvent $event): void + public function onPostListBuilding(PostListBuildingEvent $event) { global $config, $page; $title = $config->get_string(SetupConfig::TITLE); if (count($event->search_terms) > 0) { - $search = url_escape(Tag::implode($event->search_terms)); + $search = url_escape(Tag::caret(Tag::implode($event->search_terms))); $page->add_html_header(""); } else { @@ -21,7 +21,7 @@ class RSSImages extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { if ($event->page_matches("rss/images")) { $search_terms = $event->get_search_terms(); @@ -31,7 +31,7 @@ class RSSImages extends Extension return; } try { - $images = Search::find_images(($page_number - 1) * $page_size, $page_size, $search_terms); + $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); $this->do_rss($images, $search_terms, $page_number); } catch (SearchTermParseException $stpe) { $this->theme->display_error(400, "Search parse error", $stpe->error); @@ -41,17 +41,13 @@ class RSSImages extends Extension } } - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { global $cache; $cache->delete("rss-item-image:{$event->image->id}"); } - /** - * @param Image[] $images - * @param string[] $search_terms - */ - private function do_rss(array $images, array $search_terms, int $page_number): void + private function do_rss(array $images, array $search_terms, int $page_number) { global $page; global $config; @@ -71,12 +67,12 @@ class RSSImages extends Extension } if ($page_number > 1) { - $prev_url = make_link("rss/images/$search".($page_number - 1)); + $prev_url = make_link("rss/images/$search".($page_number-1)); $prev_link = ""; } else { $prev_link = ""; } - $next_url = make_link("rss/images/$search".($page_number + 1)); + $next_url = make_link("rss/images/$search".($page_number+1)); $next_link = ""; // no end... $version = VERSION; @@ -109,7 +105,7 @@ class RSSImages extends Extension $tags = html_escape($image->get_tag_list()); $thumb_url = $image->get_thumb_link(); $image_url = $image->get_image_link(); - $posted = date(DATE_RSS, strtotime_ex($image->posted)); + $posted = date(DATE_RSS, strtotime($image->posted)); $content = html_escape( "
    " . "

    " . $this->theme->build_thumb_html($image) . "

    " . @@ -133,9 +129,9 @@ class RSSImages extends Extension return $data; } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "posts") { + if ($event->parent=="posts") { $event->add_nav_link("posts_rss", new Link('rss/images'), "Feed"); } } diff --git a/ext/rss_images/test.php b/ext/rss_images/test.php index e23c40a4..89dc8acc 100644 --- a/ext/rss_images/test.php +++ b/ext/rss_images/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class RSSImagesTest extends ShimmiePHPUnitTestCase { - public function testImageFeed(): void + public function testImageFeed() { $this->log_in_as_user(); $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); diff --git a/ext/rule34/info.php b/ext/rule34/info.php new file mode 100644 index 00000000..cd01f775 --- /dev/null +++ b/ext/rule34/info.php @@ -0,0 +1,20 @@ +notify("shm_image_bans", $event->image->hash); + } + + public function onImageInfoSet(ImageInfoSetEvent $event) + { + global $cache; + $cache->delete("thumb-block:{$event->image->id}"); + } + + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) + { + global $config; + $image_link = $config->get_string(ImageConfig::ILINK); + $url0 = $event->image->parse_link_template($image_link, 0); + $url1 = $event->image->parse_link_template($image_link, 1); + $html = (string)TR( + TH("Links"), + TD( + A(["href"=>$url0], "File Only"), + " (", + A(["href"=>$url1], "Backup Server"), + ")" + ) + ); + $event->add_part($html, 90); + } + + public function onAdminBuilding(AdminBuildingEvent $event) + { + global $page; + $html = make_form(make_link("admin/cache_purge"), "POST"); + $html .= ""; + $html .= "
    "; + $html .= "\n"; + $page->add_block(new Block("Cache Purger", $html)); + } + + public function onUserPageBuilding(UserPageBuildingEvent $event) + { + global $database, $user, $config; + if ($user->can(Permissions::CHANGE_SETTING) && $config->get_bool('r34_comic_integration')) { + $current_state = bool_escape($database->get_one("SELECT comic_admin FROM users WHERE id=:id", ['id'=>$event->display_user->id])); + $this->theme->show_comic_changer($event->display_user, $current_state); + } + } + + public function onThumbnailGeneration(ThumbnailGenerationEvent $event) + { + # global $database, $user; + # if ($user->can(Permissions::MANAGE_ADMINTOOLS)) { + # $database->notify("shm_image_bans", $event->hash); + # } + } + + public function onCommand(CommandEvent $event) + { + global $cache; + if ($event->cmd == "wipe-thumb-cache") { + foreach (Image::find_images_iterable(0, null, Tag::explode($event->args[0])) as $image) { + print($image->id . "\n"); + $cache->delete("thumb-block:{$image->id}"); + } + } + } + + public function onSourceSet(SourceSetEvent $event) + { + // Maybe check for 404? + if (empty($event->source)) { + return; + } + if (!preg_match("/^(https?:\/\/)?[a-zA-Z0-9\.\-]+(\/.*)?$/", $event->source)) { + throw new SCoreException("Invalid source URL"); + } + } + + public function onRobotsBuilding(RobotsBuildingEvent $event) + { + // robots should only check the canonical site, not mirrors + if ($_SERVER['HTTP_HOST'] != "rule34.paheal.net") { + $event->add_disallow(""); + } + } + + public function onPageRequest(PageRequestEvent $event) + { + global $database, $page, $user; + + # Database might not be connected at this point... + #$database->set_timeout(null); // deleting users can take a while + + if (function_exists("sd_notify_watchdog")) { + \sd_notify_watchdog(); + } + + if ($event->page_matches("rule34/comic_admin")) { + if ($user->can(Permissions::CHANGE_SETTING) && $user->check_auth_token()) { + $input = validate_input([ + 'user_id' => 'user_id,exists', + 'is_admin' => 'bool', + ]); + $database->execute( + 'UPDATE users SET comic_admin=:is_admin WHERE id=:id', + ['is_admin'=>$input['is_admin'] ? 't' : 'f', 'id'=>$input['user_id']] + ); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(referer_or(make_link())); + } + } + + if ($event->page_matches("tnc_agreed")) { + setcookie("ui-tnc-agreed", "true", 0, "/"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(referer_or("/")); + } + + if ($event->page_matches("admin/cache_purge")) { + if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) { + $this->theme->display_permission_denied(); + } else { + if ($user->check_auth_token()) { + $all = $_POST["hash"]; + $matches = []; + if (preg_match_all("/([a-fA-F0-9]{32})/", $all, $matches)) { + $matches = $matches[0]; + foreach ($matches as $hash) { + $page->flash("Cleaning {$hash}"); + if (strlen($hash) != 32) { + continue; + } + log_info("admin", "Cleaning {$hash}"); + @unlink(warehouse_path(Image::IMAGE_DIR, $hash)); + @unlink(warehouse_path(Image::THUMBNAIL_DIR, $hash)); + $database->notify("shm_image_bans", $hash); + } + } + } + + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("admin")); + } + } + } +} diff --git a/ext/rule34/script.js b/ext/rule34/script.js new file mode 100644 index 00000000..9d22a5e6 --- /dev/null +++ b/ext/rule34/script.js @@ -0,0 +1,36 @@ +document.addEventListener('DOMContentLoaded', () => { + if(Cookies.get("ui-tnc-agreed") !== "true" && window.location.href.indexOf("/wiki/") == -1) { + $("BODY").addClass("censored"); + $("BODY").append("
    "); + $("BODY").append(""+ + "
    "+ + "

    Cookies may be used. Please read our privacy policy for more information."+ + "

    By accepting to enter you agree to our rules and terms of service."+ + "

    Agree / Disagree"+ + "

    "+ + ""); + } +}); + +function tnc_agree() { + Cookies.set("ui-tnc-agreed", "true", {path: '/', expires: 365}); + $("BODY").removeClass("censored"); + $(".tnc_bg").hide(); + $(".tnc").hide(); +} + +function image_hash_ban(id) { + var reason = prompt("WHY?", "DNP"); + if(reason) { + $.post( + "/image_hash_ban/add", + { + "image_id": id, + "reason": reason, + }, + function() { + $("#thumb_" + id).parent().parent().hide(); + } + ); + } +} diff --git a/ext/rule34/style.css b/ext/rule34/style.css new file mode 100644 index 00000000..2e394c2d --- /dev/null +++ b/ext/rule34/style.css @@ -0,0 +1,35 @@ +BODY.censored #header, +BODY.censored NAV, +BODY.censored ARTICLE, +BODY.censored FOOTER { + filter: blur(10px); +} +.tnc_bg { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #ACE4A3; + opacity: 0.75; + z-index: 999999999999999999999; +} +.tnc { + position: fixed; + top: 20%; + left: 20%; + right: 20%; + text-align: center; + font-size: 2em; + background: #ACE4A3; + border: 1px solid #7EB977; + z-index: 9999999999999999999999; +} +@media (max-width: 1024px) { + .tnc { + top: 5%; + left: 5%; + right: 5%; + font-size: 3vw; + } +} diff --git a/ext/rule34/theme.php b/ext/rule34/theme.php new file mode 100644 index 00000000..0f607fe3 --- /dev/null +++ b/ext/rule34/theme.php @@ -0,0 +1,26 @@ +'hidden', "name"=>'user_id', "value"=>$duser->id]), + LABEL(INPUT(["type"=>'checkbox', "name"=>'is_admin', "checked"=>$current_state]), "Comic Admin"), + BR(), + SHM_SUBMIT("Set") + ); + + $page->add_block(new Block("Rule34 Comic Options", $html)); + } +} diff --git a/ext/s3/config.php b/ext/s3/config.php deleted file mode 100644 index 818f2a35..00000000 --- a/ext/s3/config.php +++ /dev/null @@ -1,13 +0,0 @@ - self::SHISH_EMAIL]; - public string $license = self::LICENSE_GPLV2; - public string $description = "Push post updates to S3"; -} diff --git a/ext/s3/main.php b/ext/s3/main.php deleted file mode 100644 index c00f08a2..00000000 --- a/ext/s3/main.php +++ /dev/null @@ -1,293 +0,0 @@ -panel->create_new_block("S3 CDN"); - $sb->add_text_option(S3Config::ACCESS_KEY_ID, "Access Key ID: "); - $sb->add_text_option(S3Config::ACCESS_KEY_SECRET, "
    Access Key Secret: "); - $sb->add_text_option(S3Config::ENDPOINT, "
    Endpoint: "); - $sb->add_text_option(S3Config::IMAGE_BUCKET, "
    Image Bucket: "); - } - - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void - { - global $database; - - if ($this->get_version("ext_s3_version") < 1) { - $database->create_table("s3_sync_queue", " - hash CHAR(32) NOT NULL PRIMARY KEY, - time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - action CHAR(1) NOT NULL DEFAULT 'S' - "); - $this->set_version("ext_s3_version", 1); - } - } - - public function onAdminBuilding(AdminBuildingEvent $event): void - { - global $database, $page; - $count = $database->get_one("SELECT COUNT(*) FROM s3_sync_queue"); - $html = SHM_SIMPLE_FORM( - "admin/s3_process", - INPUT(["type" => 'number', "name" => 'count', 'value' => '10']), - SHM_SUBMIT("Sync N/$count posts"), - ); - $page->add_block(new Block("Process S3 Queue", $html)); - } - - public function onAdminAction(AdminActionEvent $event): void - { - global $database; - if($event->action == "s3_process") { - foreach($database->get_all( - "SELECT * FROM s3_sync_queue ORDER BY time ASC LIMIT :count", - ["count" => isset($_POST['count']) ? int_escape($_POST["count"]) : 10] - ) as $row) { - if($row['action'] == "S") { - $image = Image::by_hash($row['hash']); - $this->sync_post($image); - } elseif($row['action'] == "D") { - $this->remove_file($row['hash']); - } - } - $event->redirect = true; - } - } - - public function onCliGen(CliGenEvent $event): void - { - $event->app->register('s3:process') - ->addOption('count', 'c', InputOption::VALUE_REQUIRED, 'Number of items to process') - ->setDescription('Process the S3 queue') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - global $database; - $count = $database->get_one("SELECT COUNT(*) FROM s3_sync_queue"); - $output->writeln("{$count} items in queue"); - foreach($database->get_all( - "SELECT * FROM s3_sync_queue ORDER BY time ASC LIMIT :count", - ["count" => $input->getOption('count') ?? $count] - ) as $row) { - if($row['action'] == "S") { - $image = Image::by_hash($row['hash']); - $output->writeln("SYN {$row['hash']} ($image->id)"); - $this->sync_post($image); - } elseif($row['action'] == "D") { - $output->writeln("DEL {$row['hash']}"); - $this->remove_file($row['hash']); - } else { - $output->writeln("??? {$row['hash']} ({$row['action']})"); - } - } - return Command::SUCCESS; - }); - $event->app->register('s3:sync') - ->addArgument('start', InputArgument::REQUIRED) - ->addArgument('end', InputArgument::REQUIRED) - ->setDescription('Sync a range of images to S3') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $start = (int)$input->getArgument('start'); - $end = (int)$input->getArgument('end'); - $output->writeln("Syncing range: $start - $end"); - foreach(Search::find_images_iterable(tags: ["order=id", "id>=$start", "id<=$end"]) as $image) { - if($this->sync_post($image)) { - print("{$image->id}: {$image->hash}\n"); - } else { - print("{$image->id}: {$image->hash} (skipped)\n"); - } - } - return Command::SUCCESS; - }); - $event->app->register('s3:rm') - ->addArgument('hash', InputArgument::REQUIRED) - ->setDescription('Delete a leftover file from S3') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $hash = $input->getArgument('hash'); - $output->writeln("Deleting file: '$hash'"); - $this->remove_file($hash); - return Command::SUCCESS; - }); - } - - public function onPageRequest(PageRequestEvent $event): void - { - global $config, $page, $user; - if ($event->page_matches("s3/sync")) { - if ($user->check_auth_token()) { - if ($user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id'])) { - $id = int_escape($_POST['image_id']); - if ($id > 0) { - $this->sync_post(Image::by_id($id)); - log_info("s3", "Manual resync for >>$id", "File re-sync'ed"); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/$id")); - } - } - } - } - } - - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void - { - global $user; - if ($user->can(Permissions::DELETE_IMAGE)) { - $event->add_part(SHM_SIMPLE_FORM( - "s3/sync", - INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $event->image->id]), - INPUT(["type" => 'submit', "value" => 'CDN Re-Sync']), - )); - } - } - - public function onImageAddition(ImageAdditionEvent $event): void - { - // Tags aren't set at this point, let's wait for the TagSetEvent - // $this->sync_post($event->image); - } - - public function onTagSet(TagSetEvent $event): void - { - $this->sync_post($event->image, $event->new_tags); - } - - public function onImageDeletion(ImageDeletionEvent $event): void - { - $this->remove_file($event->image->hash); - } - - public function onImageReplace(ImageReplaceEvent $event): void - { - $this->remove_file($event->old_hash); - $this->sync_post($event->image); - } - - // utils - private function get_client(): ?\Aws\S3\S3Client - { - global $config; - $access_key_id = $config->get_string(S3Config::ACCESS_KEY_ID); - $access_key_secret = $config->get_string(S3Config::ACCESS_KEY_SECRET); - if(is_null($access_key_id) || is_null($access_key_secret)) { - return null; - } - $endpoint = $config->get_string(S3Config::ENDPOINT); - $credentials = new \Aws\Credentials\Credentials($access_key_id, $access_key_secret); - return new \Aws\S3\S3Client([ - 'region' => 'auto', - 'endpoint' => $endpoint, - 'version' => 'latest', - 'credentials' => $credentials, - ]); - } - - private function hash_to_path(string $hash): string - { - $ha = substr($hash, 0, 2); - $sh = substr($hash, 2, 2); - return "$ha/$sh/$hash"; - } - - private function is_busy(): bool - { - global $config; - $this->synced++; - if(PHP_SAPI == "cli") { - return false; // CLI can go on for as long as it wants - } - return $this->synced > $config->get_int(UploadConfig::COUNT); - } - - // underlying s3 interaction functions - /** - * @param string[]|null $new_tags - */ - private function sync_post(Image $image, ?array $new_tags = null, bool $overwrite = true): bool - { - global $config; - - $client = $this->get_client(); - if(is_null($client)) { - return false; - } - $image_bucket = $config->get_string(S3Config::IMAGE_BUCKET); - - $key = $this->hash_to_path($image->hash); - if(!$overwrite && $client->doesObjectExist($image_bucket, $key)) { - return false; - } - - if($this->is_busy()) { - $this->enqueue($image->hash, "S"); - } else { - if(is_null($new_tags)) { - $friendly = $image->parse_link_template('$id - $tags.$ext'); - } else { - $_orig_tags = $image->get_tag_array(); - $image->tag_array = $new_tags; - $friendly = $image->parse_link_template('$id - $tags.$ext'); - $image->tag_array = $_orig_tags; - } - $client->putObject([ - 'Bucket' => $image_bucket, - 'Key' => $key, - 'Body' => file_get_contents_ex($image->get_image_filename()), - 'ACL' => 'public-read', - 'ContentType' => $image->get_mime(), - 'ContentDisposition' => "inline; filename=\"$friendly\"", - ]); - $this->dequeue($image->hash); - } - return true; - } - - private function remove_file(string $hash): void - { - global $config; - $client = $this->get_client(); - if(is_null($client)) { - return; - } - if($this->is_busy()) { - $this->enqueue($hash, "D"); - } else { - $client->deleteObject([ - 'Bucket' => $config->get_string(S3Config::IMAGE_BUCKET), - 'Key' => $this->hash_to_path($hash), - ]); - $this->dequeue($hash); - } - } - - private function enqueue(string $hash, string $action): void - { - global $database; - $database->execute("DELETE FROM s3_sync_queue WHERE hash = :hash", ["hash" => $hash]); - $database->execute(" - INSERT INTO s3_sync_queue (hash, action) - VALUES (:hash, :action) - ", ["hash" => $hash, "action" => $action]); - } - - private function dequeue(string $hash): void - { - global $database; - $database->execute("DELETE FROM s3_sync_queue WHERE hash = :hash", ["hash" => $hash]); - } -} diff --git a/ext/setup/main.php b/ext/setup/main.php index 2cbb9a3b..8684c7a0 100644 --- a/ext/setup/main.php +++ b/ext/setup/main.php @@ -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; - require_once "config.php"; /* @@ -71,36 +67,36 @@ class SetupBlock extends Block $this->config = $config; } - public function add_label(string $text): void + public function add_label(string $text) { $this->body .= $text; } - public function start_table(): void + public function start_table() { $this->body .= "
    Title + ".($can_set ? " + ".html_escape($title)." + + " : html_escape(" + $title + "))." +
    Parent\n". + ( + !$user->is_anonymous() ? + " {$s_parent_id}\n". + " \n" + : + $s_parent_id + ). + " \n". + "
    {$image_link} Report by $userlink: $h_reason + ".make_form(make_link("image_report/remove"))." - $actions +
    $actions
    "; } - public function end_table(): void + public function end_table() { $this->body .= "
    "; } - public function start_table_row(): void + public function start_table_row() { $this->body .= ""; } - public function end_table_row(): void + public function end_table_row() { $this->body .= ""; } - public function start_table_head(): void + public function start_table_head() { $this->body .= ""; } - public function end_table_head(): void + public function end_table_head() { $this->body .= ""; } - public function add_table_header(string $content, int $colspan = 2): void + public function add_table_header($content, int $colspan = 2) { $this->start_table_head(); $this->start_table_row(); @@ -109,29 +105,29 @@ class SetupBlock extends Block $this->end_table_head(); } - public function start_table_cell(int $colspan = 1): void + public function start_table_cell(int $colspan = 1) { $this->body .= ""; } - public function end_table_cell(): void + public function end_table_cell() { $this->body .= ""; } - public function add_table_cell(string $content, int $colspan = 1): void + public function add_table_cell($content, int $colspan = 1) { $this->start_table_cell($colspan); $this->body .= $content; $this->end_table_cell(); } - public function start_table_header_cell(int $colspan = 1, string $align = 'right'): void + public function start_table_header_cell(int $colspan = 1, string $align = 'right') { $this->body .= ""; } - public function end_table_header_cell(): void + public function end_table_header_cell() { $this->body .= ""; } - public function add_table_header_cell(string $content, int $colspan = 1): void + public function add_table_header_cell($content, int $colspan = 1) { $this->start_table_header_cell($colspan); $this->body .= $content; @@ -140,11 +136,11 @@ class SetupBlock extends Block private function format_option( string $name, - string $html, + $html, ?string $label, bool $table_row, bool $label_row = false - ): void { + ) { if ($table_row) { $this->start_table_row(); } @@ -176,7 +172,7 @@ class SetupBlock extends Block } } - public function add_text_option(string $name, string $label = null, bool $table_row = false): void + public function add_text_option(string $name, string $label=null, bool $table_row = false) { $val = html_escape($this->config->get_string($name)); @@ -186,7 +182,7 @@ class SetupBlock extends Block $this->format_option($name, $html, $label, $table_row); } - public function add_longtext_option(string $name, string $label = null, bool $table_row = false): void + public function add_longtext_option(string $name, string $label=null, bool $table_row = false) { $val = html_escape($this->config->get_string($name)); @@ -197,12 +193,12 @@ class SetupBlock extends Block $this->format_option($name, $html, $label, $table_row, true); } - public function add_bool_option(string $name, string $label = null, bool $table_row = false): void + public function add_bool_option(string $name, string $label=null, bool $table_row = false) { $checked = $this->config->get_bool($name) ? " checked" : ""; $html = ""; - if (!$table_row && !is_null($label)) { + if (!$table_row&&!is_null($label)) { $html .= ""; } @@ -222,29 +218,26 @@ class SetupBlock extends Block // $this->body .= ""; // } - public function add_int_option(string $name, string $label = null, bool $table_row = false): void + public function add_int_option(string $name, string $label=null, bool $table_row = false) { $val = $this->config->get_int($name); - $html = "\n"; + $html = "\n"; $html .= "\n"; $this->format_option($name, $html, $label, $table_row); } - public function add_shorthand_int_option(string $name, string $label = null, bool $table_row = false): void + public function add_shorthand_int_option(string $name, string $label=null, bool $table_row = false) { $val = to_shorthand_int($this->config->get_int($name)); - $html = "\n"; + $html = "\n"; $html .= "\n"; $this->format_option($name, $html, $label, $table_row); } - /** - * @param array $options - */ - public function add_choice_option(string $name, array $options, string $label = null, bool $table_row = false): void + public function add_choice_option(string $name, array $options, string $label=null, bool $table_row = false) { if (is_int(array_values($options)[0])) { $current = $this->config->get_int($name); @@ -255,9 +248,9 @@ class SetupBlock extends Block $html = ""; foreach ($options as $optname => $optval) { if (in_array($optval, $current)) { - $selected = " selected"; + $selected=" selected"; } else { - $selected = ""; + $selected=""; } $html .= "\n"; } $html .= ""; $html .= "\n"; + $html .= "\n"; // setup page auto-layout counts
    tags $this->format_option($name, $html, $label, $table_row); } - public function add_color_option(string $name, string $label = null, bool $table_row = false): void + public function add_color_option(string $name, string $label=null, bool $table_row = false) { $val = html_escape($this->config->get_string($name)); @@ -305,7 +296,7 @@ class Setup extends Extension /** @var SetupTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string(SetupConfig::TITLE, "Shimmie"); @@ -315,17 +306,10 @@ class Setup extends Extension $config->set_default_bool(SetupConfig::WORD_WRAP, true); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page, $user; - if ($event->page_matches("nicedebug")) { - $page->set_mode(PageMode::DATA); - $page->set_data(json_encode_ex([ - "args" => $event->args, - ])); - } - if ($event->page_matches("nicetest")) { $page->set_mode(PageMode::DATA); $page->set_data("ok"); @@ -352,16 +336,40 @@ class Setup extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $themes = []; - foreach (glob_ex("themes/*") as $theme_dirname) { + foreach (glob("themes/*") as $theme_dirname) { $name = str_replace("themes/", "", $theme_dirname); $human = str_replace("_", " ", $name); $human = ucwords($human); $themes[$human] = $name; } + $test_url = str_replace("/index.php", "/nicetest", $_SERVER["SCRIPT_NAME"]); + + $nicescript = ""; $sb = $event->panel->create_new_block("General"); $sb->position = 0; $sb->add_text_option(SetupConfig::TITLE, "Site title: "); @@ -371,7 +379,7 @@ class Setup extends Extension $sb->add_choice_option(SetupConfig::THEME, $themes, "
    Theme: "); //$sb->add_multichoice_option("testarray", array("a" => "b", "c" => "d"), "
    Test Array: "); $sb->add_bool_option("nice_urls", "
    Nice URLs: "); - $sb->add_label("(Javascript inactive, can't test!)"); + $sb->add_label("(Javascript inactive, can't test!)$nicescript"); $sb = $event->panel->create_new_block("Remote API Integration"); $sb->add_label("Akismet"); @@ -381,7 +389,7 @@ class Setup extends Extension $sb->add_text_option("api_recaptcha_pubkey", "
    Site key: "); } - public function onConfigSave(ConfigSaveEvent $event): void + public function onConfigSave(ConfigSaveEvent $event) { $config = $event->config; foreach ($_POST as $_name => $junk) { @@ -406,45 +414,45 @@ class Setup extends Extension } } log_warning("setup", "Configuration updated"); - foreach (glob_ex("data/cache/*.css") as $css_cache) { + foreach (glob("data/cache/*.css") as $css_cache) { unlink($css_cache); } log_warning("setup", "Cache cleared"); } - public function onCliGen(CliGenEvent $event): void + public function onCommand(CommandEvent $event) { - $event->app->register('config:get') - ->addArgument('key', InputArgument::REQUIRED) - ->setDescription('Get a config value') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - global $config; - $output->writeln($config->get_string($input->getArgument('key'))); - return Command::SUCCESS; - }); - $event->app->register('config:set') - ->addArgument('key', InputArgument::REQUIRED) - ->addArgument('value', InputArgument::REQUIRED) - ->setDescription('Set a config value') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - global $cache, $config; - $config->set_string($input->getArgument('key'), $input->getArgument('value')); - $cache->delete("config"); - return Command::SUCCESS; - }); + if ($event->cmd == "help") { + print "\tconfig [get|set] \n"; + print "\t\teg 'config get db_version'\n\n"; + } + if ($event->cmd == "config") { + global $cache, $config; + $cmd = $event->args[0]; + $key = $event->args[1]; + switch ($cmd) { + case "get": + print($config->get_string($key) . "\n"); + break; + case "set": + $config->set_string($key, $event->args[2]); + break; + } + $cache->delete("config"); + } } - 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::CHANGE_SETTING)) { $event->add_nav_link("setup", new Link('setup'), "Board Config", null, 0); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::CHANGE_SETTING)) { @@ -452,7 +460,7 @@ class Setup extends Extension } } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { global $config; $event->replace('$base', $config->get_string('base_href')); diff --git a/ext/setup/script.js b/ext/setup/script.js deleted file mode 100644 index ee6d067e..00000000 --- a/ext/setup/script.js +++ /dev/null @@ -1,29 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const checkbox = document.getElementById('nice_urls'); - const out_span = document.getElementById('nicetest'); - - if(checkbox !== null && out_span !== null) { - checkbox.disabled = true; - out_span.innerHTML = '(testing...)'; - - fetch(document.body.getAttribute('data-base-href') + "/nicetest").then(response => { - if(!response.ok) { - checkbox.disabled = true; - out_span.innerHTML = '(http error)'; - } else { - response.text().then(text => { - if(text === 'ok') { - checkbox.disabled = false; - out_span.innerHTML = '(test passed)'; - } else { - checkbox.disabled = true; - out_span.innerHTML = '(test failed)'; - } - }); - } - }).catch(() => { - checkbox.disabled = true; - out_span.innerHTML = '(request failed)'; - }); - } -}); diff --git a/ext/setup/style.css b/ext/setup/style.css index 56d9a40f..bc460fa4 100644 --- a/ext/setup/style.css +++ b/ext/setup/style.css @@ -1,5 +1,7 @@ .setupblocks { column-width: 400px; + -moz-column-width: 400px; + -webkit-column-width: 400px; max-width: 1200px; margin: auto; } @@ -7,16 +9,31 @@ .setupblock { break-inside: avoid; + -moz-break-inside: avoid; + -webkit-break-inside: avoid; column-break-inside: avoid; + -moz-column-break-inside: avoid; + -webkit-column-break-inside: avoid; text-align: center; width: 90%; } .setupblock TEXTAREA { width: 100%; - font-size: 0.75rem; + font-size: 0.75em; resize: vertical; } +.helpable { + border-bottom: 1px dashed gray; +} + +.ok { + background: #AFA; +} +.bad { + background: #FAA; +} + #Setupmain .blockbody { background: none; border: none; @@ -24,9 +41,3 @@ margin: 0; padding: 0; } -.setupblock .form { - width: 100%; -} -.setupblock .form TH { - font-weight: normal; -} \ No newline at end of file diff --git a/ext/setup/test.php b/ext/setup/test.php index f732efb7..e7eb169d 100644 --- a/ext/setup/test.php +++ b/ext/setup/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class SetupTest extends ShimmiePHPUnitTestCase { - public function testNiceUrlsTest(): void + public function testNiceUrlsTest() { # XXX: this only checks that the text is "ok", to check # for a bug where it was coming out as "\nok"; it doesn't @@ -16,14 +16,14 @@ class SetupTest extends ShimmiePHPUnitTestCase $this->assert_no_content("\n"); } - public function testAuthAnon(): void + public function testAuthAnon() { $this->get_page('setup'); $this->assert_response(403); $this->assert_title("Permission Denied"); } - public function testAuthUser(): void + public function testAuthUser() { $this->log_in_as_user(); $this->get_page('setup'); @@ -31,7 +31,7 @@ class SetupTest extends ShimmiePHPUnitTestCase $this->assert_title("Permission Denied"); } - public function testAuthAdmin(): void + public function testAuthAdmin() { $this->log_in_as_admin(); $this->get_page('setup'); @@ -39,7 +39,7 @@ class SetupTest extends ShimmiePHPUnitTestCase $this->assert_text("General"); } - public function testAdvanced(): void + public function testAdvanced() { $this->log_in_as_admin(); $this->get_page('setup/advanced'); diff --git a/ext/setup/theme.php b/ext/setup/theme.php index d2a5be1d..bebf977a 100644 --- a/ext/setup/theme.php +++ b/ext/setup/theme.php @@ -17,7 +17,7 @@ class SetupTheme extends Themelet * * The page should wrap all the options in a form which links to setup_save */ - public function display_page(Page $page, SetupPanel $panel): void + public function display_page(Page $page, SetupPanel $panel) { usort($panel->blocks, "Shimmie2\blockcmp"); @@ -43,10 +43,7 @@ class SetupTheme extends Themelet $page->add_block(new Block("Setup", $table)); } - /** - * @param array $options - */ - public function display_advanced(Page $page, array $options): void + public function display_advanced(Page $page, $options) { $h_rows = ""; ksort($options); @@ -97,10 +94,11 @@ class SetupTheme extends Themelet { $h = $block->header; $b = $block->body; + $i = preg_replace('/[^a-zA-Z0-9]/', '_', $h) . "-setup"; $html = "
    - $h -
    $b + $h +
    $b
    "; return $html; diff --git a/ext/shimmie_api/info.php b/ext/shimmie_api/info.php new file mode 100644 index 00000000..34d98675 --- /dev/null +++ b/ext/shimmie_api/info.php @@ -0,0 +1,28 @@ +Admin Warning - this exposes private data, eg IP addresses +

    Developer Warning - the API is unstable; notably, private data may get hidden +

    Usage: +

    get_tags - List of all tags. (May contain unused tags) +

      tags - Optional - Search for more specific tags (Searchs TAG*)
    +

    get_image - Get image via id. +

      id - Required - User id. (Defaults to id=1 if empty)
    +

    find_images - List of latest 12(?) images. +

    get_user - Get user info. (Defaults to id=2 if both are empty) +

      id - Optional - User id.
    +
      name - Optional - User name.
    "; +} diff --git a/ext/shimmie_api/main.php b/ext/shimmie_api/main.php new file mode 100644 index 00000000..7d5e1262 --- /dev/null +++ b/ext/shimmie_api/main.php @@ -0,0 +1,170 @@ +id; + assert($_id !== null); + $this->id = $_id; + $this->height = $img->height; + $this->width = $img->width; + $this->hash = $img->hash; + $this->filesize = $img->filesize; + $this->ext = $img->get_ext(); + $this->mime = $img->get_mime(); + $this->posted = strtotime($img->posted); + $this->source = $img->source; + $this->owner_id = $img->owner_id; + $this->tags = $img->get_tag_array(); + } +} + +class ShimmieApi extends Extension +{ + public function onPageRequest(PageRequestEvent $event) + { + global $page, $user; + + if ($event->page_matches("api/shimmie")) { + $page->set_mode(PageMode::DATA); + $page->set_mime(MimeType::TEXT); + + if ($event->page_matches("api/shimmie/get_tags")) { + if ($event->count_args() > 0) { + $tag = $event->get_arg(0); + } elseif (isset($_GET['tag'])) { + $tag = $_GET['tag']; + } else { + $tag = null; + } + $res = $this->api_get_tags($tag); + $page->set_data(json_encode($res)); + } elseif ($event->page_matches("api/shimmie/get_image")) { + $arg = $event->get_arg(0); + if (empty($arg) && isset($_GET['id'])) { + $arg = $_GET['id']; + } + $image = Image::by_id(int_escape($arg)); + // FIXME: handle null image + $image->get_tag_array(); // tag data isn't loaded into the object until necessary + $safe_image = new _SafeImage($image); + $page->set_data(json_encode($safe_image)); + } elseif ($event->page_matches("api/shimmie/find_images")) { + $search_terms = $event->get_search_terms(); + $page_number = $event->get_page_number(); + $page_size = $event->get_page_size(); + $images = Image::find_images(($page_number-1)*$page_size, $page_size, $search_terms); + $safe_images = []; + foreach ($images as $image) { + $image->get_tag_array(); + $safe_images[] = new _SafeImage($image); + } + $page->set_data(json_encode($safe_images)); + } elseif ($event->page_matches("api/shimmie/get_user")) { + $query = $user->id; + $type = "id"; + if ($event->count_args() == 1) { + $query = $event->get_arg(0); + $type = "name"; + } elseif (isset($_GET['id'])) { + $query = $_GET['id']; + } elseif (isset($_GET['name'])) { + $query = $_GET['name']; + $type = "name"; + } + + $all = $this->api_get_user($type, $query); + $page->set_data(json_encode($all)); + } else { + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("ext_doc/shimmie_api")); + } + } + } + + /** + * #return string[] + */ + private function api_get_tags(?string $arg): array + { + global $database; + if (!empty($arg)) { + $all = $database->get_all("SELECT tag FROM tags WHERE tag LIKE :tag", ['tag'=>$arg . "%"]); + } else { + $all = $database->get_all("SELECT tag FROM tags"); + } + $res = []; + foreach ($all as $row) { + $res[] = $row["tag"]; + } + return $res; + } + + private function api_get_user(string $type, string $query): array + { + global $database; + $all = $database->get_row( + "SELECT id, name, joindate, class FROM users WHERE $type=:query", + ['query'=>$query] + ); + + if (!empty($all)) { + //FIXME?: For some weird reason, get_all seems to return twice. Unsetting second value to make things look nice.. + // - it returns data as eg array(0=>1234, 'id'=>1234, 1=>'bob', 'name'=>bob, ...); + for ($i = 0; $i < 4; $i++) { + unset($all[$i]); + } + $all['uploadcount'] = Image::count_images(["user_id=" . $all['id']]); + $all['commentcount'] = $database->get_one( + "SELECT COUNT(*) AS count FROM comments WHERE owner_id=:owner_id", + ["owner_id" => $all['id']] + ); + + if (isset($_GET['recent'])) { + $recents = $database->get_all( + "SELECT * FROM images WHERE owner_id=:owner_id ORDER BY id DESC LIMIT 0, 5", + ['owner_id'=>$all['id']] + ); + + $i = 0; + foreach ($recents as $recent) { + $all['recentposts'][$i] = $recent; + unset($all['recentposts'][$i]['owner_id']); //We already know the owners id.. + unset($all['recentposts'][$i]['owner_ip']); + + for ($x = 0; $x < 14; $x++) { + unset($all['recentposts'][$i][$x]); + } + if (empty($all['recentposts'][$i]['author'])) { + unset($all['recentposts'][$i]['author']); + } + if ($all['recentposts'][$i]['notes'] > 0) { + $all['recentposts'][$i]['has_notes'] = "Y"; + } else { + $all['recentposts'][$i]['has_notes'] = "N"; + } + unset($all['recentposts'][$i]['notes']); + $i += 1; + } + } + } + return $all; + } +} diff --git a/ext/shimmie_api/test.php b/ext/shimmie_api/test.php new file mode 100644 index 00000000..9efa291f --- /dev/null +++ b/ext/shimmie_api/test.php @@ -0,0 +1,33 @@ +log_in_as_user(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + + // FIXME: get_page should support GET params + $this->get_page("api/shimmie/get_tags"); + $this->get_page("api/shimmie/get_tags/pb"); + //$this->get_page("api/shimmie/get_tags?tag=pb"); + $this->get_page("api/shimmie/get_image/$image_id"); + //$this->get_page("api/shimmie/get_image?id=$image_id"); + $this->get_page("api/shimmie/find_images"); + $this->get_page("api/shimmie/find_images/pbx"); + $this->get_page("api/shimmie/find_images/pbx/1"); + + $page = $this->get_page("api/shimmie/get_user/demo"); + $this->assertEquals(200, $page->code); + + //$this->get_page("api/shimmie/get_user?name=demo"); + //$this->get_page("api/shimmie/get_user?id=2"); + + // FIXME: test unspecified / bad values + // FIXME: test that json is encoded properly + } +} diff --git a/ext/site_description/main.php b/ext/site_description/main.php index fe2961cc..e0fe5218 100644 --- a/ext/site_description/main.php +++ b/ext/site_description/main.php @@ -6,7 +6,7 @@ namespace Shimmie2; class SiteDescription extends Extension { - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page; if (!empty($config->get_string("site_description"))) { @@ -19,7 +19,7 @@ class SiteDescription extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Site Description"); $sb->add_text_option("site_description", "Description: "); diff --git a/ext/site_description/test.php b/ext/site_description/test.php index b2af0dbf..8d07917f 100644 --- a/ext/site_description/test.php +++ b/ext/site_description/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class SiteDescriptionTest extends ShimmiePHPUnitTestCase { - public function testSiteDescription(): void + public function testSiteDescription() { global $config, $page; $config->set_string("site_description", "A Shimmie testbed"); @@ -17,7 +17,7 @@ class SiteDescriptionTest extends ShimmiePHPUnitTestCase ); } - public function testSiteKeywords(): void + public function testSiteKeywords() { global $config, $page; $config->set_string("site_keywords", "foo,bar,baz"); diff --git a/ext/sitemap/info.php b/ext/sitemap/info.php index cd18e749..58ec3e84 100644 --- a/ext/sitemap/info.php +++ b/ext/sitemap/info.php @@ -11,7 +11,7 @@ class XMLSitemapInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "XML Sitemap"; public string $url = "http://drudexsoftware.com"; - public array $authors = ["Sein Kraft" => "mail@seinkraft.info","Drudex Software" => "support@drudexsoftware.com"]; + public array $authors = ["Sein Kraft"=>"mail@seinkraft.info","Drudex Software"=>"support@drudexsoftware.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "Sitemap with caching & advanced priorities"; } diff --git a/ext/sitemap/main.php b/ext/sitemap/main.php index c8c82dda..c3fefe18 100644 --- a/ext/sitemap/main.php +++ b/ext/sitemap/main.php @@ -4,131 +4,173 @@ declare(strict_types=1); namespace Shimmie2; -class XMLSitemapURL -{ - public function __construct( - public string $url, - public string $changefreq, - public string $priority, - public string $date - ) { - } -} - class XMLSitemap extends Extension { - public function onPageRequest(PageRequestEvent $event): void + private string $sitemap_queue = ""; + private string $sitemap_filepath = ""; // set onPageRequest + + public function onPageRequest(PageRequestEvent $event) { if ($event->page_matches("sitemap.xml")) { - global $config, $page; + global $config; - $cache_path = data_path("cache/sitemap.xml"); - - if ($this->new_sitemap_needed($cache_path)) { - $xml = $this->handle_full_sitemap(); - file_put_contents($cache_path, $xml); + $this->sitemap_filepath = data_path("cache/sitemap.xml"); + // determine if new sitemap needs to be generated + if ($this->new_sitemap_needed()) { + // determine which type of sitemap to generate + if ($config->get_bool("sitemap_generatefull", false)) { + $this->handle_full_sitemap(); // default false until cache fixed + } else { + $this->handle_smaller_sitemap(); + } + } else { + $this->display_existing_sitemap(); } - - $xml = file_get_contents_ex($cache_path); - $page->set_mode(PageMode::DATA); - $page->set_mime(MimeType::XML_APPLICATION); - $page->set_data($xml); } } + public function onSetupBuilding(SetupBuildingEvent $event) + { + $sb = $event->panel->create_new_block("Sitemap"); + + $sb->add_bool_option("sitemap_generatefull", "Generate full sitemap"); + $sb->add_label("
    (Enabled: every image and tag in sitemap, generation takes longer)"); + $sb->add_label("
    (Disabled: only display the last 50 uploads in the sitemap)"); + } + + // sitemap with only the latest 50 images + private function handle_smaller_sitemap() + { + /* --- Add latest images to sitemap with higher priority --- */ + $latestimages = Image::find_images(limit: 50); + if (empty($latestimages)) { + return; + } + $latestimages_urllist = []; + $last_image = null; + foreach ($latestimages as $arrayid => $image) { + // create url from image id's + $latestimages_urllist[$arrayid] = "post/view/$image->id"; + $last_image = $image; + } + + $this->add_sitemap_queue( + $latestimages_urllist, + "monthly", + "0.8", + date("Y-m-d", strtotime($last_image->posted)) + ); + + /* --- Display page --- */ + // when sitemap is ok, display it from the file + $this->generate_display_sitemap(); + } + // Full sitemap - private function handle_full_sitemap(): string + private function handle_full_sitemap() { global $database, $config; - $urls = []; - // add index - $urls[] = new XMLSitemapURL( - $config->get_string(SetupConfig::FRONT_PAGE), - "weekly", - "1", - date("Y-m-d") - ); + $index = []; + $index[0] = $config->get_string(SetupConfig::FRONT_PAGE); + $this->add_sitemap_queue($index, "weekly", "1"); /* --- Add 20 most used tags --- */ - foreach ($database->get_col("SELECT tag FROM tags ORDER BY count DESC LIMIT 20") as $tag) { - $urls[] = new XMLSitemapURL( - "post/list/$tag/1", - "weekly", - "0.9", - date("Y-m-d") - ); + $popular_tags = $database->get_all("SELECT tag, count FROM tags ORDER BY `count` DESC LIMIT 0,20"); + foreach ($popular_tags as $arrayid => $tag) { + $tag = $tag['tag']; + $popular_tags[$arrayid] = "post/list/$tag/"; } + $this->add_sitemap_queue($popular_tags, "monthly", "0.9" /* not sure how to deal with date here */); /* --- Add latest images to sitemap with higher priority --- */ - foreach(Search::find_images(limit: 50) as $image) { - $urls[] = new XMLSitemapURL( - "post/view/$image->id", - "weekly", - "0.8", - date("Y-m-d", strtotime_ex($image->posted)) - ); + $latestimages = Image::find_images(limit: 50); + $latestimages_urllist = []; + $latest_image = null; + foreach ($latestimages as $arrayid => $image) { + // create url from image id's + $latestimages_urllist[$arrayid] = "post/view/$image->id"; + $latest_image = $image; } + $this->add_sitemap_queue($latestimages_urllist, "monthly", "0.8", date("Y-m-d", strtotime($latest_image->posted))); /* --- Add other tags --- */ - foreach ($database->get_col("SELECT tag FROM tags ORDER BY count DESC LIMIT 10000 OFFSET 21") as $tag) { - $urls[] = new XMLSitemapURL( - "post/list/$tag/1", - "weekly", - "0.7", - date("Y-m-d") - ); + $other_tags = $database->get_all("SELECT tag, count FROM tags ORDER BY `count` DESC LIMIT 21,10000000"); + foreach ($other_tags as $arrayid => $tag) { + $tag = $tag['tag']; + // create url from tags (tagme ignored) + if ($tag != "tagme") { + $other_tags[$arrayid] = "post/list/$tag/"; + } } + $this->add_sitemap_queue($other_tags, "monthly", "0.7" /* not sure how to deal with date here */); /* --- Add all other images to sitemap with lower priority --- */ - foreach(Search::find_images(offset: 51, limit: 10000) as $image) { - $urls[] = new XMLSitemapURL( - "post/view/$image->id", - "monthly", - "0.6", - date("Y-m-d", strtotime_ex($image->posted)) - ); + $otherimages = Image::find_images(offset: 51, limit: 10000000); + $image = null; + foreach ($otherimages as $arrayid => $image) { + // create url from image id's + $otherimages[$arrayid] = "post/view/$image->id"; } + assert(!is_null($image)); + $this->add_sitemap_queue($otherimages, "monthly", "0.6", date("Y-m-d", strtotime($image->posted))); + /* --- Display page --- */ - return $this->generate_sitemap($urls); + // when sitemap is ok, display it from the file + $this->generate_display_sitemap(); } /** - * @param XMLSitemapURL[] $urls + * Adds an array of urls to the sitemap with the given information. */ - private function generate_sitemap(array $urls): string - { - $xml = "<" . "?xml version=\"1.0\" encoding=\"utf-8\"?" . ">\n" . - "\n"; - foreach($urls as $url) { - $link = make_http(make_link($url->url)); - $xml .= " - - $link - $url->date - $url->changefreq - $url->priority - -"; + private function add_sitemap_queue( + array $urls, + string $changefreq = "monthly", + string $priority = "0.5", + string $date = "2013-02-01" + ) { + foreach ($urls as $url) { + $link = make_http(make_link("$url")); + $this->sitemap_queue .= " + + $link + $date + $changefreq + $priority + "; } - $xml .= "\n"; + } - return $xml; + // sets sitemap with entries in sitemap_queue + private function generate_display_sitemap() + { + global $page; + + $xml = "<" . "?xml version=\"1.0\" encoding=\"utf-8\"?" . "> + + $this->sitemap_queue + "; + + // Generate new sitemap + file_put_contents($this->sitemap_filepath, $xml); + $page->set_mode(PageMode::DATA); + $page->set_mime(MimeType::XML_APPLICATION); + $page->set_data($xml); } /** * Returns true if a new sitemap is needed. */ - private function new_sitemap_needed(string $cache_path): bool + private function new_sitemap_needed(): bool { - if (!file_exists($cache_path)) { + if (!file_exists($this->sitemap_filepath)) { return true; } $sitemap_generation_interval = 86400; // allow new site map every day - $last_generated_time = filemtime($cache_path); + $last_generated_time = filemtime($this->sitemap_filepath); // if file doesn't exist, return true if ($last_generated_time == false) { @@ -136,6 +178,21 @@ class XMLSitemap extends Extension } // if it's been a day since last sitemap creation, return true - return ($last_generated_time + $sitemap_generation_interval < time()); + if ($last_generated_time + $sitemap_generation_interval < time()) { + return true; + } else { + return false; + } + } + + private function display_existing_sitemap() + { + global $page; + + $xml = file_get_contents($this->sitemap_filepath); + + $page->set_mode(PageMode::DATA); + $page->set_mime(MimeType::XML_APPLICATION); + $page->set_data($xml); } } diff --git a/ext/sitemap/test.php b/ext/sitemap/test.php index bcb89e65..b36e5a9c 100644 --- a/ext/sitemap/test.php +++ b/ext/sitemap/test.php @@ -6,22 +6,8 @@ namespace Shimmie2; class XMLSitemapTest extends ShimmiePHPUnitTestCase { - public function testBasic(): void + public function testBasic() { - // check empty DB - @unlink(data_path("cache/sitemap.xml")); - $page = $this->get_page('sitemap.xml'); - $this->assertEquals(200, $page->code); - - $this->log_in_as_user(); - $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - - // check DB with one image - @unlink(data_path("cache/sitemap.xml")); - $page = $this->get_page('sitemap.xml'); - $this->assertEquals(200, $page->code); - - // check caching $page = $this->get_page('sitemap.xml'); $this->assertEquals(200, $page->code); } diff --git a/ext/source_history/main.php b/ext/source_history/main.php index 7d137da9..b83c29c3 100644 --- a/ext/source_history/main.php +++ b/ext/source_history/main.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\{rawHTML}; - class SourceHistory extends Extension { /** @var SourceHistoryTheme */ @@ -17,18 +15,18 @@ class SourceHistory extends Extension return 40; } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int("history_limit", -1); } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_admin_block(); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -53,18 +51,18 @@ class SourceHistory extends Extension } } - public function onRobotsBuilding(RobotsBuildingEvent $event): void + public function onRobotsBuilding(RobotsBuildingEvent $event) { $event->add_disallow("source_history"); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - $event->add_part(rawHTML(" + $event->add_part("
    - "), 20); + ", 20); } /* @@ -80,22 +78,22 @@ class SourceHistory extends Extension } */ - public function onSourceSet(SourceSetEvent $event): void + public function onSourceSet(SourceSetEvent $event) { $this->add_source_history($event->image, $event->source); } - 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::BULK_EDIT_IMAGE_TAG)) { $event->add_nav_link("source_history", new Link('source_history/all/1'), "Source Changes", NavLink::is_active(["source_history"])); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { @@ -103,7 +101,7 @@ class SourceHistory extends Extension } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -137,7 +135,7 @@ class SourceHistory extends Extension /** * This function is called when a revert request is received. */ - private function process_revert_request(int $revert_id): void + private function process_revert_request(int $revert_id) { global $page; @@ -180,7 +178,7 @@ class SourceHistory extends Extension $page->set_redirect(make_link('post/view/'.$stored_image_id)); } - protected function process_bulk_revert_request(): void + protected function process_bulk_revert_request() { if (isset($_POST['revert_name']) && !empty($_POST['revert_name'])) { $revert_name = $_POST['revert_name']; @@ -189,7 +187,7 @@ class SourceHistory extends Extension } if (isset($_POST['revert_ip']) && !empty($_POST['revert_ip'])) { - $revert_ip = filter_var_ex($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); + $revert_ip = filter_var($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); if ($revert_ip === false) { // invalid ip given. @@ -219,9 +217,6 @@ class SourceHistory extends Extension $this->theme->display_revert_ip_results(); } - /** - * @return array|null - */ public function get_source_history_from_revert(int $revert_id): ?array { global $database; @@ -229,13 +224,10 @@ class SourceHistory extends Extension SELECT source_histories.*, users.name FROM source_histories JOIN users ON source_histories.user_id = users.id - WHERE source_histories.id = :id", ["id" => $revert_id]); + WHERE source_histories.id = :id", ["id"=>$revert_id]); return ($row ? $row : null); } - /** - * @return array - */ public function get_source_history_from_id(int $image_id): array { global $database; @@ -246,13 +238,10 @@ class SourceHistory extends Extension JOIN users ON source_histories.user_id = users.id WHERE image_id = :image_id ORDER BY source_histories.id DESC", - ["image_id" => $image_id] + ["image_id"=>$image_id] ); } - /** - * @return array - */ public function get_global_source_history(int $page_id): array { global $database; @@ -262,13 +251,13 @@ class SourceHistory extends Extension JOIN users ON source_histories.user_id = users.id ORDER BY source_histories.id DESC LIMIT 100 OFFSET :offset - ", ["offset" => ($page_id - 1) * 100]); + ", ["offset" => ($page_id-1)*100]); } /** * This function attempts to revert all changes by a given IP within an (optional) timeframe. */ - public function process_revert_all_changes(?string $name, ?string $ip, ?string $date): void + public function process_revert_all_changes(?string $name, ?string $ip, ?string $date) { global $database; @@ -360,7 +349,7 @@ class SourceHistory extends Extension /** * This function is called just before an images source is changed. */ - private function add_source_history(Image $image, string $source): void + private function add_source_history(Image $image, string $source) { global $database, $config, $user; @@ -384,13 +373,13 @@ class SourceHistory extends Extension } // if the image has no history, make one with the old source - $entries = $database->get_one("SELECT COUNT(*) FROM source_histories WHERE image_id = :image_id", ['image_id' => $image->id]); + $entries = $database->get_one("SELECT COUNT(*) FROM source_histories WHERE image_id = :image_id", ['image_id'=>$image->id]); if ($entries == 0 && !empty($old_source)) { $database->execute( " INSERT INTO source_histories(image_id, source, user_id, user_ip, date_set) VALUES (:image_id, :source, :user_id, :user_ip, now())", - ["image_id" => $image->id, "source" => $old_source, "user_id" => $config->get_int('anon_id'), "user_ip" => '127.0.0.1'] + ["image_id"=>$image->id, "source"=>$old_source, "user_id"=>$config->get_int('anon_id'), "user_ip"=>'127.0.0.1'] ); $entries++; } @@ -400,7 +389,7 @@ class SourceHistory extends Extension " INSERT INTO source_histories(image_id, source, user_id, user_ip, date_set) VALUES (:image_id, :source, :user_id, :user_ip, now())", - ["image_id" => $image->id, "source" => $new_source, "user_id" => $user->id, "user_ip" => get_real_ip()] + ["image_id"=>$image->id, "source"=>$new_source, "user_id"=>$user->id, "user_ip"=>get_real_ip()] ); $entries++; @@ -417,8 +406,8 @@ class SourceHistory extends Extension https://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause */ - $min_id = $database->get_one("SELECT MIN(id) FROM source_histories WHERE image_id = :image_id", ["image_id" => $image->id]); - $database->execute("DELETE FROM source_histories WHERE id = :id", ["id" => $min_id]); + $min_id = $database->get_one("SELECT MIN(id) FROM source_histories WHERE image_id = :image_id", ["image_id"=>$image->id]); + $database->execute("DELETE FROM source_histories WHERE id = :id", ["id"=>$min_id]); } } } diff --git a/ext/source_history/theme.php b/ext/source_history/theme.php index 5bb01ba2..d583abfb 100644 --- a/ext/source_history/theme.php +++ b/ext/source_history/theme.php @@ -10,18 +10,11 @@ use function MicroHTML\INPUT; use function MicroHTML\LABEL; use function MicroHTML\rawHTML; -/** - * @phpstan-type HistoryEntry array{image_id:int,id:int,source:string,date_set:string,user_id:string,user_ip:string,name:string} - */ class SourceHistoryTheme extends Themelet { - /** @var string[] */ private array $messages = []; - /** - * @param HistoryEntry[] $history - */ - public function display_history_page(Page $page, int $image_id, array $history): void + public function display_history_page(Page $page, int $image_id, array $history) { $history_html = $this->history_list($history, true); @@ -31,10 +24,7 @@ class SourceHistoryTheme extends Themelet $page->add_block(new Block("Source History", $history_html, "main", 10)); } - /** - * @param HistoryEntry[] $history - */ - public function display_global_page(Page $page, array $history, int $page_number): void + public function display_global_page(Page $page, array $history, int $page_number) { $history_html = $this->history_list($history, false); @@ -43,9 +33,9 @@ class SourceHistoryTheme extends Themelet $page->add_block(new Block("Source History", $history_html, "main", 10)); $h_prev = ($page_number <= 1) ? "Prev" : - 'Prev'; + 'Prev'; $h_index = "Index"; - $h_next = 'Next'; + $h_next = 'Next'; $nav = $h_prev.' | '.$h_index.' | '.$h_next; $page->add_block(new Block("Navigation", $nav, "left")); @@ -54,7 +44,7 @@ class SourceHistoryTheme extends Themelet /** * Add a section to the admin page. */ - public function display_admin_block(string $validation_msg = ''): void + public function display_admin_block(string $validation_msg='') { global $page; @@ -81,21 +71,18 @@ class SourceHistoryTheme extends Themelet /* * Show a standard page for results to be put into */ - public function display_revert_ip_results(): void + public function display_revert_ip_results() { global $page; $html = implode("\n", $this->messages); $page->add_block(new Block("Bulk Revert Results", $html)); } - public function add_status(string $title, string $body): void + public function add_status(string $title, string $body) { $this->messages[] = '

    '. $title .'
    '. $body .'

    '; } - /** - * @param HistoryEntry[] $history - */ protected function history_list(array $history, bool $select_2nd): string { $history_list = ""; @@ -115,9 +102,6 @@ class SourceHistoryTheme extends Themelet "; } - /** - * @param HistoryEntry $fields - */ protected function history_entry(array $fields, bool $selected): string { global $user; @@ -129,14 +113,14 @@ class SourceHistoryTheme extends Themelet $ip = $user->can(Permissions::VIEW_IP) ? rawHTML(" " . show_ip($fields['user_ip'], "Sourcing >>$image_id as '$current_source'")) : null; - $setter = A(["href" => make_link("user/" . url_escape($name))], $name); + $setter = A(["href"=>make_link("user/" . url_escape($name))], $name); return (string)LI( - INPUT(["type" => "radio", "name" => "revert", "id" => "$current_id", "value" => "$current_id", "checked" => $selected]), - A(["href" => make_link("post/view/$image_id")], $image_id), + INPUT(["type"=>"radio", "name"=>"revert", "id"=>"$current_id", "value"=>"$current_id", "checked"=>$selected]), + A(["href"=>make_link("post/view/$image_id")], $image_id), ": ", LABEL( - ["for" => "$current_id"], + ["for"=>"$current_id"], $current_source, " - ", $setter, diff --git a/ext/static_files/init.js b/ext/static_files/init.js deleted file mode 100644 index fe6cabf2..00000000 --- a/ext/static_files/init.js +++ /dev/null @@ -1,16 +0,0 @@ -function shm_cookie_set(name, value) { - Cookies.set(name, value, {expires: 365, samesite: "lax", path: "/"}); -} -function shm_cookie_get(name) { - return Cookies.get(name); -} - -function shm_log(section, ...message) { - window.dispatchEvent(new CustomEvent("shm_log", {detail: {section, message}})); -} -window.addEventListener("shm_log", function (e) { - console.log(e.detail.section, ...e.detail.message); -}); -window.addEventListener("error", function (e) { - shm_log("Window error:", e.error); -}); diff --git a/ext/static_files/installer.css b/ext/static_files/installer.css deleted file mode 100644 index c2c17c2e..00000000 --- a/ext/static_files/installer.css +++ /dev/null @@ -1,39 +0,0 @@ -#installer { - background: #EEE; - font-family: "Arial", sans-serif; - font-size: 14px; - width: 512px; - margin: 16px auto auto; - border: 1px solid black; - border-radius: 16px; -} -#installer P { - padding: 5px; -} -#installer A { - text-decoration: none; -} -#installer A:hover { - text-decoration: underline; -} -#installer H1, #installer H3 { - background: #DDD; - text-align: center; - margin: 0; - padding: 2px; -} -#installer H1 { - border-radius: 16px 16px 0 0; -} -#installer H3 { - border-top: 1px solid black; - border-bottom: 1px solid black; -} -#installer TH { - text-align: right; -} -#installer INPUT, -#installer SELECT { - width: 100%; - box-sizing: border-box; -} diff --git a/ext/static_files/main.php b/ext/static_files/main.php index e6dd42a5..d66b2557 100644 --- a/ext/static_files/main.php +++ b/ext/static_files/main.php @@ -6,7 +6,6 @@ namespace Shimmie2; class RobotsBuildingEvent extends Event { - /** @var string[] */ public array $parts = [ "User-agent: *", // Site is rate limited to 1 request / sec, @@ -22,7 +21,7 @@ class RobotsBuildingEvent extends Event class StaticFiles extends Extension { - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $config, $page; @@ -48,17 +47,14 @@ class StaticFiles extends Extension $page->add_http_header("Cache-control: public, max-age=600"); $page->add_http_header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT'); $page->set_mode(PageMode::DATA); - $page->set_data(file_get_contents_ex($filename)); + $page->set_data(file_get_contents($filename)); $page->set_mime(MimeType::get_for_file($filename)); } } } - /** - * @param Block[] $blocks - */ - private function count_main(array $blocks): int + private function count_main($blocks): int { $n = 0; foreach ($blocks as $block) { diff --git a/ext/static_files/modernizr-3.3.1.custom.js b/ext/static_files/modernizr-3.3.1.custom.js new file mode 100644 index 00000000..f5e084df --- /dev/null +++ b/ext/static_files/modernizr-3.3.1.custom.js @@ -0,0 +1,3 @@ +/*! modernizr 3.3.1 (Custom Build) | MIT * + * https://modernizr.com/download/?-applicationcache-audio-backgroundsize-borderimage-borderradius-boxshadow-canvas-canvastext-cssanimations-csscolumns-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-flexbox-fontface-generatedcontent-hashchange-history-hsla-indexeddb-inlinesvg-input-inputtypes-localstorage-multiplebgs-opacity-postmessage-rgba-sessionstorage-smil-svg-svgclippaths-textshadow-video-webgl-websockets-websqldatabase-addtest-mq-setclasses-shiv !*/ +!function(e,t,n){function r(e,t){return typeof e===t}function a(){var e,t,n,a,o,i,s;for(var c in x)if(x.hasOwnProperty(c)){if(e=[],t=x[c],t.name&&(e.push(t.name.toLowerCase()),t.options&&t.options.aliases&&t.options.aliases.length))for(n=0;nf;f++)if(m=e[f],g=H.style[m],c(m,"-")&&(m=u(m)),H.style[m]!==n){if(o||r(a,"undefined"))return i(),"pfx"==t?m:!0;try{H.style[m]=a}catch(y){}if(H.style[m]!=g)return i(),"pfx"==t?m:!0}return i(),!1}function v(e,t,n,a,o){var i=e.charAt(0).toUpperCase()+e.slice(1),s=(e+" "+D.join(i+" ")+i).split(" ");return r(t,"string")||r(t,"undefined")?g(s,t,a,o):(s=(e+" "+V.join(i+" ")+i).split(" "),p(s,t,n))}function y(e,t,r){return v(e,n,n,t,r)}var b=[],x=[],T={_version:"3.3.1",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,t){var n=this;setTimeout(function(){t(n[e])},0)},addTest:function(e,t,n){x.push({name:e,fn:t,options:n})},addAsyncTest:function(e){x.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=T,Modernizr=new Modernizr,Modernizr.addTest("applicationcache","applicationCache"in e),Modernizr.addTest("history",function(){var t=navigator.userAgent;return-1===t.indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone")?e.history&&"pushState"in e.history:!1}),Modernizr.addTest("postmessage","postMessage"in e),Modernizr.addTest("svg",!!t.createElementNS&&!!t.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect);var w=!1;try{w="WebSocket"in e&&2===e.WebSocket.CLOSING}catch(S){}Modernizr.addTest("websockets",w),Modernizr.addTest("localstorage",function(){var e="modernizr";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch(t){return!1}}),Modernizr.addTest("sessionstorage",function(){var e="modernizr";try{return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch(t){return!1}}),Modernizr.addTest("websqldatabase","openDatabase"in e);var C=t.documentElement,E="svg"===C.nodeName.toLowerCase();E||!function(e,t){function n(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x",r.insertBefore(n.lastChild,r.firstChild)}function r(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function a(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,l(t)}function o(e){var t=y[e[g]];return t||(t={},v++,e[g]=v,y[v]=t),t}function i(e,n,r){if(n||(n=t),u)return n.createElement(e);r||(r=o(n));var a;return a=r.cache[e]?r.cache[e].cloneNode():h.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||m.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function s(e,n){if(e||(e=t),u)return e.createDocumentFragment();n=n||o(e);for(var a=n.frag.cloneNode(),i=0,s=r(),c=s.length;c>i;i++)a.createElement(s[i]);return a}function c(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+r().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function l(e){e||(e=t);var r=o(e);return!b.shivCSS||d||r.hasCSS||(r.hasCSS=!!n(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),u||c(e,r),e}var d,u,f="3.7.3",p=e.html5||{},m=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,h=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,g="_html5shiv",v=0,y={};!function(){try{var e=t.createElement("a");e.innerHTML="",d="hidden"in e,u=1==e.childNodes.length||function(){t.createElement("a");var e=t.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(n){d=!0,u=!0}}();var b={elements:p.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:f,shivCSS:p.shivCSS!==!1,supportsUnknownElements:u,shivMethods:p.shivMethods!==!1,type:"default",shivDocument:l,createElement:i,createDocumentFragment:s,addElements:a};e.html5=b,l(t),"object"==typeof module&&module.exports&&(module.exports=b)}("undefined"!=typeof e?e:this,t);var k;!function(){var e={}.hasOwnProperty;k=r(e,"undefined")||r(e.call,"undefined")?function(e,t){return t in e&&r(e.constructor.prototype[t],"undefined")}:function(t,n){return e.call(t,n)}}(),T._l={},T.on=function(e,t){this._l[e]||(this._l[e]=[]),this._l[e].push(t),Modernizr.hasOwnProperty(e)&&setTimeout(function(){Modernizr._trigger(e,Modernizr[e])},0)},T._trigger=function(e,t){if(this._l[e]){var n=this._l[e];setTimeout(function(){var e,r;for(e=0;e-1}),Modernizr.addTest("inlinesvg",function(){var e=s("div");return e.innerHTML="","http://www.w3.org/2000/svg"==("undefined"!=typeof SVGRect&&e.firstChild&&e.firstChild.namespaceURI)});var _=function(){function e(e,t){var a;return e?(t&&"string"!=typeof t||(t=s(t||"div")),e="on"+e,a=e in t,!a&&r&&(t.setAttribute||(t=s("div")),t.setAttribute(e,""),a="function"==typeof t[e],t[e]!==n&&(t[e]=n),t.removeAttribute(e)),a):!1}var r=!("onblur"in t.documentElement);return e}();T.hasEvent=_,Modernizr.addTest("hashchange",function(){return _("hashchange",e)===!1?!1:t.documentMode===n||t.documentMode>7});var P=s("input"),N="autocomplete autofocus list placeholder max min multiple pattern required step".split(" "),z={};Modernizr.input=function(t){for(var n=0,r=t.length;r>n;n++)z[t[n]]=!!(t[n]in P);return z.list&&(z.list=!(!s("datalist")||!e.HTMLDataListElement)),z}(N);var R="search tel url email datetime date month week time datetime-local number range color".split(" "),$={};Modernizr.inputtypes=function(e){for(var r,a,o,i=e.length,s="1)",c=0;i>c;c++)P.setAttribute("type",r=e[c]),o="text"!==P.type&&"style"in P,o&&(P.value=s,P.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(r)&&P.style.WebkitAppearance!==n?(C.appendChild(P),a=t.defaultView,o=a.getComputedStyle&&"textfield"!==a.getComputedStyle(P,null).WebkitAppearance&&0!==P.offsetHeight,C.removeChild(P)):/^(search|tel)$/.test(r)||(o=/^(url|email)$/.test(r)?P.checkValidity&&P.checkValidity()===!1:P.value!=s)),$[e[c]]=!!o;return $}(R);var A=T._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];T._prefixes=A,Modernizr.addTest("cssgradients",function(){for(var e,t="background-image:",n="gradient(linear,left top,right bottom,from(#9f9),to(white));",r="",a=0,o=A.length-1;o>a;a++)e=0===a?"to ":"",r+=t+A[a]+"linear-gradient("+e+"left top, #9f9, white);";Modernizr._config.usePrefixes&&(r+=t+"-webkit-"+n);var i=s("a"),c=i.style;return c.cssText=r,(""+c.backgroundImage).indexOf("gradient")>-1}),Modernizr.addTest("opacity",function(){var e=s("a").style;return e.cssText=A.join("opacity:.55;"),/^0.55$/.test(e.opacity)}),Modernizr.addTest("hsla",function(){var e=s("a").style;return e.cssText="background-color:hsla(120,40%,100%,.5)",c(e.backgroundColor,"rgba")||c(e.backgroundColor,"hsla")});var O="CSS"in e&&"supports"in e.CSS,L="supportsCSS"in e;Modernizr.addTest("supports",O||L);var M={}.toString;Modernizr.addTest("svgclippaths",function(){return!!t.createElementNS&&/SVGClipPath/.test(M.call(t.createElementNS("http://www.w3.org/2000/svg","clipPath")))}),Modernizr.addTest("smil",function(){return!!t.createElementNS&&/SVGAnimate/.test(M.call(t.createElementNS("http://www.w3.org/2000/svg","animate")))});var B=function(){var t=e.matchMedia||e.msMatchMedia;return t?function(e){var n=t(e);return n&&n.matches||!1}:function(t){var n=!1;return d("@media "+t+" { #modernizr { position: absolute; } }",function(t){n="absolute"==(e.getComputedStyle?e.getComputedStyle(t,null):t.currentStyle).position}),n}}();T.mq=B;var j=T.testStyles=d;j('#modernizr{font:0/0 a}#modernizr:after{content:":)";visibility:hidden;font:7px/1 a}',function(e){Modernizr.addTest("generatedcontent",e.offsetHeight>=7)});var F=function(){var e=navigator.userAgent,t=e.match(/applewebkit\/([0-9]+)/gi)&&parseFloat(RegExp.$1),n=e.match(/w(eb)?osbrowser/gi),r=e.match(/windows phone/gi)&&e.match(/iemobile\/([0-9])+/gi)&&parseFloat(RegExp.$1)>=9,a=533>t&&e.match(/android/gi);return n||a||r}();F?Modernizr.addTest("fontface",!1):j('@font-face {font-family:"font";src:url("https://")}',function(e,n){var r=t.getElementById("smodernizr"),a=r.sheet||r.styleSheet,o=a?a.cssRules&&a.cssRules[0]?a.cssRules[0].cssText:a.cssText||"":"",i=/src/i.test(o)&&0===o.indexOf(n.split(" ")[0]);Modernizr.addTest("fontface",i)});var I="Moz O ms Webkit",D=T._config.usePrefixes?I.split(" "):[];T._cssomPrefixes=D;var q=function(t){var r,a=A.length,o=e.CSSRule;if("undefined"==typeof o)return n;if(!t)return!1;if(t=t.replace(/^@/,""),r=t.replace(/-/g,"_").toUpperCase()+"_RULE",r in o)return"@"+t;for(var i=0;a>i;i++){var s=A[i],c=s.toUpperCase()+"_"+r;if(c in o)return"@-"+s.toLowerCase()+"-"+t}return!1};T.atRule=q;var V=T._config.usePrefixes?I.toLowerCase().split(" "):[];T._domPrefixes=V;var W={elem:s("modernizr")};Modernizr._q.push(function(){delete W.elem});var H={style:W.elem.style};Modernizr._q.unshift(function(){delete H.style});var U=T.testProp=function(e,t,r){return g([e],n,t,r)};Modernizr.addTest("textshadow",U("textShadow","1px 1px")),T.testAllProps=v;var G,J=T.prefixed=function(e,t,n){return 0===e.indexOf("@")?q(e):(-1!=e.indexOf("-")&&(e=u(e)),t?v(e,t,n):v(e,"pfx"))};try{G=J("indexedDB",e)}catch(S){}Modernizr.addTest("indexeddb",!!G),G&&Modernizr.addTest("indexeddb.deletedatabase","deleteDatabase"in G),T.testAllProps=y,Modernizr.addTest("cssanimations",y("animationName","a",!0)),Modernizr.addTest("backgroundsize",y("backgroundSize","100%",!0)),Modernizr.addTest("borderimage",y("borderImage","url() 1",!0)),Modernizr.addTest("borderradius",y("borderRadius","0px",!0)),Modernizr.addTest("boxshadow",y("boxShadow","1px 1px",!0)),Modernizr.addTest("flexbox",y("flexBasis","1px",!0)),function(){Modernizr.addTest("csscolumns",function(){var e=!1,t=y("columnCount");try{(e=!!t)&&(e=new Boolean(e))}catch(n){}return e});for(var e,t,n=["Width","Span","Fill","Gap","Rule","RuleColor","RuleStyle","RuleWidth","BreakBefore","BreakAfter","BreakInside"],r=0;r { /** Load jQuery extensions **/ //Code via: https://stackoverflow.com/a/13106698 @@ -30,7 +32,7 @@ document.addEventListener('DOMContentLoaded', () => { /** Setup sidebar toggle **/ let sidebar_hidden = []; try { - sidebar_hidden = (shm_cookie_get("ui-sidebar-hidden") || "").split("|"); + sidebar_hidden = (Cookies.get("ui-sidebar-hidden") || "").split("|"); for (let i=0; i 0) { $(sidebar_hidden[i]+" .blockbody").hide(); @@ -53,7 +55,7 @@ document.addEventListener('DOMContentLoaded', () => { } } } - shm_cookie_set("ui-sidebar-hidden", sidebar_hidden.join("|")); + Cookies.set("ui-sidebar-hidden", sidebar_hidden.join("|"), {expires: 365}); }); }); diff --git a/ext/static_files/style.css b/ext/static_files/style.css index 821e0395..b2b79819 100644 --- a/ext/static_files/style.css +++ b/ext/static_files/style.css @@ -1,36 +1,74 @@ -INPUT, TEXTAREA, SELECT, BUTTON { - box-sizing: border-box; - font-size: 1em; -} + +ARTICLE SELECT {width: 150px;} +INPUT, TEXTAREA {box-sizing: border-box;} +TD>INPUT[type="button"], +TD>INPUT[type="submit"], +TD>INPUT[type="text"], +TD>INPUT[type="password"], +TD>INPUT[type="email"], +TD>SELECT, +TD>TEXTAREA, +TD>BUTTON {width: 100%;} TABLE.form {width: 300px;} -TABLE.form.zebra {width: 100%;} - TABLE.form TD, TABLE.form TH {vertical-align: middle;} -TABLE.form TBODY TR TD {text-align: left;} -TABLE.form TBODY TR TH {text-align: right; padding-right: 4px; width: 1%; white-space: nowrap;} -TABLE.form TBODY TR.header TD, -TABLE.form TBODY TR.header TH {text-align: center; width: auto;} +TABLE.form TBODY TD {text-align: left;} +TABLE.form TBODY TH {text-align: right; padding-right: 4px; width: 1%; white-space: nowrap;} TABLE.form TD + TH {padding-left: 8px;} -TABLE.form INPUT:not([type="checkbox"]):not([type="radio"]), -TABLE.form SELECT, -TABLE.form TEXTAREA, -TABLE.form BUTTON {width: 100%;} *[onclick], -H3[class~="shm-toggler"] { +H3[class~="shm-toggler"], +.sortable TH { cursor: pointer; } +IMG {border: none;} +FORM {margin: 0;} +IMG.lazy {display: none;} #flash { background: #FF7; - color: #444; display: block; padding: 8px; margin: 8px; border: 1px solid #882; } -.tag { - overflow-wrap: anywhere; -} \ No newline at end of file +#installer { + background: #EEE; + font-family: "Arial", sans-serif; + font-size: 14px; + width: 512px; + margin: 16px auto auto; + border: 1px solid black; + border-radius: 16px; +} +#installer P { + padding: 5px; +} +#installer A { + text-decoration: none; +} +#installer A:hover { + text-decoration: underline; +} +#installer H1, #installer H3 { + background: #DDD; + text-align: center; + margin: 0; + padding: 2px; +} +#installer H1 { + border-radius: 16px 16px 0 0; +} +#installer H3 { + border-top: 1px solid black; + border-bottom: 1px solid black; +} +#installer TH { + text-align: right; +} +#installer INPUT, +#installer SELECT { + width: 100%; + box-sizing: border-box; +} diff --git a/ext/static_files/test.php b/ext/static_files/test.php index 2255b0e0..e26288b9 100644 --- a/ext/static_files/test.php +++ b/ext/static_files/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class StaticFilesTest extends ShimmiePHPUnitTestCase { - public function testStaticHandler(): void + public function testStaticHandler() { $this->get_page('favicon.ico'); $this->assert_response(200); diff --git a/ext/statsd/main.php b/ext/statsd/main.php index d9d45756..b853f7b5 100644 --- a/ext/statsd/main.php +++ b/ext/statsd/main.php @@ -8,10 +8,9 @@ _d("STATSD_HOST", null); class StatsDInterface extends Extension { - /** @var array */ public static array $stats = []; - private function _stats(string $type): void + private function _stats(string $type) { global $_shm_event_count, $cache, $database, $_shm_load_start; $time = ftime() - $_shm_load_start; @@ -26,7 +25,7 @@ class StatsDInterface extends Extension StatsDInterface::$stats["shimmie.$type.cache-misses"] = $cache->get("__etc_cache_misses", -1)."|c"; } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { $this->_stats("overall"); @@ -46,30 +45,26 @@ class StatsDInterface extends Extension $this->_stats("other"); } - // @phpstan-ignore-next-line - if (STATSD_HOST) { - $this->send(STATSD_HOST, StatsDInterface::$stats, 1.0); - } - + $this->send(StatsDInterface::$stats, 1.0); StatsDInterface::$stats = []; } - public function onUserCreation(UserCreationEvent $event): void + public function onUserCreation(UserCreationEvent $event) { StatsDInterface::$stats["shimmie_events.user_creations"] = "1|c"; } - public function onDataUpload(DataUploadEvent $event): void + public function onDataUpload(DataUploadEvent $event) { StatsDInterface::$stats["shimmie_events.uploads"] = "1|c"; } - public function onCommentPosting(CommentPostingEvent $event): void + public function onCommentPosting(CommentPostingEvent $event) { StatsDInterface::$stats["shimmie_events.comments"] = "1|c"; } - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { StatsDInterface::$stats["shimmie_events.info-sets"] = "1|c"; } @@ -79,11 +74,12 @@ class StatsDInterface extends Extension return 99; } - /** - * @param array $data - */ - private function send(string $host, array $data, float $sampleRate = 1): void + private function send(array $data, float $sampleRate=1) { + if (!STATSD_HOST) { + return; + } + // sampling $sampledData = []; @@ -103,11 +99,11 @@ class StatsDInterface extends Extension // Wrap this in a try/catch - failures in any of this should be silently ignored try { - $parts = explode(":", $host); + $parts = explode(":", STATSD_HOST); $host = $parts[0]; $port = (int)$parts[1]; $fp = fsockopen("udp://$host", $port, $errno, $errstr); - if (!$fp) { + if (! $fp) { return; } foreach ($sampledData as $stat => $value) { diff --git a/ext/system/info.php b/ext/system/info.php index 55383480..47eded77 100644 --- a/ext/system/info.php +++ b/ext/system/info.php @@ -10,7 +10,7 @@ class SystemInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "System"; - 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 system screen"; public bool $core = true; diff --git a/ext/system/main.php b/ext/system/main.php index 18aa2280..f33a7a12 100644 --- a/ext/system/main.php +++ b/ext/system/main.php @@ -6,20 +6,20 @@ namespace Shimmie2; class System extends Extension { - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page; if ($event->page_matches("system")) { $e = send_event(new PageSubNavBuildingEvent("system")); - usort($e->links, fn (NavLink $a, NavLink $b) => $a->order - $b->order); + usort($e->links, "Shimmie2\sort_nav_links"); $link = $e->links[0]->link; $page->set_redirect($link->make_link()); $page->set_mode(PageMode::REDIRECT); } } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("system", new Link('system'), "System"); } diff --git a/ext/system/test.php b/ext/system/test.php deleted file mode 100644 index 3b259679..00000000 --- a/ext/system/test.php +++ /dev/null @@ -1,15 +0,0 @@ -get_page("system"); - $this->assertEquals(PageMode::REDIRECT, $page->mode); - } -} diff --git a/ext/tag_categories/info.php b/ext/tag_categories/info.php index e7639853..221148b0 100644 --- a/ext/tag_categories/info.php +++ b/ext/tag_categories/info.php @@ -11,6 +11,6 @@ class TagCategoriesInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Tag Categories"; public string $url = "https://code.shishnet.org/shimmie2/"; - public array $authors = ["Daniel Oaks" => "danneh@danneh.net"]; + public array $authors = ["Daniel Oaks"=>"danneh@danneh.net"]; public string $description = "Let tags be split into 'categories', like Danbooru's tagging"; } diff --git a/ext/tag_categories/main.php b/ext/tag_categories/main.php index d6718321..0a31a7a8 100644 --- a/ext/tag_categories/main.php +++ b/ext/tag_categories/main.php @@ -11,7 +11,7 @@ class TagCategories extends Extension /** @var TagCategoriesTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; @@ -20,7 +20,7 @@ class TagCategories extends Extension $config->set_default_bool(TagCategoriesConfig::SPLIT_ON_VIEW, true); } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -45,39 +45,39 @@ class TagCategories extends Extension if ($number_of_db_rows == 0) { $database->execute( 'INSERT INTO image_tag_categories VALUES (:category, :single, :multiple, :color)', - ["category" => "artist", "single" => "Artist", "multiple" => "Artists", "color" => "#BB6666"] + ["category"=>"artist", "single"=>"Artist", "multiple"=>"Artists", "color"=>"#BB6666"] ); $database->execute( 'INSERT INTO image_tag_categories VALUES (:category, :single, :multiple, :color)', - ["category" => "series", "single" => "Series", "multiple" => "Series", "color" => "#AA00AA"] + ["category"=>"series", "single"=>"Series", "multiple"=>"Series", "color"=>"#AA00AA"] ); $database->execute( 'INSERT INTO image_tag_categories VALUES (:category, :single, :multiple, :color)', - ["category" => "character", "single" => "Character", "multiple" => "Characters", "color" => "#66BB66"] + ["category"=>"character", "single"=>"Character", "multiple"=>"Characters", "color"=>"#66BB66"] ); } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "tags") { + if ($event->parent=="tags") { $event->add_nav_link("tag_categories", new Link('tags/categories'), "Tag Categories", NavLink::is_active(["tag_categories"])); } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { - global $database, $page, $user; + global $page, $user; if ($event->page_matches("tags/categories")) { if ($user->can(Permissions::EDIT_TAG_CATEGORIES)) { $this->page_update(); - $this->theme->show_tag_categories($page, $database->get_all('SELECT * FROM image_tag_categories')); + $this->show_tag_categories($page); } } } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { if (is_null($event->term)) { return; @@ -106,9 +106,9 @@ class TagCategories extends Extension } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Tag Categories"; $block->body = $this->theme->get_help_html(); @@ -116,26 +116,26 @@ class TagCategories extends Extension } } - /** - * @return array - */ - public function getKeyedDict(): array + public function getDict(): array { global $database; - $tc_dict = $database->get_all('SELECT * FROM image_tag_categories'); + return $database->get_all('SELECT * FROM image_tag_categories;'); + } + + public function getKeyedDict($key_with = 'category'): array + { + $tc_dict = $this->getDict(); $tc_keyed_dict = []; foreach ($tc_dict as $row) { - $tc_keyed_dict[(string)$row['category']] = $row; + $key = $row[$key_with]; + $tc_keyed_dict[$key] = $row; } return $tc_keyed_dict; } - /** - * @param array $tag_category_dict - */ - public function getTagHtml(string $h_tag, array $tag_category_dict, string $extra_text = ''): string + public function getTagHtml(string $h_tag, $tag_category_dict, string $extra_text = ''): string { $h_tag_no_underscores = str_replace("_", " ", $h_tag); @@ -156,12 +156,12 @@ class TagCategories extends Extension return $h_tag_no_underscores; } - public function page_update(): void + public function page_update(): bool { global $user, $database; if (!$user->can(Permissions::EDIT_TAG_CATEGORIES)) { - return; + return false; } if (!isset($_POST['tc_status']) and @@ -169,11 +169,13 @@ class TagCategories extends Extension !isset($_POST['tc_display_singular']) and !isset($_POST['tc_display_multiple']) and !isset($_POST['tc_color'])) { - return; + return false; } + $is_success = null; + if ($_POST['tc_status'] == 'edit') { - $database->execute( + $is_success = $database->execute( 'UPDATE image_tag_categories SET display_singular=:display_singular, display_multiple=:display_multiple, @@ -187,7 +189,7 @@ class TagCategories extends Extension ] ); } elseif ($_POST['tc_status'] == 'new') { - $database->execute( + $is_success = $database->execute( 'INSERT INTO image_tag_categories VALUES (:category, :display_singular, :display_multiple, :color)', [ @@ -198,7 +200,7 @@ class TagCategories extends Extension ] ); } elseif ($_POST['tc_status'] == 'delete') { - $database->execute( + $is_success = $database->execute( 'DELETE FROM image_tag_categories WHERE category=:category', [ @@ -206,5 +208,12 @@ class TagCategories extends Extension ] ); } + + return $is_success; + } + + public function show_tag_categories($page) + { + $this->theme->show_tag_categories($page, $this->getDict()); } } diff --git a/ext/tag_categories/theme.php b/ext/tag_categories/theme.php index 59bc79e1..f47b4ded 100644 --- a/ext/tag_categories/theme.php +++ b/ext/tag_categories/theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class TagCategoriesTheme extends Themelet { - /** - * @param array $tc_dict - */ - public function show_tag_categories(Page $page, array $tc_dict): void + public function show_tag_categories(Page $page, $tc_dict) { $tc_block_index = 0; $html = ''; diff --git a/ext/tag_edit/main.php b/ext/tag_edit/main.php index caba7468..dc510a20 100644 --- a/ext/tag_edit/main.php +++ b/ext/tag_edit/main.php @@ -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; - /* * OwnerSetEvent: * $image_id @@ -33,7 +29,7 @@ class SourceSetEvent extends Event public Image $image; public ?string $source; - public function __construct(Image $image, string $source = null) + public function __construct(Image $image, string $source=null) { parent::__construct(); $this->image = $image; @@ -48,7 +44,7 @@ class TagSetException extends UserErrorException public function __construct(string $msg, ?string $redirect = null) { - parent::__construct($msg); + parent::__construct($msg, null); $this->redirect = $redirect; } } @@ -56,29 +52,25 @@ class TagSetException extends UserErrorException class TagSetEvent extends Event { public Image $image; - /** @var string[] */ - public array $old_tags; - /** @var string[] */ - public array $new_tags; - /** @var string[] */ + public array $tags; public array $metatags; /** - * @param string[] $tags + * #param string[] $tags */ public function __construct(Image $image, array $tags) { parent::__construct(); $this->image = $image; - $this->old_tags = $image->get_tag_array(); - $this->new_tags = []; + + $this->tags = []; $this->metatags = []; foreach ($tags as $tag) { if ((!str_contains($tag, ':')) && (!str_contains($tag, '='))) { //Tag doesn't contain : or =, meaning it can't possibly be a metatag. //This should help speed wise, as it avoids running every single tag through a bunch of preg_match instead. - $this->new_tags[] = $tag; + $this->tags[] = $tag; continue; } @@ -86,7 +78,7 @@ class TagSetEvent extends Event //seperate tags from metatags if (!$ttpe->metatag) { - $this->new_tags[] = $tag; + $this->tags[] = $tag; } else { $this->metatags[] = $tag; } @@ -143,7 +135,7 @@ class TagEdit extends Extension /** @var TagEditTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $user, $page; if ($event->page_matches("tag_edit")) { @@ -160,28 +152,13 @@ class TagEdit extends Extension if ($user->can(Permissions::MASS_TAG_EDIT) && isset($_POST['tags']) && isset($_POST['source'])) { $this->mass_source_edit($_POST['tags'], $_POST['source']); $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(search_link()); + $page->set_redirect(make_link("post/list")); } } } } - public function onCliGen(CliGenEvent $event): void - { - $event->app->register('tag-replace') - ->addArgument('old_tag', InputArgument::REQUIRED) - ->addArgument('new_tag', InputArgument::REQUIRED) - ->setDescription('Mass edit tags') - ->setCode(function (InputInterface $input, OutputInterface $output): int { - $old_tag = $input->getArgument('old_tag'); - $new_tag = $input->getArgument('new_tag'); - $output->writeln("Mass editing tags: '$old_tag' -> '$new_tag'"); - $this->mass_tag_edit($old_tag, $new_tag, true); - return Command::SUCCESS; - }); - } - - // public function onPostListBuilding(PostListBuildingEvent $event): void + // public function onPostListBuilding(PostListBuildingEvent $event) // { // global $user; // if ($user->can(UserAbilities::BULK_EDIT_IMAGE_SOURCE) && !empty($event->search_terms)) { @@ -189,20 +166,7 @@ class TagEdit extends Extension // } // } - public function onImageAddition(ImageAdditionEvent $event): void - { - if(!empty($event->metadata['tags'])) { - send_event(new TagSetEvent($event->image, $event->metadata['tags'])); - } - if(!empty($event->metadata['source'])) { - send_event(new SourceSetEvent($event->image, $event->metadata['source'])); - } - if (!empty($event->metadata['locked'])) { - send_event(new LockSetEvent($event->image, $event->metadata['locked'])); - } - } - - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { global $page, $user; if ($user->can(Permissions::EDIT_IMAGE_OWNER) && isset($_POST['tag_edit__owner'])) { @@ -230,12 +194,12 @@ class TagEdit extends Extension } } if ($user->can(Permissions::EDIT_IMAGE_LOCK)) { - $locked = isset($_POST['tag_edit__locked']) && $_POST['tag_edit__locked'] == "on"; + $locked = isset($_POST['tag_edit__locked']) && $_POST['tag_edit__locked']=="on"; send_event(new LockSetEvent($event->image, $locked)); } } - public function onOwnerSet(OwnerSetEvent $event): void + public function onOwnerSet(OwnerSetEvent $event) { global $user; if ($user->can(Permissions::EDIT_IMAGE_OWNER) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) { @@ -243,18 +207,18 @@ class TagEdit extends Extension } } - public function onTagSet(TagSetEvent $event): void + public function onTagSet(TagSetEvent $event) { global $user; if ($user->can(Permissions::EDIT_IMAGE_TAG) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) { - $event->image->set_tags($event->new_tags); + $event->image->set_tags($event->tags); } foreach ($event->metatags as $tag) { send_event(new TagTermParseEvent($tag, $event->image->id)); } } - public function onSourceSet(SourceSetEvent $event): void + public function onSourceSet(SourceSetEvent $event) { global $user; if ($user->can(Permissions::EDIT_IMAGE_SOURCE) && (!$event->image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK))) { @@ -262,7 +226,7 @@ class TagEdit extends Extension } } - public function onLockSet(LockSetEvent $event): void + public function onLockSet(LockSetEvent $event) { global $user; if ($user->can(Permissions::EDIT_IMAGE_LOCK)) { @@ -270,19 +234,19 @@ class TagEdit extends Extension } } - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { $event->image->delete_tags_from_image(); } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_mass_editor(); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "tags") { + if ($event->parent=="tags") { $event->add_nav_link("tags_help", new Link('ext_doc/tag_edit'), "Help"); } } @@ -290,12 +254,12 @@ class TagEdit extends Extension /** * When an alias is added, oldtag becomes inaccessible. */ - public function onAddAlias(AddAliasEvent $event): void + public function onAddAlias(AddAliasEvent $event) { $this->mass_tag_edit($event->oldtag, $event->newtag, false); } - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { $event->add_part($this->theme->get_user_editor_html($event->image), 39); $event->add_part($this->theme->get_tag_editor_html($event->image), 40); @@ -303,14 +267,14 @@ class TagEdit extends Extension $event->add_part($this->theme->get_lock_editor_html($event->image), 42); } - public function onTagTermCheck(TagTermCheckEvent $event): void + public function onTagTermCheck(TagTermCheckEvent $event) { if (preg_match("/^source[=|:](.*)$/i", $event->term)) { $event->metatag = true; } } - public function onTagTermParse(TagTermParseEvent $event): void + public function onTagTermParse(TagTermParseEvent $event) { if (preg_match("/^source[=|:](.*)$/i", $event->term, $matches)) { $source = ($matches[1] !== "none" ? $matches[1] : null); @@ -318,7 +282,7 @@ class TagEdit extends Extension } } - public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void + public function onParseLinkTemplate(ParseLinkTemplateEvent $event) { $tags = $event->image->get_tag_list(); $tags = str_replace("/", "", $tags); @@ -326,7 +290,7 @@ class TagEdit extends Extension $event->replace('$tags', $tags); } - private function mass_tag_edit(string $search, string $replace, bool $commit): void + private function mass_tag_edit(string $search, string $replace, bool $commit) { global $database, $tracer_enabled, $_tracer; @@ -336,7 +300,7 @@ class TagEdit extends Extension log_info("tag_edit", "Mass editing tags: '$search' -> '$replace'"); if (count($search_set) == 1 && count($replace_set) == 1) { - $images = Search::find_images(limit: 10, tags: $replace_set); + $images = Image::find_images(limit: 10, tags: $replace_set); if (count($images) == 0) { log_info("tag_edit", "No images found with target tag, doing in-place rename"); $database->execute( @@ -365,7 +329,7 @@ class TagEdit extends Extension $search_forward[] = "id<$last_id"; } - $images = Search::find_images(limit: 100, tags: $search_forward); + $images = Image::find_images(limit: 100, tags: $search_forward); if (count($images) == 0) { break; } @@ -377,9 +341,6 @@ class TagEdit extends Extension $last_id = $image->id; } if ($commit) { - // Mass tag edit can take longer than the page timeout, - // so we need to commit periodically to save what little - // work we've done and avoid starting from scratch. $database->commit(); $database->begin_transaction(); } @@ -389,7 +350,7 @@ class TagEdit extends Extension } } - private function mass_source_edit(string $tags, string $source): void + private function mass_source_edit(string $tags, string $source) { $tags = Tag::explode($tags); @@ -403,7 +364,7 @@ class TagEdit extends Extension $search_forward[] = "id<$last_id"; } - $images = Search::find_images(limit: 100, tags: $search_forward); + $images = Image::find_images(limit: 100, tags: $search_forward); if (count($images) == 0) { break; } diff --git a/ext/tag_edit/test.php b/ext/tag_edit/test.php index 8a1034dd..6ed8b765 100644 --- a/ext/tag_edit/test.php +++ b/ext/tag_edit/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class TagEditTest extends ShimmiePHPUnitTestCase { - public function testValidChange(): void + public function testValidChange() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); @@ -22,24 +22,21 @@ class TagEditTest extends ShimmiePHPUnitTestCase $this->assert_title("Post $image_id: new"); } - public function testInvalidChange(): void + public function testInvalidChange() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); $image = Image::by_id($image_id); - $e = $this->assertException(TagSetException::class, function () use ($image) { + try { send_event(new TagSetEvent($image, [])); - }); - $this->assertEquals("Tried to set zero tags", $e->getMessage()); - - $e = $this->assertException(TagSetException::class, function () use ($image) { - send_event(new TagSetEvent($image, ["*test*"])); - }); - $this->assertEquals("Can't set a tag which contains a wildcard (*)", $e->getMessage()); + $this->fail(); + } catch (SCoreException $e) { + $this->assertEquals("Tried to set zero tags", $e->error); + } } - public function testTagEdit_tooLong(): void + public function testTagEdit_tooLong() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", str_repeat("a", 500)); @@ -47,7 +44,7 @@ class TagEditTest extends ShimmiePHPUnitTestCase $this->assert_title("Post $image_id: tagme"); } - public function testSourceEdit(): void + public function testSourceEdit() { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); diff --git a/ext/tag_edit/theme.php b/ext/tag_edit/theme.php index 8ef6eeac..3d9e18f9 100644 --- a/ext/tag_edit/theme.php +++ b/ext/tag_edit/theme.php @@ -4,24 +4,20 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\{TR, TH, TD, emptyHTML, rawHTML, joinHTML, DIV, INPUT, A, TEXTAREA}; - class TagEditTheme extends Themelet { /* * Display a form which links to tag_edit/replace with POST[search] * and POST[replace] set appropriately */ - public function display_mass_editor(): void + public function display_mass_editor() { global $page; $html = " - " . make_form(make_link("tag_edit/replace")) . " + ".make_form(make_link("tag_edit/replace"))." - - + +
    Search
    Replace
    Search
    Replace
    @@ -29,7 +25,7 @@ class TagEditTheme extends Themelet $page->add_block(new Block("Mass Tag Edit", $html)); } - public function mss_html(string $terms): string + public function mss_html($terms): string { $h_terms = html_escape($terms); $html = make_form(make_link("tag_edit/mass_source_set"), "POST") . " @@ -41,94 +37,115 @@ class TagEditTheme extends Themelet return $html; } - public function get_tag_editor_html(Image $image): HTMLElement + public function get_tag_editor_html(Image $image): string { global $user; $tag_links = []; foreach ($image->get_tag_array() as $tag) { - $tag_links[] = A([ - "href" => search_link([$tag]), - "class" => "tag", - "title" => "View all posts tagged $tag" - ], $tag); + $h_tag = html_escape($tag); + $u_tag = url_escape($tag); + $h_link = make_link("post/list/$u_tag/1"); + $tag_links[] = "$h_tag"; } + $h_tag_links = Tag::implode($tag_links); + $h_tags = html_escape($image->get_tag_list()); - return SHM_POST_INFO( - "Tags", - joinHTML(", ", $tag_links), - $user->can(Permissions::EDIT_IMAGE_TAG) ? TEXTAREA([ - "class" => "autocomplete_tags", - "type" => "text", - "name" => "tag_edit__tags", - "id" => "tag_editor", - "spellcheck" => "off", - ], $image->get_tag_list()) : null, - link: Extension::is_enabled(TagHistoryInfo::KEY) ? - make_link("tag_history/{$image->id}") : - null, - ); + return " + + Tags + + ".($user->can(Permissions::EDIT_IMAGE_TAG) ? " + $h_tag_links +
    + +
    + " : " + $h_tag_links + ")." + + + "; } - public function get_user_editor_html(Image $image): HTMLElement + public function get_user_editor_html(Image $image): string { global $user; - $owner = $image->get_owner()->name; - $date = rawHTML(autodate($image->posted)); - $ip = $user->can(Permissions::VIEW_IP) ? rawHTML(" (" . show_ip($image->owner_ip, "Post posted {$image->posted}") . ")") : ""; - $info = SHM_POST_INFO( - "Uploader", - emptyHTML(A(["class" => "username", "href" => make_link("user/$owner")], $owner), $ip, ", ", $date), - $user->can(Permissions::EDIT_IMAGE_OWNER) ? INPUT(["type" => "text", "name" => "tag_edit__owner", "value" => $owner]) : null - ); - // SHM_POST_INFO returns a TR, let's sneakily append - // a TD with the avatar in it - $info->appendChild( - TD( - ["width" => "80px", "rowspan" => "4"], - rawHTML($image->get_owner()->get_avatar_html()) - ) - ); - return $info; + $h_owner = html_escape($image->get_owner()->name); + $h_av = $image->get_owner()->get_avatar_html(); + $h_date = autodate($image->posted); + $h_ip = $user->can(Permissions::VIEW_IP) ? " (".show_ip($image->owner_ip, "Post posted {$image->posted}").")" : ""; + return " + + Uploader + + ".($user->can(Permissions::EDIT_IMAGE_OWNER) ? " + $h_owner$h_ip, $h_date + + " : " + $h_owner$h_ip, $h_date + ")." + + $h_av + + "; } - public function get_source_editor_html(Image $image): HTMLElement + public function get_source_editor_html(Image $image): string { global $user; - return SHM_POST_INFO( - "Source Link", - DIV( - ["style" => "overflow: hidden; white-space: nowrap; max-width: 350px; text-overflow: ellipsis;"], - $this->format_source($image->get_source()) - ), - $user->can(Permissions::EDIT_IMAGE_SOURCE) ? INPUT(["type" => "text", "name" => "tag_edit__source", "value" => $image->get_source()]) : null, - link: Extension::is_enabled(SourceHistoryInfo::KEY) ? make_link("source_history/{$image->id}") : null, - ); + $h_source = html_escape($image->get_source()); + $f_source = $this->format_source($image->get_source()); + $style = "overflow: hidden; white-space: nowrap; max-width: 350px; text-overflow: ellipsis;"; + return " + + Source + + ".($user->can(Permissions::EDIT_IMAGE_SOURCE) ? " +
    $f_source
    + + " : " +
    $f_source
    + ")." + + + "; } - protected function format_source(string $source = null): HTMLElement + protected function format_source(string $source=null): string { if (!empty($source)) { - if (!str_contains($source, "://")) { - $source = "https://" . $source; + if (!str_starts_with($source, "http://") && !str_starts_with($source, "https://")) { + $source = "http://" . $source; } $proto_domain = explode("://", $source); - $h_source = $proto_domain[1]; + $h_source = html_escape($proto_domain[1]); + $u_source = html_escape($source); if (str_ends_with($h_source, "/")) { $h_source = substr($h_source, 0, -1); } - return A(["href" => $source], $h_source); + return "$h_source"; } - return rawHTML("Unknown"); + return "Unknown"; } - public function get_lock_editor_html(Image $image): HTMLElement + public function get_lock_editor_html(Image $image): string { global $user; - return SHM_POST_INFO( - "Locked", - $image->is_locked() ? "Yes (Only admins may edit these details)" : "No", - $user->can(Permissions::EDIT_IMAGE_LOCK) ? INPUT(["type" => "checkbox", "name" => "tag_edit__locked", "checked" => $image->is_locked()]) : null - ); + $b_locked = $image->is_locked() ? "Yes (Only admins may edit these details)" : "No"; + $h_locked = $image->is_locked() ? " checked" : ""; + return " + + Locked + + ".($user->can(Permissions::EDIT_IMAGE_LOCK) ? " + $b_locked + + " : " + $b_locked + ")." + + + "; } } diff --git a/ext/tag_editcloud/info.php b/ext/tag_editcloud/info.php index a26edb96..315c1bc4 100644 --- a/ext/tag_editcloud/info.php +++ b/ext/tag_editcloud/info.php @@ -10,6 +10,6 @@ class TagEditCloudInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Tag EditCloud"; - public array $authors = ["AtomicDryad" => null, "Luana Latte" => "luana.latte.cat@gmail.com"]; + public array $authors = ["AtomicDryad", "Luana Latte"=>"luana.latte.cat@gmail.com"]; public string $description = "Add or remove tags to the editor via clicking."; } diff --git a/ext/tag_editcloud/main.php b/ext/tag_editcloud/main.php index edeb3773..98396b7f 100644 --- a/ext/tag_editcloud/main.php +++ b/ext/tag_editcloud/main.php @@ -4,17 +4,13 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\rawHTML; - /* Todo: * usepref(todo2: port userpref) * theme junk */ class TagEditCloud extends Extension { - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { global $config; @@ -26,7 +22,7 @@ class TagEditCloud extends Extension } } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool("tageditcloud_disable", false); @@ -38,9 +34,9 @@ class TagEditCloud extends Extension $config->set_default_string("tageditcloud_ignoretags", 'tagme'); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { - $sort_by = ['Alphabetical' => 'a','Popularity' => 'p','Relevance' => 'r','Categories' => 'c']; + $sort_by = ['Alphabetical'=>'a','Popularity'=>'p','Relevance'=>'r','Categories'=>'c']; $sb = $event->panel->create_new_block("Tag Edit Cloud"); $sb->add_bool_option("tageditcloud_disable", "Disable Tag Selection Cloud: "); @@ -57,7 +53,7 @@ class TagEditCloud extends Extension $sb->add_text_option("tageditcloud_ignoretags"); } - private function build_tag_map(Image $image): ?HTMLElement + private function build_tag_map(Image $image): ?string { global $database, $config; @@ -88,8 +84,6 @@ class TagEditCloud extends Extension if (count($relevant_tags) == 0) { return null; } - $relevant_tag_ids = implode(',', array_map(fn ($t) => Tag::get_or_create_id($t), $relevant_tags)); - $tag_data = $database->get_all( " SELECT t2.tag AS tag, COUNT(image_id) AS count, FLOOR(LN(LN(COUNT(image_id) - :tag_min1 + 1)+1)*150)/200 AS scaled @@ -97,11 +91,11 @@ class TagEditCloud extends Extension JOIN image_tags it2 USING(image_id) JOIN tags t1 ON it1.tag_id = t1.id JOIN tags t2 ON it2.tag_id = t2.id - WHERE t1.count >= :tag_min2 AND t1.tag_id IN ($relevant_tag_ids) + WHERE t1.count >= :tag_min2 AND t1.tag IN(:relevant_tags) GROUP BY t2.tag ORDER BY count DESC LIMIT :limit", - ["tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count] + ["tag_min1" => $tags_min, "tag_min2" => $tags_min, "limit" => $max_count, "relevant_tags"=>$relevant_tags] ); break; /** @noinspection PhpMissingBreakStatementInspection */ @@ -163,7 +157,7 @@ class TagEditCloud extends Extension } $size = sprintf("%.2f", max($row['scaled'], 0.5)); - $js = html_escape('tageditcloud_toggle_tag(this,'.json_encode_ex($full_tag).')'); //Ugly, but it works + $js = html_escape('tageditcloud_toggle_tag(this,'.json_encode($full_tag).')'); //Ugly, but it works if (in_array($row['tag'], $image->get_tag_array())) { if ($used_first) { @@ -210,7 +204,7 @@ class TagEditCloud extends Extension $html .= "

    [show {$rem} more tags]"; } - return rawHTML("
    {$html}
    "); // FIXME: stupidasallhell + return "
    {$html}
    "; // FIXME: stupidasallhell } private function can_tag(Image $image): bool diff --git a/ext/tag_editcloud/style.css b/ext/tag_editcloud/style.css index 995d2d56..0c5b9566 100644 --- a/ext/tag_editcloud/style.css +++ b/ext/tag_editcloud/style.css @@ -1,4 +1,4 @@ -.tageditcloud span.tag-selected { +span.tag-selected { background:#88EE88; } diff --git a/ext/tag_history/info.php b/ext/tag_history/info.php index ec87ddaf..6cd8a6c1 100644 --- a/ext/tag_history/info.php +++ b/ext/tag_history/info.php @@ -10,6 +10,6 @@ class TagHistoryInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Tag History"; - public array $authors = ["Bzchan" => "bzchan@animemahou.com","jgen" => "jgen.tech@gmail.com"]; + public array $authors = ["Bzchan"=>"bzchan@animemahou.com","jgen"=>"jgen.tech@gmail.com"]; public string $description = "Keep a record of tag changes, and allows you to revert changes."; } diff --git a/ext/tag_history/main.php b/ext/tag_history/main.php index de9c01cb..1384f3db 100644 --- a/ext/tag_history/main.php +++ b/ext/tag_history/main.php @@ -4,25 +4,29 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\rawHTML; - class TagHistory extends Extension { /** @var TagHistoryTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + // in before tags are actually set, so that "get current tags" works + public function get_priority(): int + { + return 40; + } + + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int("history_limit", -1); } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_admin_block(); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -47,18 +51,18 @@ class TagHistory extends Extension } } - public function onRobotsBuilding(RobotsBuildingEvent $event): void + public function onRobotsBuilding(RobotsBuildingEvent $event) { $event->add_disallow("tag_history"); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { - $event->add_part(rawHTML(" + $event->add_part("
    - "), 20); + ", 20); } /* @@ -74,72 +78,15 @@ class TagHistory extends Extension } */ - public function onTagSet(TagSetEvent $event): void + public function onTagSet(TagSetEvent $event) { - global $database, $config, $user; - - $new_tags = Tag::implode($event->new_tags); - $old_tags = Tag::implode($event->old_tags); - - if ($new_tags == $old_tags) { - return; - } - - if (empty($old_tags)) { - /* no old tags, so we are probably adding the image for the first time */ - log_debug("tag_history", "adding new tag history: [$new_tags]"); - } else { - log_debug("tag_history", "adding tag history: [$old_tags] -> [$new_tags]"); - } - - $allowed = $config->get_int("history_limit"); - if ($allowed == 0) { - return; - } - - // if the image has no history, make one with the old tags - $entries = $database->get_one("SELECT COUNT(*) FROM tag_histories WHERE image_id = :id", ["id" => $event->image->id]); - if ($entries == 0 && !empty($old_tags)) { - $database->execute( - " - INSERT INTO tag_histories(image_id, tags, user_id, user_ip, date_set) - VALUES (:image_id, :tags, :user_id, :user_ip, now())", - ["image_id" => $event->image->id, "tags" => $old_tags, "user_id" => $config->get_int('anon_id'), "user_ip" => '127.0.0.1'] - ); - $entries++; - } - - // add a history entry - $database->execute( - " - INSERT INTO tag_histories(image_id, tags, user_id, user_ip, date_set) - VALUES (:image_id, :tags, :user_id, :user_ip, now())", - ["image_id" => $event->image->id, "tags" => $new_tags, "user_id" => $user->id, "user_ip" => get_real_ip()] - ); - $entries++; - - // if needed remove oldest one - if ($allowed == -1) { - return; - } - if ($entries > $allowed) { - // TODO: Make these queries better - /* - MySQL does NOT allow you to modify the same table which you use in the SELECT part. - Which means that these will probably have to stay as TWO separate queries... - - https://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html - https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause - */ - $min_id = $database->get_one("SELECT MIN(id) FROM tag_histories WHERE image_id = :image_id", ["image_id" => $event->image->id]); - $database->execute("DELETE FROM tag_histories WHERE id = :id", ["id" => $min_id]); - } + $this->add_tag_history($event->image, $event->tags); } - 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::BULK_EDIT_IMAGE_TAG)) { $event->add_nav_link("tag_history", new Link('tag_history/all/1'), "Tag Changes", NavLink::is_active(["tag_history"])); } @@ -147,7 +94,7 @@ class TagHistory extends Extension } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::BULK_EDIT_IMAGE_TAG)) { @@ -155,7 +102,7 @@ class TagHistory extends Extension } } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -189,7 +136,7 @@ class TagHistory extends Extension /** * This function is called when a revert request is received. */ - private function process_revert_request(int $revert_id): void + private function process_revert_request(int $revert_id) { global $page; @@ -216,7 +163,7 @@ class TagHistory extends Extension $stored_tags = $result['tags']; $image = Image::by_id($stored_image_id); - if (!$image instanceof Image) { + if (! $image instanceof Image) { throw new ImageDoesNotExist("Error: cannot find any image with the ID = ". $stored_image_id); } @@ -229,7 +176,7 @@ class TagHistory extends Extension $page->set_redirect(make_link('post/view/'.$stored_image_id)); } - protected function process_bulk_revert_request(): void + protected function process_bulk_revert_request() { if (isset($_POST['revert_name']) && !empty($_POST['revert_name'])) { $revert_name = $_POST['revert_name']; @@ -238,7 +185,7 @@ class TagHistory extends Extension } if (isset($_POST['revert_ip']) && !empty($_POST['revert_ip'])) { - $revert_ip = filter_var_ex($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); + $revert_ip = filter_var($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); if ($revert_ip === false) { // invalid ip given. @@ -268,9 +215,6 @@ class TagHistory extends Extension $this->theme->display_revert_ip_results(); } - /** - * @return array|null - */ public function get_tag_history_from_revert(int $revert_id): ?array { global $database; @@ -278,13 +222,10 @@ class TagHistory extends Extension SELECT tag_histories.*, users.name FROM tag_histories JOIN users ON tag_histories.user_id = users.id - WHERE tag_histories.id = :id", ["id" => $revert_id]); + WHERE tag_histories.id = :id", ["id"=>$revert_id]); return ($row ? $row : null); } - /** - * @return array - */ public function get_tag_history_from_id(int $image_id): array { global $database; @@ -295,13 +236,10 @@ class TagHistory extends Extension JOIN users ON tag_histories.user_id = users.id WHERE image_id = :id ORDER BY tag_histories.id DESC", - ["id" => $image_id] + ["id"=>$image_id] ); } - /** - * @return array - */ public function get_global_tag_history(int $page_id): array { global $database; @@ -311,13 +249,13 @@ class TagHistory extends Extension JOIN users ON tag_histories.user_id = users.id ORDER BY tag_histories.id DESC LIMIT 100 OFFSET :offset - ", ["offset" => ($page_id - 1) * 100]); + ", ["offset" => ($page_id-1)*100]); } /** * This function attempts to revert all changes by a given IP within an (optional) timeframe. */ - public function process_revert_all_changes(?string $name, ?string $ip, ?string $date): void + public function process_revert_all_changes(?string $name, ?string $ip, ?string $date) { global $database; @@ -390,7 +328,7 @@ class TagHistory extends Extension $stored_tags = $result['tags']; $image = Image::by_id($stored_image_id); - if (!$image instanceof Image) { + if (! $image instanceof Image) { continue; //throw new ImageDoesNotExist("Error: cannot find any image with the ID = ". $stored_image_id); } @@ -404,4 +342,71 @@ class TagHistory extends Extension log_info("tag_history", 'Reverted '.count($result).' edits.'); } + + /** + * This function is called just before an images tag are changed. + * + * #param string[] $tags + */ + private function add_tag_history(Image $image, array $tags) + { + global $database, $config, $user; + + $new_tags = Tag::implode($tags); + $old_tags = $image->get_tag_list(); + + if ($new_tags == $old_tags) { + return; + } + + if (empty($old_tags)) { + /* no old tags, so we are probably adding the image for the first time */ + log_debug("tag_history", "adding new tag history: [$new_tags]"); + } else { + log_debug("tag_history", "adding tag history: [$old_tags] -> [$new_tags]"); + } + + $allowed = $config->get_int("history_limit"); + if ($allowed == 0) { + return; + } + + // if the image has no history, make one with the old tags + $entries = $database->get_one("SELECT COUNT(*) FROM tag_histories WHERE image_id = :id", ["id"=>$image->id]); + if ($entries == 0 && !empty($old_tags)) { + $database->execute( + " + INSERT INTO tag_histories(image_id, tags, user_id, user_ip, date_set) + VALUES (:image_id, :tags, :user_id, :user_ip, now())", + ["image_id"=>$image->id, "tags"=>$old_tags, "user_id"=>$config->get_int('anon_id'), "user_ip"=>'127.0.0.1'] + ); + $entries++; + } + + // add a history entry + $database->execute( + " + INSERT INTO tag_histories(image_id, tags, user_id, user_ip, date_set) + VALUES (:image_id, :tags, :user_id, :user_ip, now())", + ["image_id"=>$image->id, "tags"=>$new_tags, "user_id"=>$user->id, "user_ip"=>get_real_ip()] + ); + $entries++; + + // if needed remove oldest one + if ($allowed == -1) { + return; + } + if ($entries > $allowed) { + // TODO: Make these queries better + /* + MySQL does NOT allow you to modify the same table which you use in the SELECT part. + Which means that these will probably have to stay as TWO separate queries... + + https://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html + https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause + */ + $min_id = $database->get_one("SELECT MIN(id) FROM tag_histories WHERE image_id = :image_id", ["image_id"=>$image->id]); + $database->execute("DELETE FROM tag_histories WHERE id = :id", ["id"=>$min_id]); + } + } } diff --git a/ext/tag_history/test.php b/ext/tag_history/test.php index 4c3fc084..e94f2900 100644 --- a/ext/tag_history/test.php +++ b/ext/tag_history/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class TagHistoryTest extends ShimmiePHPUnitTestCase { - public function testTagHistory(): void + public function testTagHistory() { $this->log_in_as_admin(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "old_tag"); diff --git a/ext/tag_history/theme.php b/ext/tag_history/theme.php index e16e1ca2..c6ea43b4 100644 --- a/ext/tag_history/theme.php +++ b/ext/tag_history/theme.php @@ -11,18 +11,11 @@ use function MicroHTML\LABEL; use function MicroHTML\SPAN; use function MicroHTML\rawHTML; -/** - * @phpstan-type HistoryEntry array{image_id:int,id:int,tags:string,date_set:string,user_id:string,user_ip:string,name:string} - */ class TagHistoryTheme extends Themelet { - /** @var string[] */ private array $messages = []; - /** - * @param HistoryEntry[] $history - */ - public function display_history_page(Page $page, int $image_id, array $history): void + public function display_history_page(Page $page, int $image_id, array $history) { $history_html = $this->history_list($history, true); @@ -32,10 +25,7 @@ class TagHistoryTheme extends Themelet $page->add_block(new Block("Tag History", $history_html, "main", 10)); } - /** - * @param HistoryEntry[] $history - */ - public function display_global_page(Page $page, array $history, int $page_number): void + public function display_global_page(Page $page, array $history, int $page_number) { $history_html = $this->history_list($history, false); @@ -44,9 +34,9 @@ class TagHistoryTheme extends Themelet $page->add_block(new Block("Tag History", $history_html, "main", 10)); $h_prev = ($page_number <= 1) ? "Prev" : - 'Prev'; + 'Prev'; $h_index = "Index"; - $h_next = 'Next'; + $h_next = 'Next'; $nav = $h_prev.' | '.$h_index.' | '.$h_next; $page->add_block(new Block("Navigation", $nav, "left", 0)); @@ -55,7 +45,7 @@ class TagHistoryTheme extends Themelet /** * Add a section to the admin page. */ - public function display_admin_block(string $validation_msg = ''): void + public function display_admin_block(string $validation_msg='') { global $page; @@ -82,21 +72,18 @@ class TagHistoryTheme extends Themelet /* * Show a standard page for results to be put into */ - public function display_revert_ip_results(): void + public function display_revert_ip_results() { global $page; $html = implode("\n", $this->messages); $page->add_block(new Block("Bulk Revert Results", $html)); } - public function add_status(string $title, string $body): void + public function add_status(string $title, string $body) { $this->messages[] = '

    '. $title .'
    '. $body .'

    '; } - /** - * @param HistoryEntry[] $history - */ protected function history_list(array $history, bool $select_2nd): string { $history_list = ""; @@ -116,9 +103,6 @@ class TagHistoryTheme extends Themelet "; } - /** - * @param HistoryEntry $fields - */ protected function history_entry(array $fields, bool $selected): string { global $user; @@ -130,21 +114,21 @@ class TagHistoryTheme extends Themelet $ip = $user->can(Permissions::VIEW_IP) ? rawHTML(" " . show_ip($fields['user_ip'], "Tagging >>$image_id as '$current_tags'")) : null; - $setter = A(["href" => make_link("user/" . url_escape($name))], $name); + $setter = A(["href"=>make_link("user/" . url_escape($name))], $name); $current_tags = Tag::explode($current_tags); $taglinks = SPAN(); foreach ($current_tags as $tag) { - $taglinks->appendChild(A(["href" => search_link([$tag])], $tag)); + $taglinks->appendChild(A(["href"=>make_link("post/list/$tag/1")], $tag)); $taglinks->appendChild(" "); } return (string)LI( - INPUT(["type" => "radio", "name" => "revert", "id" => "$current_id", "value" => "$current_id", "checked" => $selected]), - A(["href" => make_link("post/view/$image_id")], $image_id), + INPUT(["type"=>"radio", "name"=>"revert", "id"=>"$current_id", "value"=>"$current_id", "checked"=>$selected]), + A(["href"=>make_link("post/view/$image_id")], $image_id), ": ", LABEL( - ["for" => "$current_id"], + ["for"=>"$current_id"], $taglinks, " - ", $setter, diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php index 284f7586..14dc67d4 100644 --- a/ext/tag_list/main.php +++ b/ext/tag_list/main.php @@ -11,9 +11,9 @@ class TagList extends Extension /** @var TagListTheme */ protected Themelet $theme; - private mixed $tagcategories = null; + private $tagcategories = null; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_int(TagListConfig::LENGTH, 15); @@ -27,7 +27,7 @@ class TagList extends Extension $config->set_default_bool(TagListConfig::PAGES, false); } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page; @@ -57,7 +57,7 @@ class TagList extends Extension } } - public function onPostListBuilding(PostListBuildingEvent $event): void + public function onPostListBuilding(PostListBuildingEvent $event) { global $config, $page; if ($config->get_int(TagListConfig::LENGTH) > 0) { @@ -69,27 +69,27 @@ class TagList extends Extension } } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("tags", new Link('tags/map'), "Tags"); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "tags") { + if ($event->parent=="tags") { $event->add_nav_link("tags_map", new Link('tags/map'), "Map"); $event->add_nav_link("tags_alphabetic", new Link('tags/alphabetic'), "Alphabetic"); $event->add_nav_link("tags_popularity", new Link('tags/popularity'), "Popularity"); } } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $config, $page; if ($config->get_int(TagListConfig::LENGTH) > 0) { $type = $config->get_string(TagListConfig::IMAGE_TYPE); if ($type == TagListConfig::TYPE_TAGS || $type == TagListConfig::TYPE_BOTH) { - if (Extension::is_enabled(TagCategoriesInfo::KEY) and $config->get_bool(TagCategoriesConfig::SPLIT_ON_VIEW)) { + if (class_exists("Shimmie2\TagCategories") and $config->get_bool(TagCategoriesConfig::SPLIT_ON_VIEW)) { $this->add_split_tags_block($page, $event->image); } else { $this->add_tags_block($page, $event->image); @@ -101,7 +101,7 @@ class TagList extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Tag Map Options"); $sb->add_int_option(TagListConfig::TAGS_MIN, "Only show tags used at least "); @@ -152,9 +152,6 @@ class TagList extends Extension } } - /** - * @return int[] - */ private static function get_omitted_tags(): array { global $cache, $config, $database; @@ -163,7 +160,7 @@ class TagList extends Extension $results = $cache->get("tag_list_omitted_tags:".$tags_config); if (is_null($results)) { - $tags = Tag::explode($tags_config, false); + $tags = explode(" ", $tags_config); if (count($tags) == 0) { return []; @@ -217,11 +214,11 @@ class TagList extends Extension FROM tags WHERE count >= :tags_min ORDER BY LOWER(substr(tag, 1, 1)) - ", ["tags_min" => $tags_min]); + ", ["tags_min"=>$tags_min]); $html = ""; foreach ($tag_data as $a) { - $html .= " $a"; + $html .= " $a"; } $html .= "\n


    "; @@ -234,7 +231,7 @@ class TagList extends Extension $h_map = "Map"; $h_alphabetic = "Alphabetic"; $h_popularity = "Popularity"; - $h_all = "Show All"; + $h_all = "Show All"; return "$h_index
     
    $h_map
    $h_alphabetic
    $h_popularity
     
    $h_all"; } @@ -251,25 +248,26 @@ class TagList extends Extension md5("tc" . $tags_min . $starts_with . VERSION) ); if (file_exists($cache_key)) { - return file_get_contents_ex($cache_key); + return file_get_contents($cache_key); } + // SHIT: PDO/pgsql has problems using the same named param twice -_-;; $tag_data = $database->get_all(" SELECT tag, - FLOOR(LN(LN(count - :tags_min + 1)+1)*1.5*100)/100 AS scaled + FLOOR(LOG(2.7, LOG(2.7, count - :tags_min2 + 1)+1)*1.5*100)/100 AS scaled FROM tags WHERE count >= :tags_min AND LOWER(tag) LIKE LOWER(:starts_with) ORDER BY LOWER(tag) - ", ["tags_min" => $tags_min, "starts_with" => $starts_with]); + ", ["tags_min"=>$tags_min, "tags_min2"=>$tags_min, "starts_with"=>$starts_with]); $html = ""; if ($config->get_bool(TagListConfig::PAGES)) { $html .= $this->build_az(); } $tag_category_dict = []; - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $this->tagcategories = new TagCategories(); $tag_category_dict = $this->tagcategories->getKeyedDict(); } @@ -277,11 +275,11 @@ class TagList extends Extension $h_tag = html_escape($row['tag']); $size = sprintf("%.2f", (float)$row['scaled']); $link = $this->theme->tag_link($row['tag']); - if ($size < 0.5) { + if ($size<0.5) { $size = 0.5; } $h_tag_no_underscores = str_replace("_", " ", $h_tag); - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $h_tag_no_underscores = $this->tagcategories->getTagHtml($h_tag, $tag_category_dict); } $html .= " $h_tag_no_underscores \n"; @@ -307,7 +305,7 @@ class TagList extends Extension md5("ta" . $tags_min . $starts_with . VERSION) ); if (file_exists($cache_key)) { - return file_get_contents_ex($cache_key); + return file_get_contents($cache_key); } $tag_data = $database->get_pairs(" @@ -316,7 +314,7 @@ class TagList extends Extension WHERE count >= :tags_min AND LOWER(tag) LIKE LOWER(:starts_with) ORDER BY LOWER(tag) - ", ["tags_min" => $tags_min, "starts_with" => $starts_with]); + ", ["tags_min"=>$tags_min, "starts_with"=>$starts_with]); $html = ""; if ($config->get_bool(TagListConfig::PAGES)) { @@ -339,7 +337,7 @@ class TagList extends Extension mb_internal_encoding('UTF-8'); $tag_category_dict = []; - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $this->tagcategories = new TagCategories(); $tag_category_dict = $this->tagcategories->getKeyedDict(); } @@ -351,14 +349,14 @@ class TagList extends Extension foreach ($tag_data as $tag => $count) { // In PHP, $array["10"] sets the array key as int(10), not string("10")... $tag = (string)$tag; - if ($lastLetter != mb_strtolower(substr($tag, 0, strlen($starts_with) + 1))) { - $lastLetter = mb_strtolower(substr($tag, 0, strlen($starts_with) + 1)); + if ($lastLetter != mb_strtolower(substr($tag, 0, strlen($starts_with)+1))) { + $lastLetter = mb_strtolower(substr($tag, 0, strlen($starts_with)+1)); $h_lastLetter = html_escape($lastLetter); $html .= "

    $h_lastLetter
    "; } $link = $this->theme->tag_link($tag); $h_tag = html_escape($tag); - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $h_tag = $this->tagcategories->getTagHtml($h_tag, $tag_category_dict, " ($count)"); } $html .= "$h_tag\n"; @@ -389,15 +387,15 @@ class TagList extends Extension md5("tp" . $tags_min . VERSION) ); if (file_exists($cache_key)) { - return file_get_contents_ex($cache_key); + return file_get_contents($cache_key); } $tag_data = $database->get_all(" - SELECT tag, count, FLOOR(LOG(10, count)) AS scaled + SELECT tag, count, FLOOR(LOG(count)) AS scaled FROM tags WHERE count >= :tags_min ORDER BY count DESC, tag ASC - ", ["tags_min" => $tags_min]); + ", ["tags_min"=>$tags_min]); $html = "Results grouped by log10(n)"; $lastLog = ""; @@ -455,7 +453,7 @@ class TagList extends Extension } } - private function add_split_tags_block(Page $page, Image $image): void + private function add_split_tags_block(Page $page, Image $image) { global $database; @@ -466,7 +464,7 @@ class TagList extends Extension AND image_tags.image_id = :image_id ORDER BY tags.count DESC "; - $args = ["image_id" => $image->id]; + $args = ["image_id"=>$image->id]; $tags = $database->get_all($query, $args); if (count($tags) > 0) { @@ -474,7 +472,7 @@ class TagList extends Extension } } - private function add_tags_block(Page $page, Image $image): void + private function add_tags_block(Page $page, Image $image) { global $database; @@ -485,7 +483,7 @@ class TagList extends Extension AND image_tags.image_id = :image_id ORDER BY tags.count DESC "; - $args = ["image_id" => $image->id]; + $args = ["image_id"=>$image->id]; $tags = $database->get_all($query, $args); if (count($tags) > 0) { @@ -493,7 +491,7 @@ class TagList extends Extension } } - private function add_popular_block(Page $page): void + private function add_popular_block(Page $page) { global $cache, $database, $config; @@ -520,7 +518,7 @@ class TagList extends Extension "; } - $args = ["popular_tag_list_length" => $config->get_int(TagListConfig::POPULAR_TAG_LIST_LENGTH)]; + $args = ["popular_tag_list_length"=>$config->get_int(TagListConfig::POPULAR_TAG_LIST_LENGTH)]; $tags = $database->get_all($query, $args); @@ -532,9 +530,9 @@ class TagList extends Extension } /** - * @param string[] $search + * #param string[] $search */ - private function add_refine_block(Page $page, array $search): void + private function add_refine_block(Page $page, array $search) { global $config; @@ -551,14 +549,11 @@ class TagList extends Extension } } - /** - * @param string[] $search - * @return array - */ public static function get_related_tags(array $search, int $limit): array { global $cache, $database; + $wild_tags = $search; $cache_key = "related_tags:" . md5(Tag::implode($search)); $related_tags = $cache->get($cache_key); diff --git a/ext/tag_list/test.php b/ext/tag_list/test.php index a345508d..f054a72e 100644 --- a/ext/tag_list/test.php +++ b/ext/tag_list/test.php @@ -6,10 +6,9 @@ namespace Shimmie2; class TagListTest extends ShimmiePHPUnitTestCase { - /** @var string[] */ private array $pages = ["map", "alphabetic", "popularity", "categories"]; - public function testTagList(): void + public function testTagList() { $this->get_page('tags/map'); $this->assert_title('Tag List'); @@ -26,19 +25,19 @@ class TagListTest extends ShimmiePHPUnitTestCase # FIXME: test that these show the right stuff } - public function testMinCount(): void + public function testMinCount() { foreach ($this->pages as $page) { - $this->get_page("tags/$page", ["mincount" => 999999]); + $this->get_page("tags/$page", ["mincount"=>999999]); $this->assert_title("Tag List"); - $this->get_page("tags/$page", ["mincount" => 1]); + $this->get_page("tags/$page", ["mincount"=>1]); $this->assert_title("Tag List"); - $this->get_page("tags/$page", ["mincount" => 0]); + $this->get_page("tags/$page", ["mincount"=>0]); $this->assert_title("Tag List"); - $this->get_page("tags/$page", ["mincount" => -1]); + $this->get_page("tags/$page", ["mincount"=>-1]); $this->assert_title("Tag List"); } } diff --git a/ext/tag_list/theme.php b/ext/tag_list/theme.php index 3922ba8e..27cc8e54 100644 --- a/ext/tag_list/theme.php +++ b/ext/tag_list/theme.php @@ -9,24 +9,24 @@ class TagListTheme extends Themelet public string $heading = ""; public string $list = ""; public ?string $navigation; - private mixed $tagcategories = null; + private $tagcategories = null; - public function set_heading(string $text): void + public function set_heading(string $text) { $this->heading = $text; } - public function set_tag_list(string $list): void + public function set_tag_list(string $list) { $this->list = $list; } - public function set_navigation(string $nav): void + public function set_navigation(string $nav) { $this->navigation = $nav; } - public function display_page(Page $page): void + public function display_page(Page $page) { $page->set_title("Tag List"); $page->set_heading($this->heading); @@ -44,7 +44,7 @@ class TagListTheme extends Themelet $tag_count_is_visible = $config->get_bool("tag_list_numbers"); return ' - +
    ' . ($tag_info_link_is_visible ? '' : '') . ('') . @@ -60,10 +60,13 @@ class TagListTheme extends Themelet '; } - /** - * @param array $tag_infos + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) */ - public function display_split_related_block(Page $page, array $tag_infos): void + public function display_split_related_block(Page $page, $tag_infos) { global $config; @@ -71,7 +74,7 @@ class TagListTheme extends Themelet asort($tag_infos); } - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $this->tagcategories = new TagCategories(); $tag_category_dict = $this->tagcategories->getKeyedDict(); } else { @@ -121,8 +124,11 @@ class TagListTheme extends Themelet } } - /** - * @param array $tag_infos + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) */ private function get_tag_list_html(array $tag_infos, string $sort): string { @@ -130,7 +136,7 @@ class TagListTheme extends Themelet asort($tag_infos); } - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $this->tagcategories = new TagCategories(); $tag_category_dict = $this->tagcategories->getKeyedDict(); } else { @@ -150,10 +156,13 @@ class TagListTheme extends Themelet return $main_html; } - /** - * @param array $tag_infos + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) */ - public function display_related_block(Page $page, array $tag_infos, string $block_name): void + public function display_related_block(Page $page, $tag_infos, $block_name) { global $config; @@ -165,10 +174,14 @@ class TagListTheme extends Themelet $page->add_block(new Block($block_name, $main_html, "left", 10)); } - /** - * @param array $tag_infos + + /* + * $tag_infos = array( + * array('tag' => $tag, 'count' => $number_of_uses), + * ... + * ) */ - public function display_popular_block(Page $page, array $tag_infos): void + public function display_popular_block(Page $page, $tag_infos) { global $config; @@ -181,11 +194,14 @@ class TagListTheme extends Themelet $page->add_block(new Block("Popular Tags", $main_html, "left", 60)); } - /** - * @param array $tag_infos - * @param string[] $search + /* + * $tag_infos = array( + * array('tag' => $tag), + * ... + * ) + * $search = the current array of tags being searched for */ - public function display_refine_block(Page $page, array $tag_infos, array $search): void + public function display_refine_block(Page $page, $tag_infos, $search) { global $config; @@ -198,11 +214,6 @@ class TagListTheme extends Themelet $page->add_block(new Block("Refine Search", $main_html, "left", 60)); } - /** - * @param array{tag: string, count: int} $row - * @param array $tag_category_dict - * @return array{0: string, 1: string} - */ public function return_tag(array $row, array $tag_category_dict): array { global $config; @@ -241,9 +252,6 @@ class TagListTheme extends Themelet return [$category, $display_html]; } - /** - * @param string[] $tags - */ protected function ars(string $tag, array $tags): string { // FIXME: a better fix would be to make sure the inputs are correct @@ -258,9 +266,6 @@ class TagListTheme extends Themelet return $html; } - /** - * @param string[] $tags - */ protected function get_remove_link(array $tags, string $tag): string { if (!in_array($tag, $tags) && !in_array("-$tag", $tags)) { @@ -271,9 +276,6 @@ class TagListTheme extends Themelet } } - /** - * @param string[] $tags - */ protected function get_add_link(array $tags, string $tag): string { if (in_array($tag, $tags)) { @@ -284,9 +286,6 @@ class TagListTheme extends Themelet } } - /** - * @param string[] $tags - */ protected function get_subtract_link(array $tags, string $tag): string { if (in_array("-$tag", $tags)) { @@ -299,6 +298,7 @@ class TagListTheme extends Themelet public function tag_link(string $tag): string { - return search_link([$tag]); + $u_tag = url_escape(Tag::caret($tag)); + return make_link("post/list/$u_tag/1"); } } diff --git a/ext/tag_tools/main.php b/ext/tag_tools/main.php index 93202c81..2743f1f6 100644 --- a/ext/tag_tools/main.php +++ b/ext/tag_tools/main.php @@ -10,12 +10,12 @@ class TagTools extends Extension /** @var TagToolsTheme */ protected Themelet $theme; - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { $this->theme->display_form(); } - public function onAdminAction(AdminActionEvent $event): void + public function onAdminAction(AdminActionEvent $event) { $action = $event->action; if (method_exists($this, $action)) { diff --git a/ext/tag_tools/test.php b/ext/tag_tools/test.php index 41e88715..e99acbf4 100644 --- a/ext/tag_tools/test.php +++ b/ext/tag_tools/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class TagToolsTest extends ShimmiePHPUnitTestCase { - public function testLowercaseAndSetCase(): void + public function testLowercaseAndSetCase() { // Create a problem $ts = time(); // we need a tag that hasn't been used before @@ -34,7 +34,7 @@ class TagToolsTest extends ShimmiePHPUnitTestCase } # FIXME: make sure the admin tools actually work - public function testRecount(): void + public function testRecount() { global $database; @@ -43,7 +43,7 @@ class TagToolsTest extends ShimmiePHPUnitTestCase send_event(new UserLoginEvent(User::by_name(self::$admin_name))); $database->execute( "INSERT INTO tags(tag, count) VALUES(:tag, :count)", - ["tag" => "tes$ts", "count" => 42] + ["tag"=>"tes$ts", "count"=>42] ); // Fix @@ -54,7 +54,7 @@ class TagToolsTest extends ShimmiePHPUnitTestCase 0, $database->get_one( "SELECT count FROM tags WHERE tag = :tag", - ["tag" => "tes$ts"] + ["tag"=>"tes$ts"] ) ); } diff --git a/ext/tag_tools/theme.php b/ext/tag_tools/theme.php index 7a365401..2f52c6b4 100644 --- a/ext/tag_tools/theme.php +++ b/ext/tag_tools/theme.php @@ -8,7 +8,7 @@ use function MicroHTML\INPUT; class TagToolsTheme extends Themelet { - protected function button(string $name, string $action, bool $protected = false): string + protected function button(string $name, string $action, bool $protected=false): string { $c_protected = $protected ? " protected" : ""; $html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected"); @@ -28,7 +28,7 @@ class TagToolsTheme extends Themelet * 'recount tag use' * etc */ - public function display_form(): void + public function display_form() { global $page; @@ -39,7 +39,7 @@ class TagToolsTheme extends Themelet $html = (string)SHM_SIMPLE_FORM( "admin/set_tag_case", - INPUT(["type" => 'text', "name" => 'tag', "placeholder" => 'Enter tag with correct case', "autocomplete" => 'off']), + INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "autocomplete"=>'off']), SHM_SUBMIT('Set Tag Case'), ); $page->add_block(new Block("Set Tag Case", $html)); diff --git a/ext/tagger_xml/info.php b/ext/tagger_xml/info.php index 3de7720d..c7754bab 100644 --- a/ext/tagger_xml/info.php +++ b/ext/tagger_xml/info.php @@ -10,7 +10,7 @@ class TaggerXMLInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Tagger AJAX backend"; - public array $authors = ["Artanis (Erik Youngren)" => "artanis.00@gmail.com"]; + public array $authors = ["Artanis (Erik Youngren)"=>"artanis.00@gmail.com"]; public ExtensionVisibility $visibility = ExtensionVisibility::HIDDEN; public string $description = "Advanced Tagging v2 AJAX backend"; } diff --git a/ext/tagger_xml/main.php b/ext/tagger_xml/main.php index 80faca03..e945e2eb 100644 --- a/ext/tagger_xml/main.php +++ b/ext/tagger_xml/main.php @@ -12,14 +12,14 @@ class TaggerXML extends Extension return 10; } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { if ($event->page_matches("tagger/tags")) { global $page; //$match_tags = null; //$image_tags = null; - $tags = null; + $tags=null; if (isset($_GET['s'])) { // tagger/tags[/...]?s=$string // return matching tags in XML form $tags = $this->match_tag_list($_GET['s']); @@ -58,7 +58,7 @@ class TaggerXML extends Extension // $exclude = $event->get_arg(1)? "AND NOT IN ".$this->image_tags($event->get_arg(1)) : null; // Hidden Tags - $hidden = $config->get_string('ext-tagger_show-hidden', 'N') == 'N' ? + $hidden = $config->get_string('ext-tagger_show-hidden', 'N')=='N' ? "AND substring(tag,1,1) != '.'" : null; $q_where = "WHERE {$match} {$hidden} AND count > 0"; @@ -69,7 +69,7 @@ class TaggerXML extends Extension $q_from = "FROM (SELECT * FROM `tags` {$q_where} ". "ORDER BY count DESC LIMIT {$limit_rows} OFFSET 0) AS `c_tags`"; $q_where = null; - $count = ["max" => $count]; + $count = ["max"=>$count]; } else { $q_from = "FROM `tags`"; $count = []; @@ -93,20 +93,17 @@ class TaggerXML extends Extension $tags = $database->execute(" SELECT tags.* FROM image_tags JOIN tags ON image_tags.tag_id = tags.id - WHERE image_id=:image_id ORDER BY tag", ['image_id' => $image_id]); + WHERE image_id=:image_id ORDER BY tag", ['image_id'=>$image_id]); return $this->list_to_xml($tags, "image", (string)$image_id); } - /** - * @param array $misc - */ - private function list_to_xml(\FFSPHP\PDOStatement $tags, string $type, string $query, ?array $misc = []): string + private function list_to_xml(\FFSPHP\PDOStatement $tags, string $type, string $query, ?array $misc=[]): string { $props = [ - "id" => $type, - "query" => $query, + "id"=>$type, + "query"=>$query, // @phpstan-ignore-next-line - "rows" => $tags->_numOfRows + "rows"=>$tags->_numOfRows ]; if (!is_null($misc)) { foreach ($misc as $attr => $val) { @@ -116,18 +113,18 @@ class TaggerXML extends Extension $list = new \MicroHTML\HTMLElement("list", [$props]); foreach ($tags as $tag) { - $list->appendChild(new \MicroHTML\HTMLElement("tag", [["id" => $tag["id"], "count" => $tag["count"]], $tag["tag"]])); + $list->appendChild(new \MicroHTML\HTMLElement("tag", [["id"=>$tag["id"], "count"=>$tag["count"]], $tag["tag"]])); } return (string)($list); } - /** - * @param array $values - */ - private function count(string $query, array $values): int + private function count(string $query, $values) { global $database; - return $database->get_one("SELECT COUNT(*) FROM `tags` $query", $values); + return $database->execute( + "SELECT COUNT(*) FROM `tags` $query", + $values + )->fields['COUNT(*)']; } } diff --git a/ext/tips/info.php b/ext/tips/info.php index 01fd91aa..d8dd65cd 100644 --- a/ext/tips/info.php +++ b/ext/tips/info.php @@ -10,7 +10,7 @@ class TipsInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Random Tip"; - public array $authors = ["Sein Kraft" => "mail@seinkraft.info"]; + public array $authors = ["Sein Kraft"=>"mail@seinkraft.info"]; public string $license = "GPLv2"; public string $description = "Show a random line of text in the subheader space"; public ?string $documentation = "Formatting is done with HTML"; diff --git a/ext/tips/main.php b/ext/tips/main.php index 7d4234d3..172184a6 100644 --- a/ext/tips/main.php +++ b/ext/tips/main.php @@ -34,7 +34,7 @@ class Tips extends Extension /** @var TipsTheme */ protected Themelet $theme; - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -50,7 +50,7 @@ class Tips extends Extension " INSERT INTO tips (enable, image, text) VALUES (:enable, :image, :text)", - ["enable" => true, "image" => "coins.png", "text" => "Do you like this extension? Please support us for developing new ones. Donate through paypal."] + ["enable"=>true, "image"=>"coins.png", "text"=>"Do you like this extension? Please support us for developing new ones. Donate through paypal."] ); $this->set_version("ext_tips_version", 2); @@ -61,7 +61,7 @@ class Tips extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -98,17 +98,17 @@ class Tips 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::TIPS_ADMIN)) { $event->add_nav_link("tips", new Link('tips/list'), "Tips Editor"); } } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if ($user->can(Permissions::TIPS_ADMIN)) { @@ -116,12 +116,12 @@ class Tips extends Extension } } - private function manageTips(): void + private function manageTips() { $data_href = get_base_href(); $url = $data_href."/ext/tips/images/"; - $dirPath = dir_ex('./ext/tips/images'); + $dirPath = dir('./ext/tips/images'); $images = []; while (($file = $dirPath->read()) !== false) { if ($file[0] != ".") { @@ -134,18 +134,18 @@ class Tips extends Extension $this->theme->manageTips($url, $images); } - public function onCreateTip(CreateTipEvent $event): void + public function onCreateTip(CreateTipEvent $event) { global $database; $database->execute( " INSERT INTO tips (enable, image, text) VALUES (:enable, :image, :text)", - ["enable" => $event->enable, "image" => $event->image, "text" => $event->text] + ["enable"=>$event->enable, "image"=>$event->image, "text"=>$event->text] ); } - private function getTip(): void + private function getTip() { global $database; @@ -158,14 +158,14 @@ class Tips extends Extension WHERE enable = :true ORDER BY RAND() LIMIT 1 - ", ["true" => true]); + ", ["true"=>true]); if ($tip) { $this->theme->showTip($url, $tip); } } - private function getAll(): void + private function getAll() { global $database; @@ -177,20 +177,20 @@ class Tips extends Extension $this->theme->showAll($url, $tips); } - private function setStatus(int $tipID): void + private function setStatus(int $tipID) { global $database; - $tip = $database->get_row("SELECT * FROM tips WHERE id = :id ", ["id" => $tipID]); + $tip = $database->get_row("SELECT * FROM tips WHERE id = :id ", ["id"=>$tipID]); $enable = bool_escape($tip['enable']); - $database->execute("UPDATE tips SET enable = :enable WHERE id = :id", ["enable" => $enable, "id" => $tipID]); + $database->execute("UPDATE tips SET enable = :enable WHERE id = :id", ["enable"=>$enable, "id"=>$tipID]); } - public function onDeleteTip(DeleteTipEvent $event): void + public function onDeleteTip(DeleteTipEvent $event) { global $database; - $database->execute("DELETE FROM tips WHERE id = :id", ["id" => $event->tip_id]); + $database->execute("DELETE FROM tips WHERE id = :id", ["id"=>$event->tip_id]); } } diff --git a/ext/tips/test.php b/ext/tips/test.php index 737da52f..4a3d22dc 100644 --- a/ext/tips/test.php +++ b/ext/tips/test.php @@ -14,7 +14,7 @@ class TipsTest extends ShimmiePHPUnitTestCase $database->execute("DELETE FROM tips"); } - public function testImageless(): void + public function testImageless() { global $database; $this->log_in_as_admin(); @@ -32,7 +32,7 @@ class TipsTest extends ShimmiePHPUnitTestCase $this->assert_no_text("a postless tip"); } - public function testImaged(): void + public function testImaged() { global $database; $this->log_in_as_admin(); @@ -50,7 +50,7 @@ class TipsTest extends ShimmiePHPUnitTestCase $this->assert_no_text("a postless tip"); } - public function testDisabled(): void + public function testDisabled() { global $database; $this->log_in_as_admin(); diff --git a/ext/tips/theme.php b/ext/tips/theme.php index 01f106e9..c9787b22 100644 --- a/ext/tips/theme.php +++ b/ext/tips/theme.php @@ -4,15 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -/** - * @phpstan-type Tip array{id: int, image: string, text: string, enable: bool} - */ class TipsTheme extends Themelet { - /** - * @param string[] $images - */ - public function manageTips(string $url, array $images): void + public function manageTips($url, $images) { global $page; $select = ""; - foreach ($options as $display => $value) { + foreach ($options as $display=>$value) { $html .= ""; } diff --git a/ext/transcode_video/info.php b/ext/transcode_video/info.php index 81f7f7fc..4dd7877c 100644 --- a/ext/transcode_video/info.php +++ b/ext/transcode_video/info.php @@ -10,8 +10,8 @@ class TranscodeVideoInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Transcode Video"; - 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 = "Allows admins to automatically and manually transcode videos."; - public ?string $documentation = "Requires ffmpeg"; + public ?string $documentation ="Requires ffmpeg"; } diff --git a/ext/transcode_video/main.php b/ext/transcode_video/main.php index 5d3bcf42..ad8b698e 100644 --- a/ext/transcode_video/main.php +++ b/ext/transcode_video/main.php @@ -36,7 +36,7 @@ class TranscodeVideo extends Extension } - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool(TranscodeVideoConfig::ENABLED, true); @@ -44,19 +44,19 @@ class TranscodeVideo extends Extension $config->set_default_bool(TranscodeVideoConfig::UPLOAD_TO_NATIVE_CONTAINER, false); } - public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { global $user; - if ($event->image->video === true && $user->can(Permissions::EDIT_FILES)) { + if ($event->image->video===true && $user->can(Permissions::EDIT_FILES)) { $options = self::get_output_options($event->image->get_mime(), $event->image->video_codec); - if (!empty($options) && sizeof($options) > 1) { + if (!empty($options)&&sizeof($options)>1) { $event->add_part($this->theme->get_transcode_html($event->image, $options)); } } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Video Transcode"); $sb->start_table(); @@ -65,39 +65,39 @@ class TranscodeVideo extends Extension $sb->end_table(); } - /* - public function onDataUpload(DataUploadEvent $event): void - { - global $config; +/* + public function onDataUpload(DataUploadEvent $event) + { + global $config; - if ($config->get_bool(TranscodeVideoConfig::UPLOAD) == true) { - $ext = strtolower($event->type); + if ($config->get_bool(TranscodeVideoConfig::UPLOAD) == true) { + $ext = strtolower($event->type); - $ext = Media::normalize_format($ext); + $ext = Media::normalize_format($ext); - if ($event->type=="gif"&&Media::is_animated_gif($event->tmpname)) { + if ($event->type=="gif"&&Media::is_animated_gif($event->tmpname)) { + return; + } + + if (in_array($ext, array_values(self::INPUT_FORMATS))) { + $target_format = $config->get_string(TranscodeVideoConfig::UPLOAD_PREFIX.$ext); + if (empty($target_format)) { return; } - - if (in_array($ext, array_values(self::INPUT_FORMATS))) { - $target_format = $config->get_string(TranscodeVideoConfig::UPLOAD_PREFIX.$ext); - if (empty($target_format)) { - return; - } - try { - $new_image = $this->transcode_image($event->tmpname, $ext, $target_format); - $event->set_tmpname($new_image, Media::determine_ext($target_format)); - } catch (Exception $e) { - log_error("transcode_video", "Error while performing upload transcode: ".$e->getMessage()); - // We don't want to interfere with the upload process, - // so if something goes wrong the untranscoded image jsut continues - } + try { + $new_image = $this->transcode_image($event->tmpname, $ext, $target_format); + $event->set_tmpname($new_image, Media::determine_ext($target_format)); + } catch (Exception $e) { + log_error("transcode_video", "Error while performing upload transcode: ".$e->getMessage()); + // We don't want to interfere with the upload process, + // so if something goes wrong the untranscoded image jsut continues } } } - */ + } +*/ - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; @@ -126,7 +126,7 @@ class TranscodeVideo extends Extension } } - public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void + public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) { global $user; @@ -141,7 +141,7 @@ class TranscodeVideo extends Extension } } - public function onBulkAction(BulkActionEvent $event): void + public function onBulkAction(BulkActionEvent $event) { global $user, $database, $page; @@ -155,17 +155,23 @@ class TranscodeVideo extends Extension $total = 0; foreach ($event->items as $image) { try { + $database->begin_transaction(); + + $output_image = $this->transcode_and_replace_video($image, $format); // If a subsequent transcode fails, the database needs to have everything about the previous // transcodes recorded already, otherwise the image entries will be stuck pointing to // missing image files - $transcoded = $database->with_savepoint(function () use ($image, $format) { - return $this->transcode_and_replace_video($image, $format); - }); - if ($transcoded) { + $database->commit(); + if ($output_image!=$image) { $total++; } } catch (\Exception $e) { log_error("transcode_video", "Error while bulk transcode on item {$image->id} to $format: ".$e->getMessage()); + try { + $database->rollback(); + } catch (\Exception $e) { + // is this safe? o.o + } } } $page->flash("Transcoded $total items"); @@ -174,18 +180,16 @@ class TranscodeVideo extends Extension } } - /** - * @return array - */ - private static function get_output_options(?string $starting_container = null, ?string $starting_codec = null): array + private static function get_output_options(?String $starting_container = null, ?String $starting_codec = null): array { $output = ["" => ""]; + foreach (VideoContainers::ALL as $container) { - if ($starting_container == $container) { + if ($starting_container==$container) { continue; } - if (!empty($starting_codec) && + if (!empty($starting_codec)&& !VideoContainers::is_video_codec_supported($container, $starting_codec)) { continue; } @@ -195,13 +199,13 @@ class TranscodeVideo extends Extension return $output; } - private function transcode_and_replace_video(Image $image, string $target_mime): bool + private function transcode_and_replace_video(Image $image, String $target_mime): Image { - if ($image->get_mime() == $target_mime) { - return false; + if ($image->get_mime()==$target_mime) { + return $image; } - if ($image->video == null || ($image->video === true && empty($image->video_codec))) { + if ($image->video==null||($image->video===true && empty($image->video_codec))) { // If image predates the media system, or the video codec support, run a media check send_event(new MediaCheckPropertiesEvent($image)); $image->save_to_db(); @@ -211,14 +215,35 @@ class TranscodeVideo extends Extension } $original_file = warehouse_path(Image::IMAGE_DIR, $image->hash); - $tmp_filename = shm_tempnam("transcode_video"); - $tmp_filename = $this->transcode_video($original_file, $image->video_codec, $target_mime, $tmp_filename); - send_event(new ImageReplaceEvent($image, $tmp_filename)); - return true; + + $tmp_filename = tempnam(sys_get_temp_dir(), "shimmie_transcode_video"); + try { + $tmp_filename = $this->transcode_video($original_file, $image->video_codec, $target_mime, $tmp_filename); + + $new_image = new Image(); + $new_image->hash = md5_file($tmp_filename); + $new_image->filesize = filesize($tmp_filename); + $new_image->filename = $image->filename; + $new_image->width = $image->width; + $new_image->height = $image->height; + + /* Move the new image into the main storage location */ + $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash); + if (!@copy($tmp_filename, $target)) { + throw new VideoTranscodeException("Failed to copy new post file from temporary location ({$tmp_filename}) to archive ($target)"); + } + + send_event(new ImageReplaceEvent($image->id, $new_image)); + + return $new_image; + } finally { + /* Remove temporary file */ + @unlink($tmp_filename); + } } - private function transcode_video(string $source_file, string $source_video_codec, string $target_mime, string $target_file): string + private function transcode_video(String $source_file, String $source_video_codec, String $target_mime, string $target_file): string { global $config; diff --git a/ext/transcode_video/theme.php b/ext/transcode_video/theme.php index cb74de42..244730b2 100644 --- a/ext/transcode_video/theme.php +++ b/ext/transcode_video/theme.php @@ -4,16 +4,12 @@ declare(strict_types=1); namespace Shimmie2; -use function MicroHTML\{rawHTML}; - class TranscodeVideoTheme extends Themelet { - /** + /* * Display a link to resize an image - * - * @param array $options */ - public function get_transcode_html(Image $image, array $options): \MicroHTML\HTMLElement + public function get_transcode_html(Image $image, array $options): string { $html = " ".make_form( @@ -30,16 +26,13 @@ class TranscodeVideoTheme extends Themelet "; - return rawHTML($html); + return $html; } - /** - * @param array $options - */ public function get_transcode_picker_html(array $options): string { $html = " $form = SHM_FORM("upload", "POST", true); $form->appendChild( emptyHTML( - INPUT(["id" => "data[]", "name" => "data[]", "size" => "16", "type" => "file", "accept" => $accept, "multiple" => true]), - INPUT(["name" => "tags", "type" => "text", "placeholder" => "tagme", "class" => "autocomplete_tags", "required" => true]), - INPUT(["type" => "submit", "value" => "Post"]), + INPUT(["id"=>"data[]", "name"=>"data[]", "size"=>"16", "type"=>"file", "accept"=>$accept, "multiple"=>true]), + INPUT(["name"=>"tags", "type"=>"text", "placeholder"=>"tagme", "class"=>"autocomplete_tags", "required"=>true, "autocomplete"=>"off"]), + INPUT(["type"=>"submit", "value"=>"Post"]), ) ); return DIV( - ["class" => 'mini_upload'], + ["class"=>'mini_upload'], $form, - SMALL( - "(", - $max_size > 0 ? "File limit $max_kb" : null, - $max_size > 0 && $max_total_size > 0 ? " / " : null, - $max_total_size > 0 ? "Total limit $max_total_kb" : null, - ")", - ), - NOSCRIPT(BR(), A(["href" => make_link("upload")], "Larger Form")) + SMALL("(Max file size is $max_kb)"), + SMALL(BR(), "(Max total size is $max_total_kb)"), + NOSCRIPT(BR(), A(["href"=>make_link("upload")], "Larger Form")) ); } + protected function get_accept(): string { return ".".join(",.", DataHandlerExtension::get_all_supported_exts()); diff --git a/ext/user/events.php b/ext/user/events.php index 8a558141..aacf7fa1 100644 --- a/ext/user/events.php +++ b/ext/user/events.php @@ -8,10 +8,9 @@ use MicroHTML\HTMLElement; class UserBlockBuildingEvent extends Event { - /** @var array */ public array $parts = []; - public function add_link(string|HTMLElement $name, string $link, int $position = 50): void + public function add_link(string|HTMLElement $name, string $link, int $position=50): void { while (isset($this->parts[$position])) { $position++; @@ -22,7 +21,6 @@ class UserBlockBuildingEvent extends Event class UserOperationsBuildingEvent extends Event { - /** @var string[] */ public array $parts = []; public function __construct(public User $user, public BaseConfig $user_config) @@ -38,7 +36,6 @@ class UserOperationsBuildingEvent extends Event class UserPageBuildingEvent extends Event { - /** @var array */ public array $stats = []; public function __construct(public User $display_user) @@ -46,7 +43,7 @@ class UserPageBuildingEvent extends Event parent::__construct(); } - public function add_stats(string $html, int $position = 50): void + public function add_stats(string $html, int $position=50) { while (isset($this->stats[$position])) { $position++; diff --git a/ext/user/main.php b/ext/user/main.php index 36384270..7b8679d2 100644 --- a/ext/user/main.php +++ b/ext/user/main.php @@ -24,7 +24,7 @@ class UserNameColumn extends TextColumn { public function display(array $row): HTMLElement { - return A(["href" => make_link("user/{$row[$this->name]}")], $row[$this->name]); + return A(["href"=>make_link("user/{$row[$this->name]}")], $row[$this->name]); } } @@ -38,7 +38,7 @@ class UserActionColumn extends ActionColumn public function display(array $row): HTMLElement { - return A(["href" => search_link(["user={$row['name']}"])], "Posts"); + return A(["href"=>make_link("post/list/user={$row['name']}/1")], "Posts"); } } @@ -46,8 +46,9 @@ class UserTable extends Table { public function __construct(\FFSPHP\PDO $db) { + global $_shm_user_classes; $classes = []; - foreach (UserClass::$known_classes as $cls) { + foreach ($_shm_user_classes as $cls) { $classes[$cls->name] = $cls->name; } ksort($classes); @@ -66,7 +67,7 @@ class UserTable extends Table new UserActionColumn(), ]); $this->order_by = ["id DESC"]; - $this->table_attrs = ["class" => "zebra form"]; + $this->table_attrs = ["class" => "zebra"]; } } @@ -138,7 +139,7 @@ class UserPage extends Extension /** @var UserPageTheme $theme */ public Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool("login_signup_enabled", true); @@ -148,27 +149,20 @@ class UserPage extends Extension $config->set_default_string("avatar_gravatar_default", ""); $config->set_default_string("avatar_gravatar_rating", "g"); $config->set_default_bool("login_tac_bbcode", true); - $config->set_default_bool("user_email_required", true); } - public function onUserLogin(UserLoginEvent $event): void + public function onUserLogin(UserLoginEvent $event) { global $user; $user = $event->user; } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { - global $config, $database, $page, $user; + global $config, $database, $page, $user, $_shm_user_classes; $this->show_user_info(); - if ($user->can(Permissions::VIEW_HELLBANNED)) { - $page->add_html_header(""); - } elseif(!$user->can(Permissions::HELLBANNED)) { - $page->add_html_header(""); - } - if ($event->page_matches("user_admin")) { if ($event->get_arg(0) == "login") { if (isset($_POST['user']) && isset($_POST['pass'])) { @@ -198,8 +192,8 @@ class UserPage extends Extension } elseif ($event->get_arg(0) == "classes") { $this->theme->display_user_classes( $page, - UserClass::$known_classes, - (new \ReflectionClass(Permissions::class))->getReflectionConstants() + $_shm_user_classes, + (new \ReflectionClass('\Shimmie2\Permissions'))->getReflectionConstants() ); } elseif ($event->get_arg(0) == "logout") { $this->page_logout(); @@ -263,7 +257,7 @@ class UserPage extends Extension } } - public function onUserPageBuilding(UserPageBuildingEvent $event): void + public function onUserPageBuilding(UserPageBuildingEvent $event) { global $user, $config; @@ -296,7 +290,7 @@ class UserPage extends Extension } } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { global $user; if ($user->is_anonymous()) { @@ -340,7 +334,7 @@ class UserPage extends Extension } } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { global $config; @@ -353,14 +347,12 @@ class UserPage extends Extension $sb->start_table(); $sb->add_bool_option(UserConfig::ENABLE_API_KEYS, "Enable user API keys", true); $sb->add_bool_option("login_signup_enabled", "Allow new signups", true); - $sb->add_bool_option("user_email_required", "Require email address", true); $sb->add_longtext_option("login_tac", "Terms & Conditions", true); $sb->add_choice_option( "user_loginshowprofile", [ "Return to previous page" => 0, // 0 is default - "Send to user profile" => 1, - ], + "Send to user profile" => 1], "On log in/out", true ); @@ -376,17 +368,17 @@ class UserPage extends Extension $sb->add_choice_option( "avatar_gravatar_type", [ - 'Default' => 'default', - 'Wavatar' => 'wavatar', - 'Monster ID' => 'monsterid', - 'Identicon' => 'identicon' + 'Default'=>'default', + 'Wavatar'=>'wavatar', + 'Monster ID'=>'monsterid', + 'Identicon'=>'identicon' ], "Type", true ); $sb->add_choice_option( "avatar_gravatar_rating", - ['G' => 'g', 'PG' => 'pg', 'R' => 'r', 'X' => 'x'], + ['G'=>'g', 'PG'=>'pg', 'R'=>'r', 'X'=>'x'], "Rating", true ); @@ -394,21 +386,21 @@ class UserPage extends Extension $sb->end_table(); } - 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::EDIT_USER_PASSWORD)) { $event->add_nav_link("user_admin", new Link('user_admin/list'), "User List", NavLink::is_active(["user_admin"])); } } - if ($event->parent === "user" && !$user->is_anonymous()) { + if ($event->parent==="user" && !$user->is_anonymous()) { $event->add_nav_link("logout", new Link('user_admin/logout'), "Log Out", false, 90); } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; $event->add_link("My Profile", make_link("user")); @@ -421,7 +413,7 @@ class UserPage extends Extension $event->add_link("Log Out", make_link("user_admin/logout"), 99); } - public function onAdminBuilding(AdminBuildingEvent $event): void + public function onAdminBuilding(AdminBuildingEvent $event) { global $user; if ($user->can(Permissions::CREATE_OTHER_USER)) { @@ -429,14 +421,13 @@ class UserPage extends Extension } } - public function onUserCreation(UserCreationEvent $event): void + public function onUserCreation(UserCreationEvent $event) { - global $config, $page, $user; - $name = $event->username; //$pass = $event->password; //$email = $event->email; + global $config, $page, $user; if (!$user->can(Permissions::CREATE_USER)) { throw new UserCreationException("Account creation is currently disabled"); } @@ -461,14 +452,6 @@ class UserPage extends Extension if ($event->password != $event->password2) { throw new UserCreationException("Passwords don't match"); } - if( - // Users who can create other users (ie, admins) are exempt - // from the email requirement - !$user->can(Permissions::CREATE_OTHER_USER) && - ($config->get_bool("user_email_required") && empty($event->email)) - ) { - throw new UserCreationException("Email address is required"); - } $new_user = $this->create_user($event); if ($event->login) { @@ -479,13 +462,10 @@ class UserPage extends Extension public const USER_SEARCH_REGEX = "/^(?:poster|user)(!?)[=|:](.*)$/i"; public const USER_ID_SEARCH_REGEX = "/^(?:poster|user)_id(!?)[=|:]([0-9]+)$/i"; - /** - * @param string[] $context - */ public static function has_user_query(array $context): bool { foreach ($context as $term) { - if (preg_match(self::USER_SEARCH_REGEX, $term) || + if (preg_match(self::USER_SEARCH_REGEX, $term)|| preg_match(self::USER_ID_SEARCH_REGEX, $term)) { return true; } @@ -493,7 +473,7 @@ class UserPage extends Extension return false; } - public function onSearchTermParse(SearchTermParseEvent $event): void + public function onSearchTermParse(SearchTermParseEvent $event) { global $user; @@ -519,9 +499,9 @@ class UserPage extends Extension } } - public function onHelpPageBuilding(HelpPageBuildingEvent $event): void + public function onHelpPageBuilding(HelpPageBuildingEvent $event) { - if ($event->key === HelpPages::SEARCH) { + if ($event->key===HelpPages::SEARCH) { $block = new Block(); $block->header = "Users"; $block->body = (string)$this->theme->get_help_html(); @@ -636,7 +616,7 @@ class UserPage extends Extension $database->execute( "INSERT INTO users (name, pass, joindate, email, class) VALUES (:username, :hash, now(), :email, :class)", - ["username" => $event->username, "hash" => '', "email" => $email, "class" => $class] + ["username"=>$event->username, "hash"=>'', "email"=>$email, "class"=>$class] ); $uid = $database->get_last_insert_id('users_id_seq'); $new_user = User::by_name($event->username); @@ -663,13 +643,13 @@ class UserPage extends Extension $page->add_cookie( "user", $name, - time() + 60 * 60 * 24 * 365, + time()+60*60*24*365, '/' ); $page->add_cookie( "session", $this->get_session_id($name), - time() + 60 * 60 * 24 * $config->get_int('login_memory'), + time()+60*60*24*$config->get_int('login_memory'), '/' ); } @@ -764,9 +744,6 @@ class UserPage extends Extension } } - /** - * @return array - */ private function count_upload_ips(User $duser): array { global $database; @@ -777,12 +754,9 @@ class UserPage extends Extension FROM images WHERE owner_id=:id GROUP BY owner_ip - ORDER BY max(posted) DESC", ["id" => $duser->id]); + ORDER BY max(posted) DESC", ["id"=>$duser->id]); } - /** - * @return array - */ private function count_comment_ips(User $duser): array { global $database; @@ -793,15 +767,12 @@ class UserPage extends Extension FROM comments WHERE owner_id=:id GROUP BY owner_ip - ORDER BY max(posted) DESC", ["id" => $duser->id]); + ORDER BY max(posted) DESC", ["id"=>$duser->id]); } - /** - * @return array - */ private function count_log_ips(User $duser): array { - if (!Extension::is_enabled(LogDatabaseInfo::KEY)) { + if (!class_exists('Shimmie2\LogDatabase')) { return []; } global $database; @@ -812,10 +783,10 @@ class UserPage extends Extension FROM score_log WHERE username=:username GROUP BY address - ORDER BY MAX(date_sent) DESC", ["username" => $duser->name]); + ORDER BY MAX(date_sent) DESC", ["username"=>$duser->name]); } - private function delete_user(Page $page, bool $with_images = false, bool $with_comments = false): void + private function delete_user(Page $page, bool $with_images=false, bool $with_comments=false): void { global $user, $config, $database; @@ -831,7 +802,7 @@ class UserPage extends Extension "You need to specify the account number to edit" )); } else { - $uid = int_escape((string)$_POST['id']); + $uid = int_escape($_POST['id']); $duser = User::by_id($uid); log_warning("user", "Deleting user #{$uid} (@{$duser->name})"); @@ -869,7 +840,7 @@ class UserPage extends Extension ); $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link()); + $page->set_redirect(make_link("post/list")); } } } diff --git a/ext/user/main.php.orig b/ext/user/main.php.orig deleted file mode 100644 index 9e975cb3..00000000 --- a/ext/user/main.php.orig +++ /dev/null @@ -1,850 +0,0 @@ -make_link("user/{$row[$this->name]}")], $row[$this->name]); - } -} - -class UserActionColumn extends ActionColumn -{ - public function __construct() - { - parent::__construct("id"); - $this->sortable = false; - } - - public function display(array $row): HTMLElement - { - return A(["href"=>make_link("post/list/user={$row['name']}/1")], "Posts"); - } -} - -class UserTable extends Table -{ - public function __construct(\FFSPHP\PDO $db) - { - global $_shm_user_classes; - $classes = []; - foreach ($_shm_user_classes as $cls) { - $classes[$cls->name] = $cls->name; - } - ksort($classes); - parent::__construct($db); - $this->table = "users"; - $this->base_query = "SELECT * FROM users"; - $this->size = 100; - $this->limit = 1000000; - $this->set_columns([ - new IntegerColumn("id", "ID"), - new UserNameColumn("name", "Name"), - new EnumColumn("class", "Class", $classes), - // Added later, for admins only - // new TextColumn("email", "Email"), - new DateColumn("joindate", "Join Date"), - new UserActionColumn(), - ]); - $this->order_by = ["id DESC"]; - $this->table_attrs = ["class" => "zebra"]; - } -} - -class UserCreationException extends SCoreException -{ -} - -class NullUserException extends SCoreException -{ -} - -#[Type] -class LoginResult -{ - public function __construct( - #[Field] - public User $user, - #[Field] - public ?string $session = null, - #[Field] - public ?string $error = null, - ) { - } - - #[Mutation] - public static function login(string $username, string $password): LoginResult - { - global $config; - $duser = User::by_name_and_pass($username, $password); - if (!is_null($duser)) { - return new LoginResult( - $duser, - UserPage::get_session_id($duser->name), - null - ); - } else { - $anon = User::by_id($config->get_int("anon_id", 0)); - return new LoginResult( - $anon, - null, - "No user found" - ); - } - } - - #[Mutation] - public static function create_user(string $username, string $password1, string $password2, string $email): LoginResult - { - global $config; - try { - $uce = send_event(new UserCreationEvent($username, $password1, $password2, $email, true)); - return new LoginResult( - User::by_name($username), - UserPage::get_session_id($username), - null - ); - } catch (UserCreationException $ex) { - return new LoginResult( - User::by_id($config->get_int("anon_id", 0)), - null, - $ex->getMessage() - ); - } - } -} - -class UserPage extends Extension -{ - /** @var UserPageTheme $theme */ - public Themelet $theme; - - public function onInitExt(InitExtEvent $event) - { - global $config; - $config->set_default_bool("login_signup_enabled", true); - $config->set_default_int("login_memory", 365); - $config->set_default_string("avatar_host", "none"); - $config->set_default_int("avatar_gravatar_size", 80); - $config->set_default_string("avatar_gravatar_default", ""); - $config->set_default_string("avatar_gravatar_rating", "g"); - $config->set_default_bool("login_tac_bbcode", true); - } - - public function onUserLogin(UserLoginEvent $event) - { - global $user; - $user = $event->user; - } - - public function onPageRequest(PageRequestEvent $event) - { - global $config, $database, $page, $user, $_shm_user_classes; - - $this->show_user_info(); - - if ($event->page_matches("user_admin")) { - if ($event->get_arg(0) == "login") { - if (isset($_POST['user']) && isset($_POST['pass'])) { - $this->page_login($_POST['user'], $_POST['pass']); - } else { - $this->theme->display_login_page($page); - } - } elseif ($event->get_arg(0) == "recover") { - $this->page_recover($_POST['username']); - } elseif ($event->get_arg(0) == "create") { - $this->page_create(); - } elseif ($event->get_arg(0) == "create_other") { - send_event(new UserCreationEvent($_POST['name'], $_POST['pass1'], $_POST['pass1'], $_POST['email'], false)); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("admin")); - $page->flash("Created new user"); - } elseif ($event->get_arg(0) == "list") { - $t = new UserTable($database->raw_db()); - $t->token = $user->get_auth_token(); - $t->inputs = $_GET; - if ($user->can(Permissions::DELETE_USER)) { - $col = new TextColumn("email", "Email"); - // $t->columns[] = $col; - array_splice($t->columns, 2, 0, [$col]); - } - $this->theme->display_user_list($page, $t->table($t->query()), $t->paginator()); - } elseif ($event->get_arg(0) == "classes") { - $this->theme->display_user_classes( - $page, - $_shm_user_classes, -<<<<<<< HEAD - (new \ReflectionClass('\Shimmie2\Permissions'))->getReflectionConstants() -======= - (new \ReflectionClass('\Shimmie2\Permissions'))->getConstants() ->>>>>>> e51c6bed (show a table of user classes, see #921) - ); - } elseif ($event->get_arg(0) == "logout") { - $this->page_logout(); - } - - if (!$user->check_auth_token()) { - return; - } elseif ($event->get_arg(0) == "change_name") { - $input = validate_input([ - 'id' => 'user_id,exists', - 'name' => 'user_name', - ]); - $duser = User::by_id($input['id']); - $this->change_name_wrapper($duser, $input['name']); - } elseif ($event->get_arg(0) == "change_pass") { - $input = validate_input([ - 'id' => 'user_id,exists', - 'pass1' => 'password', - 'pass2' => 'password', - ]); - $duser = User::by_id($input['id']); - $this->change_password_wrapper($duser, $input['pass1'], $input['pass2']); - } elseif ($event->get_arg(0) == "change_email") { - $input = validate_input([ - 'id' => 'user_id,exists', - 'address' => 'email', - ]); - $duser = User::by_id($input['id']); - $this->change_email_wrapper($duser, $input['address']); - } elseif ($event->get_arg(0) == "change_class") { - $input = validate_input([ - 'id' => 'user_id,exists', - 'class' => 'user_class', - ]); - $duser = User::by_id($input['id']); - $this->change_class_wrapper($duser, $input['class']); - } elseif ($event->get_arg(0) == "delete_user") { - $this->delete_user($page, isset($_POST["with_images"]), isset($_POST["with_comments"])); - } - } - - if ($event->page_matches("user")) { - $display_user = ($event->count_args() == 0) ? $user : User::by_name($event->get_arg(0)); - if ($event->count_args() == 0 && $user->is_anonymous()) { - $this->theme->display_error( - 401, - "Not Logged In", - "You aren't logged in. First do that, then you can see your stats." - ); - } elseif (!is_null($display_user) && ($display_user->id != $config->get_int("anon_id"))) { - $e = send_event(new UserPageBuildingEvent($display_user)); - $this->display_stats($e); - } else { - $this->theme->display_error( - 404, - "No Such User", - "If you typed the ID by hand, try again; if you came from a link on this ". - "site, it might be bug report time..." - ); - } - } - } - - public function onUserPageBuilding(UserPageBuildingEvent $event) - { - global $user, $config; - - $h_join_date = autodate($event->display_user->join_date); - if ($event->display_user->can(Permissions::HELLBANNED)) { - $h_class = $event->display_user->class->parent->name; - } else { - $h_class = $event->display_user->class->name; - } - - $event->add_stats("Joined: $h_join_date", 10); - if ($user->name == $event->display_user->name) { - $event->add_stats("Current IP: " . get_real_ip(), 80); - } - $event->add_stats("Class: $h_class", 90); - - $av = $event->display_user->get_avatar_html(); - if ($av) { - $event->add_stats($av, 0); - } elseif (( - $config->get_string("avatar_host") == "gravatar" - ) && - ($user->id == $event->display_user->id) - ) { - $event->add_stats( - "No avatar? This gallery uses Gravatar for avatar hosting, use the". - "
    same email address here and there to have your avatar synced
    ", - 0 - ); - } - } - - public function onPageNavBuilding(PageNavBuildingEvent $event) - { - global $user; - if ($user->is_anonymous()) { - $event->add_nav_link("user", new Link('user_admin/login'), "Account", null, 10); - } else { - $event->add_nav_link("user", new Link('user'), "Account", null, 10); - } - } - - private function display_stats(UserPageBuildingEvent $event): void - { - global $user, $page, $config; - - ksort($event->stats); - $this->theme->display_user_page($event->display_user, $event->stats); - - if (!$user->is_anonymous()) { - if ($user->id == $event->display_user->id || $user->can("edit_user_info")) { - $user_config = UserConfig::get_for_user($event->display_user->id); - - $uobe = send_event(new UserOperationsBuildingEvent($event->display_user, $user_config)); - $page->add_block(new Block("Operations", $this->theme->build_operations($event->display_user, $uobe), "main", 60)); - } - } - - if ($user->id == $event->display_user->id) { - $ubbe = send_event(new UserBlockBuildingEvent()); - ksort($ubbe->parts); - $this->theme->display_user_links($page, $user, $ubbe->parts); - } - if ( - ($user->can(Permissions::VIEW_IP) || ($user->is_logged_in() && $user->id == $event->display_user->id)) && # admin or self-user - ($event->display_user->id != $config->get_int('anon_id')) # don't show anon's IP list, it is le huge - ) { - $this->theme->display_ip_list( - $page, - $this->count_upload_ips($event->display_user), - $this->count_comment_ips($event->display_user), - $this->count_log_ips($event->display_user) - ); - } - } - - public function onSetupBuilding(SetupBuildingEvent $event) - { - global $config; - - $hosts = [ - "None" => "none", - "Gravatar" => "gravatar" - ]; - - $sb = $event->panel->create_new_block("User Options"); - $sb->start_table(); - $sb->add_bool_option(UserConfig::ENABLE_API_KEYS, "Enable user API keys", true); - $sb->add_bool_option("login_signup_enabled", "Allow new signups", true); - $sb->add_longtext_option("login_tac", "Terms & Conditions", true); - $sb->add_choice_option( - "user_loginshowprofile", - [ - "Return to previous page" => 0, // 0 is default - "Send to user profile" => 1], - "On log in/out", - true - ); - $sb->add_choice_option("avatar_host", $hosts, "Avatars", true); - - if ($config->get_string("avatar_host") == "gravatar") { - $sb->start_table_row(); - $sb->start_table_cell(2); - $sb->add_label("
    Gravatar Options
    "); - $sb->end_table_cell(); - $sb->end_table_row(); - - $sb->add_choice_option( - "avatar_gravatar_type", - [ - 'Default'=>'default', - 'Wavatar'=>'wavatar', - 'Monster ID'=>'monsterid', - 'Identicon'=>'identicon' - ], - "Type", - true - ); - $sb->add_choice_option( - "avatar_gravatar_rating", - ['G'=>'g', 'PG'=>'pg', 'R'=>'r', 'X'=>'x'], - "Rating", - true - ); - } - $sb->end_table(); - } - - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) - { - global $user; - if ($event->parent==="system") { - if ($user->can(Permissions::EDIT_USER_PASSWORD)) { - $event->add_nav_link("user_admin", new Link('user_admin/list'), "User List", NavLink::is_active(["user_admin"])); - } - } - - if ($event->parent==="user" && !$user->is_anonymous()) { - $event->add_nav_link("logout", new Link('user_admin/logout'), "Log Out", false, 90); - } - } - - public function onUserBlockBuilding(UserBlockBuildingEvent $event) - { - global $user; - $event->add_link("My Profile", make_link("user")); - if ($user->can(Permissions::EDIT_USER_PASSWORD)) { - $event->add_link("User List", make_link("user_admin/list"), 97); - } - if ($user->can(Permissions::EDIT_USER_CLASS)) { - $event->add_link("User Classes", make_link("user_admin/classes"), 98); - } - $event->add_link("Log Out", make_link("user_admin/logout"), 99); - } - - public function onAdminBuilding(AdminBuildingEvent $event) - { - global $user; - if ($user->can(Permissions::CREATE_OTHER_USER)) { - $this->theme->display_user_creator(); - } - } - - public function onUserCreation(UserCreationEvent $event) - { - $name = $event->username; - //$pass = $event->password; - //$email = $event->email; - - global $config, $page, $user; - if (!$user->can(Permissions::CREATE_USER)) { - throw new UserCreationException("Account creation is currently disabled"); - } - if (!$config->get_bool("login_signup_enabled") && !$user->can(Permissions::CREATE_OTHER_USER)) { - throw new UserCreationException("Account creation is currently disabled"); - } - if (strlen($name) < 1) { - throw new UserCreationException("Username must be at least 1 character"); - } - if (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) { - throw new UserCreationException( - "Username contains invalid characters. Allowed characters are ". - "letters, numbers, dash, and underscore" - ); - } - if (User::by_name($name)) { - throw new UserCreationException("That username is already taken"); - } - if (!captcha_check()) { - throw new UserCreationException("Error in captcha"); - } - if ($event->password != $event->password2) { - throw new UserCreationException("Passwords don't match"); - } - - $new_user = $this->create_user($event); - if ($event->login) { - send_event(new UserLoginEvent($new_user)); - } - } - - public const USER_SEARCH_REGEX = "/^(?:poster|user)(!?)[=|:](.*)$/i"; - public const USER_ID_SEARCH_REGEX = "/^(?:poster|user)_id(!?)[=|:]([0-9]+)$/i"; - - public static function has_user_query(array $context): bool - { - foreach ($context as $term) { - if (preg_match(self::USER_SEARCH_REGEX, $term)|| - preg_match(self::USER_ID_SEARCH_REGEX, $term)) { - return true; - } - } - return false; - } - - public function onSearchTermParse(SearchTermParseEvent $event) - { - global $user; - - if (is_null($event->term)) { - return; - } - - $matches = []; - if (preg_match(self::USER_SEARCH_REGEX, $event->term, $matches)) { - $duser = User::by_name($matches[2]); - if (is_null($duser)) { - throw new SearchTermParseException( - "Can't find the user named ".html_escape($matches[2]) - ); - } - $event->add_querylet(new Querylet("images.owner_id {$matches[1]}= {$duser->id}")); - } elseif (preg_match(self::USER_ID_SEARCH_REGEX, $event->term, $matches)) { - $user_id = int_escape($matches[2]); - $event->add_querylet(new Querylet("images.owner_id {$matches[1]}= $user_id")); - } elseif ($user->can(Permissions::VIEW_IP) && preg_match("/^(?:poster|user)_ip[=|:]([0-9\.]+)$/i", $event->term, $matches)) { - $user_ip = $matches[1]; // FIXME: ip_escape? - $event->add_querylet(new Querylet("images.owner_ip = '$user_ip'")); - } - } - - public function onHelpPageBuilding(HelpPageBuildingEvent $event) - { - if ($event->key===HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Users"; - $block->body = (string)$this->theme->get_help_html(); - $event->add_block($block); - } - } - - private function show_user_info(): void - { - global $user, $page; - // user info is shown on all pages - if ($user->is_anonymous()) { - $this->theme->display_login_block($page); - } else { - $ubbe = send_event(new UserBlockBuildingEvent()); - ksort($ubbe->parts); - $this->theme->display_user_block($page, $user, $ubbe->parts); - } - } - - private function page_login(string $name, string $pass): void - { - global $config, $page; - - if (empty($name) || empty($pass)) { - $this->theme->display_error(400, "Error", "Username or password left blank"); - return; - } - - $duser = User::by_name_and_pass($name, $pass); - if (!is_null($duser)) { - send_event(new UserLoginEvent($duser)); - $this->set_login_cookie($duser->name); - $page->set_mode(PageMode::REDIRECT); - - // Try returning to previous page - if ($config->get_int("user_loginshowprofile", 0)) { - $page->set_redirect(referer_or(make_link(), ["user/"])); - } else { - $page->set_redirect(make_link("user")); - } - } else { - $this->theme->display_error(401, "Error", "No user with those details was found"); - } - } - - private function page_logout(): void - { - global $page, $config; - $page->add_cookie("session", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); - if (SPEED_HAX) { - # to keep as few versions of content as possible, - # make cookies all-or-nothing - $page->add_cookie("user", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); - } - log_info("user", "Logged out"); - $page->set_mode(PageMode::REDIRECT); - - // Try forwarding to same page on logout unless user comes from registration page - if ($config->get_int("user_loginshowprofile", 0)) { - $page->set_redirect(referer_or(make_link(), ["post/"])); - } else { - $page->set_redirect(make_link()); - } - } - - private function page_recover(string $username): void - { - $my_user = User::by_name($username); - if (is_null($my_user)) { - $this->theme->display_error(404, "Error", "There's no user with that name"); - } elseif (is_null($my_user->email)) { - $this->theme->display_error(400, "Error", "That user has no registered email address"); - } else { - throw new SCoreException("Email sending not implemented"); - } - } - - private function page_create(): void - { - global $config, $page, $user; - if (!$user->can(Permissions::CREATE_USER)) { - $this->theme->display_error(403, "Account creation blocked", "Account creation is currently disabled"); - return; - } - - if (!$config->get_bool("login_signup_enabled")) { - $this->theme->display_signups_disabled($page); - } elseif (!isset($_POST['name'])) { - $this->theme->display_signup_page($page); - } else { - try { - $uce = send_event(new UserCreationEvent($_POST['name'], $_POST['pass1'], $_POST['pass2'], $_POST['email'], true)); - $this->set_login_cookie($uce->username); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("user")); - } catch (UserCreationException $ex) { - $this->theme->display_error(400, "User Creation Error", $ex->getMessage()); - } - } - } - - private function create_user(UserCreationEvent $event): User - { - global $database; - - $email = (!empty($event->email)) ? $event->email : null; - - // if there are currently no admins, the new user should be one - $need_admin = ($database->get_one("SELECT COUNT(*) FROM users WHERE class='admin'") == 0); - $class = $need_admin ? 'admin' : 'user'; - - $database->execute( - "INSERT INTO users (name, pass, joindate, email, class) VALUES (:username, :hash, now(), :email, :class)", - ["username"=>$event->username, "hash"=>'', "email"=>$email, "class"=>$class] - ); - $uid = $database->get_last_insert_id('users_id_seq'); - $new_user = User::by_name($event->username); - $new_user->set_password($event->password); - - log_info("user", "Created User #$uid ({$event->username})"); - - return $new_user; - } - - public static function get_session_id(string $name): string - { - global $config; - $addr = get_session_ip($config); - $hash = User::by_name($name)->passhash; - return md5($hash.$addr); - } - - private function set_login_cookie(string $name): void - { - global $config, $page; - - - $page->add_cookie( - "user", - $name, - time()+60*60*24*365, - '/' - ); - $page->add_cookie( - "session", - $this->get_session_id($name), - time()+60*60*24*$config->get_int('login_memory'), - '/' - ); - } - - private function user_can_edit_user(User $a, User $b): bool - { - if ($a->is_anonymous()) { - $this->theme->display_error(401, "Error", "You aren't logged in"); - return false; - } - - if ( - ($a->name == $b->name) || - ($b->can(Permissions::PROTECTED) && $a->class->name == "admin") || - (!$b->can(Permissions::PROTECTED) && $a->can(Permissions::EDIT_USER_INFO)) - ) { - return true; - } else { - $this->theme->display_error(401, "Error", "You need to be an admin to change other people's details"); - return false; - } - } - - private function redirect_to_user(User $duser): void - { - global $page, $user; - - if ($user->id == $duser->id) { - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("user")); - } else { - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("user/{$duser->name}")); - } - } - - private function change_name_wrapper(User $duser, string $name): void - { - global $page, $user; - - if ($user->can(Permissions::EDIT_USER_NAME) && $this->user_can_edit_user($user, $duser)) { - $duser->set_name($name); - $page->flash("Username changed"); - // TODO: set login cookie if user changed themselves - $this->redirect_to_user($duser); - } else { - $this->theme->display_error(400, "Error", "Permission denied"); - } - } - - private function change_password_wrapper(User $duser, string $pass1, string $pass2): void - { - global $page, $user; - - if ($this->user_can_edit_user($user, $duser)) { - if ($pass1 != $pass2) { - $this->theme->display_error(400, "Error", "Passwords don't match"); - } else { - // FIXME: send_event() - $duser->set_password($pass1); - - if ($duser->id == $user->id) { - $this->set_login_cookie($duser->name); - } - - $page->flash("Password changed"); - $this->redirect_to_user($duser); - } - } - } - - private function change_email_wrapper(User $duser, string $address): void - { - global $page, $user; - - if ($this->user_can_edit_user($user, $duser)) { - $duser->set_email($address); - - $page->flash("Email changed"); - $this->redirect_to_user($duser); - } - } - - private function change_class_wrapper(User $duser, string $class): void - { - global $page, $user; - - if ($user->class->name == "admin") { - $duser->set_class($class); - $page->flash("Class changed"); - $this->redirect_to_user($duser); - } - } - - private function count_upload_ips(User $duser): array - { - global $database; - return $database->get_pairs(" - SELECT - owner_ip, - COUNT(images.id) AS count - FROM images - WHERE owner_id=:id - GROUP BY owner_ip - ORDER BY max(posted) DESC", ["id"=>$duser->id]); - } - - private function count_comment_ips(User $duser): array - { - global $database; - return $database->get_pairs(" - SELECT - owner_ip, - COUNT(comments.id) AS count - FROM comments - WHERE owner_id=:id - GROUP BY owner_ip - ORDER BY max(posted) DESC", ["id"=>$duser->id]); - } - - private function count_log_ips(User $duser): array - { - if (!class_exists('Shimmie2\LogDatabase')) { - return []; - } - global $database; - return $database->get_pairs(" - SELECT - address, - COUNT(id) AS count - FROM score_log - WHERE username=:username - GROUP BY address - ORDER BY MAX(date_sent) DESC", ["username"=>$duser->name]); - } - - private function delete_user(Page $page, bool $with_images=false, bool $with_comments=false): void - { - global $user, $config, $database; - - $page->set_title("Error"); - $page->set_heading("Error"); - $page->add_block(new NavBlock()); - - if (!$user->can(Permissions::DELETE_USER)) { - $page->add_block(new Block("Not Admin", "Only admins can delete accounts")); - } elseif (!isset($_POST['id']) || !is_numeric($_POST['id'])) { - $page->add_block(new Block( - "No ID Specified", - "You need to specify the account number to edit" - )); - } else { - $uid = int_escape($_POST['id']); - $duser = User::by_id($uid); - log_warning("user", "Deleting user #{$uid} (@{$duser->name})"); - - if ($with_images) { - log_warning("user", "Deleting user #{$_POST['id']} (@{$duser->name})'s uploads"); - $image_ids = $database->get_col("SELECT id FROM images WHERE owner_id = :owner_id", ["owner_id" => $_POST['id']]); - foreach ($image_ids as $image_id) { - $image = Image::by_id((int)$image_id); - if ($image) { - send_event(new ImageDeletionEvent($image)); - } - } - } else { - $database->execute( - "UPDATE images SET owner_id = :new_owner_id WHERE owner_id = :old_owner_id", - ["new_owner_id" => $config->get_int('anon_id'), "old_owner_id" => $_POST['id']] - ); - } - - if ($with_comments) { - log_warning("user", "Deleting user #{$_POST['id']} (@{$duser->name})'s comments"); - $database->execute("DELETE FROM comments WHERE owner_id = :owner_id", ["owner_id" => $_POST['id']]); - } else { - $database->execute( - "UPDATE comments SET owner_id = :new_owner_id WHERE owner_id = :old_owner_id", - ["new_owner_id" => $config->get_int('anon_id'), "old_owner_id" => $_POST['id']] - ); - } - - send_event(new UserDeletionEvent($uid)); - - $database->execute( - "DELETE FROM users WHERE id = :id", - ["id" => $_POST['id']] - ); - - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/list")); - } - } -} diff --git a/ext/user/test.php b/ext/user/test.php index 2aa53d8e..1aa90f2a 100644 --- a/ext/user/test.php +++ b/ext/user/test.php @@ -6,7 +6,7 @@ namespace Shimmie2; class UserPageTest extends ShimmiePHPUnitTestCase { - public function testUserPage(): void + public function testUserPage() { $this->get_page('user'); $this->assert_title("Not Logged In"); @@ -42,39 +42,15 @@ class UserPageTest extends ShimmiePHPUnitTestCase # FIXME: test user creation # FIXME: test adminifying # FIXME: test password reset - public function testUserList(): void + public function testUserList() { $this->get_page('user_admin/list'); $this->assert_text("demo"); } - public function testUserClasses(): void + public function testUserClasses() { $this->get_page('user_admin/classes'); $this->assert_text("admin"); } - - public function testCreateOther(): void - { - global $page; - - $this->assertException(UserCreationException::class, function () { - $this->log_out(); - $this->post_page('user_admin/create_other', [ - 'name' => 'testnew', - 'pass1' => 'testnew', - 'email' => '', - ]); - }); - $this->assertNull(User::by_name('testnew'), "Anon can't create others"); - - $this->log_in_as_admin(); - $this->post_page('user_admin/create_other', [ - 'name' => 'testnew', - 'pass1' => 'testnew', - 'email' => '', - ]); - $this->assertEquals(302, $page->code); - $this->assertNotNull(User::by_name('testnew'), "Admin can create others"); - } } diff --git a/ext/user/theme.php b/ext/user/theme.php index a3fc9fae..33d8a5fb 100644 --- a/ext/user/theme.php +++ b/ext/user/theme.php @@ -25,7 +25,7 @@ use function MicroHTML\OPTION; class UserPageTheme extends Themelet { - public function display_login_page(Page $page): void + public function display_login_page(Page $page) { $page->set_title("Login"); $page->set_heading("Login"); @@ -36,78 +36,68 @@ class UserPageTheme extends Themelet )); } - public function display_user_list(Page $page, HTMLElement $table, HTMLElement $paginator): void + public function display_user_list(Page $page, $table, $paginator) { $page->set_title("User List"); $page->set_heading("User List"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Users", emptyHTML($table, $paginator))); + $page->add_block(new Block("Users", $table . $paginator)); } - /** - * @param array $parts - */ - public function display_user_links(Page $page, User $user, array $parts): void + public function display_user_links(Page $page, User $user, $parts) { # $page->add_block(new Block("User Links", join(", ", $parts), "main", 10)); } - /** - * @param array $parts - */ - public function display_user_block(Page $page, User $user, array $parts): void + public function display_user_block(Page $page, User $user, $parts) { $html = emptyHTML('Logged in as ', $user->name); foreach ($parts as $part) { $html->appendChild(BR()); - $html->appendChild(A(["href" => $part["link"]], $part["name"])); + $html->appendChild(A(["href"=>$part["link"]], $part["name"])); } $b = new Block("User Links", $html, "left", 90); $b->is_content = false; $page->add_block($b); } - public function display_signup_page(Page $page): void + public function display_signup_page(Page $page) { - global $config, $user; + global $config; $tac = $config->get_string("login_tac", ""); if ($config->get_bool("login_tac_bbcode")) { - $tac = send_event(new TextFormattingEvent($tac))->formatted; + $tfe = send_event(new TextFormattingEvent($tac)); + $tac = $tfe->formatted; } - $email_required = ( - $config->get_bool("user_email_required") && - !$user->can(Permissions::CREATE_OTHER_USER) - ); - $form = SHM_SIMPLE_FORM( "user_admin/create", TABLE( - ["class" => "form"], + ["class"=>"form"], TBODY( TR( TH("Name"), - TD(INPUT(["type" => 'text', "name" => 'name', "required" => true])) + TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) ), TR( TH("Password"), - TD(INPUT(["type" => 'password', "name" => 'pass1', "required" => true])) + TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) ), TR( TH(rawHTML("Repeat Password")), - TD(INPUT(["type" => 'password', "name" => 'pass2', "required" => true])) + TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) ), TR( - TH($email_required ? "Email" : rawHTML("Email (Optional)")), - TD(INPUT(["type" => 'email', "name" => 'email', "required" => $email_required])) + TH(rawHTML("Email (Optional)")), + TD(INPUT(["type"=>'email', "name"=>'email'])) ), TR( - TD(["colspan" => "2"], rawHTML(captcha_get_html())) + TD(["colspan"=>"2"], rawHTML(captcha_get_html())) ), ), TFOOT( - TR(TD(["colspan" => "2"], INPUT(["type" => "submit", "value" => "Create Account"]))) + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) ) ) ); @@ -123,44 +113,41 @@ class UserPageTheme extends Themelet $page->add_block(new Block("Signup", $html)); } - public function display_user_creator(): void + public function display_user_creator() { global $page; $form = SHM_SIMPLE_FORM( "user_admin/create_other", TABLE( - ["class" => "form"], + ["class"=>"form"], TBODY( TR( TH("Name"), - TD(INPUT(["type" => 'text', "name" => 'name', "required" => true])) + TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) ), TR( TH("Password"), - TD(INPUT(["type" => 'password', "name" => 'pass1', "required" => true])) + TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) ), TR( TH(rawHTML("Repeat Password")), - TD(INPUT(["type" => 'password', "name" => 'pass2', "required" => true])) + TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) ), TR( - TH(rawHTML("Email")), - TD(INPUT(["type" => 'email', "name" => 'email'])) - ), - TR( - TD(["colspan" => 2], rawHTML("(Email is optional for admin-created accounts)")), + TH(rawHTML("Email (Optional)")), + TD(INPUT(["type"=>'email', "name"=>'email'])) ), ), TFOOT( - TR(TD(["colspan" => "2"], INPUT(["type" => "submit", "value" => "Create Account"]))) + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) ) ) ); $page->add_block(new Block("Create User", (string)$form, "main", 75)); } - public function display_signups_disabled(Page $page): void + public function display_signups_disabled(Page $page) { $page->set_title("Signups Disabled"); $page->set_heading("Signups Disabled"); @@ -171,30 +158,25 @@ class UserPageTheme extends Themelet )); } - public function display_login_block(Page $page): void - { - $page->add_block(new Block("Login", $this->create_login_block(), "left", 90)); - } - - public function create_login_block(): HTMLElement + public function display_login_block(Page $page) { global $config, $user; $form = SHM_SIMPLE_FORM( "user_admin/login", TABLE( - ["style" => "width: 100%", "class" => "form"], + ["style"=>"width: 100%", "class"=>"form"], TBODY( TR( - TH(LABEL(["for" => "user"], "Name")), - TD(INPUT(["id" => "user", "type" => "text", "name" => "user", "autocomplete" => "username"])) + TH(LABEL(["for"=>"user"], "Name")), + TD(INPUT(["id"=>"user", "type"=>"text", "name"=>"user", "autocomplete"=>"username"])) ), TR( - TH(LABEL(["for" => "pass"], "Password")), - TD(INPUT(["id" => "pass", "type" => "password", "name" => "pass", "autocomplete" => "current-password"])) + TH(LABEL(["for"=>"pass"], "Password")), + TD(INPUT(["id"=>"pass", "type"=>"password", "name"=>"pass", "autocomplete"=>"current-password"])) ) ), TFOOT( - TR(TD(["colspan" => "2"], INPUT(["type" => "submit", "value" => "Log In"]))) + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Log In"]))) ) ) ); @@ -202,15 +184,12 @@ class UserPageTheme extends Themelet $html = emptyHTML(); $html->appendChild($form); if ($config->get_bool("login_signup_enabled") && $user->can(Permissions::CREATE_USER)) { - $html->appendChild(SMALL(A(["href" => make_link("user_admin/create")], "Create Account"))); + $html->appendChild(SMALL(A(["href"=>make_link("user_admin/create")], "Create Account"))); } - return $html; + $page->add_block(new Block("Login", $html, "left", 90)); } - /** - * @param array $ips - */ private function _ip_list(string $name, array $ips): HTMLElement { $td = TD("$name: "); @@ -227,34 +206,27 @@ class UserPageTheme extends Themelet return $td; } - /** - * @param array $uploads - * @param array $comments - * @param array $events - */ - public function display_ip_list(Page $page, array $uploads, array $comments, array $events): void + public function display_ip_list(Page $page, array $uploads, array $comments, array $events) { $html = TABLE( - ["id" => "ip-history"], + ["id"=>"ip-history"], TR( $this->_ip_list("Uploaded from", $uploads), $this->_ip_list("Commented from", $comments), $this->_ip_list("Logged Events", $events) ), TR( - TD(["colspan" => "3"], "(Most recent at top)") + TD(["colspan"=>"3"], "(Most recent at top)") ) ); $page->add_block(new Block("IPs", $html, "main", 70)); } - /** - * @param string[] $stats - */ - public function display_user_page(User $duser, array $stats): void + public function display_user_page(User $duser, $stats) { global $page; + assert(is_array($stats)); $stats[] = 'User ID: '.$duser->id; $page->set_title(html_escape($duser->name)."'s Page"); @@ -278,7 +250,7 @@ class UserPageTheme extends Themelet "Change Name", TBODY(TR( TH("New name"), - TD(INPUT(["type" => 'text', "name" => 'name', "value" => $duser->name])) + TD(INPUT(["type"=>'text', "name"=>'name', "value"=>$duser->name])) )), "Set" )); @@ -291,11 +263,11 @@ class UserPageTheme extends Themelet TBODY( TR( TH("Password"), - TD(INPUT(["type" => 'password', "name" => 'pass1', "autocomplete" => 'new-password'])) + TD(INPUT(["type"=>'password', "name"=>'pass1', "autocomplete"=>'new-password'])) ), TR( TH("Repeat Password"), - TD(INPUT(["type" => 'password', "name" => 'pass2', "autocomplete" => 'new-password'])) + TD(INPUT(["type"=>'password', "name"=>'pass2', "autocomplete"=>'new-password'])) ), ), "Set" @@ -307,16 +279,17 @@ class UserPageTheme extends Themelet "Change Email", TBODY(TR( TH("Address"), - TD(INPUT(["type" => 'text', "name" => 'address', "value" => $duser->email, "autocomplete" => 'email', "inputmode" => 'email'])) + TD(INPUT(["type"=>'text', "name"=>'address', "value"=>$duser->email, "autocomplete"=>'email', "inputmode"=>'email'])) )), "Set" )); if ($user->can(Permissions::EDIT_USER_CLASS)) { - $select = SELECT(["name" => "class"]); - foreach (UserClass::$known_classes as $name => $values) { + global $_shm_user_classes; + $select = SELECT(["name"=>"class"]); + foreach ($_shm_user_classes as $name => $values) { $select->appendChild( - OPTION(["value" => $name, "selected" => $name == $duser->class->name], ucwords($name)) + OPTION(["value"=>$name, "selected"=>$name == $duser->class->name], ucwords($name)) ); } $html->appendChild(SHM_USER_FORM( @@ -334,12 +307,12 @@ class UserPageTheme extends Themelet "user_admin/delete_user", "Delete User", TBODY( - TR(TD(LABEL(INPUT(["type" => 'checkbox', "name" => 'with_images']), "Delete images"))), - TR(TD(LABEL(INPUT(["type" => 'checkbox', "name" => 'with_comments']), "Delete comments"))), + TR(TD(LABEL(INPUT(["type"=>'checkbox', "name"=>'with_images']), "Delete images"))), + TR(TD(LABEL(INPUT(["type"=>'checkbox', "name"=>'with_comments']), "Delete comments"))), ), TFOOT( - TR(TD(INPUT(["type" => 'button', "class" => 'shm-unlocker', "data-unlock-sel" => '.deluser', "value" => 'Unlock']))), - TR(TD(INPUT(["type" => 'submit', "class" => 'deluser', "value" => 'Delete User', "disabled" => 'true']))), + TR(TD(INPUT(["type"=>'button', "class"=>'shm-unlocker', "data-unlock-sel"=>'.deluser', "value"=>'Unlock']))), + TR(TD(INPUT(["type"=>'submit', "class"=>'deluser', "value"=>'Delete User', "disabled"=>'true']))), ) )); } @@ -380,7 +353,7 @@ class UserPageTheme extends Themelet */ public function display_user_classes(Page $page, array $classes, array $permissions): void { - $table = TABLE(["class" => "zebra"]); + $table = TABLE(["class"=>"zebra"]); $row = TR(); $row->appendChild(TH("Permission")); @@ -401,9 +374,9 @@ class UserPageTheme extends Themelet foreach ($classes as $class) { $opacity = array_key_exists($perm->getValue(), $class->abilities) ? 1 : 0.2; if ($class->can($perm->getValue())) { - $cell = TD(["style" => "color: green; opacity: $opacity;"], "✔"); + $cell = TD(["style"=>"color: green; opacity: $opacity;"], "✔"); } else { - $cell = TD(["style" => "color: red; opacity: $opacity;"], "✘"); + $cell = TD(["style"=>"color: red; opacity: $opacity;"], "✘"); } $row->appendChild($cell); } @@ -411,7 +384,7 @@ class UserPageTheme extends Themelet $doc = $perm->getDocComment(); if ($doc) { $doc = preg_replace('/\/\*\*|\n\s*\*\s*|\*\//', '', $doc); - $row->appendChild(TD(["style" => "text-align: left;"], $doc)); + $row->appendChild(TD(["style"=>"text-align: left;"], $doc)); } else { $row->appendChild(TD("")); } diff --git a/ext/user/theme.php.orig b/ext/user/theme.php.orig deleted file mode 100644 index 90b16fbd..00000000 --- a/ext/user/theme.php.orig +++ /dev/null @@ -1,429 +0,0 @@ -set_title("Login"); - $page->set_heading("Login"); - $page->add_block(new NavBlock()); - $page->add_block(new Block( - "Login There", - "There should be a login box to the left" - )); - } - - public function display_user_list(Page $page, $table, $paginator) - { - $page->set_title("User List"); - $page->set_heading("User List"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Users", $table . $paginator)); - } - - public function display_user_links(Page $page, User $user, $parts) - { - # $page->add_block(new Block("User Links", join(", ", $parts), "main", 10)); - } - - public function display_user_block(Page $page, User $user, $parts) - { - $html = emptyHTML('Logged in as ', $user->name); - foreach ($parts as $part) { - $html->appendChild(BR()); - $html->appendChild(A(["href"=>$part["link"]], $part["name"])); - } - $b = new Block("User Links", $html, "left", 90); - $b->is_content = false; - $page->add_block($b); - } - - public function display_signup_page(Page $page) - { - global $config; - $tac = $config->get_string("login_tac", ""); - - if ($config->get_bool("login_tac_bbcode")) { - $tfe = send_event(new TextFormattingEvent($tac)); - $tac = $tfe->formatted; - } - - $form = SHM_SIMPLE_FORM( - "user_admin/create", - TABLE( - ["class"=>"form"], - TBODY( - TR( - TH("Name"), - TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) - ), - TR( - TH("Password"), - TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) - ), - TR( - TH(rawHTML("Repeat Password")), - TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) - ), - TR( - TH(rawHTML("Email (Optional)")), - TD(INPUT(["type"=>'email', "name"=>'email'])) - ), - TR( - TD(["colspan"=>"2"], rawHTML(captcha_get_html())) - ), - ), - TFOOT( - TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) - ) - ) - ); - - $html = emptyHTML( - $tac ? P(rawHTML($tac)) : null, - $form - ); - - $page->set_title("Create Account"); - $page->set_heading("Create Account"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Signup", $html)); - } - - public function display_user_creator() - { - global $page; - - $form = SHM_SIMPLE_FORM( - "user_admin/create_other", - TABLE( - ["class"=>"form"], - TBODY( - TR( - TH("Name"), - TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) - ), - TR( - TH("Password"), - TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) - ), - TR( - TH(rawHTML("Repeat Password")), - TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) - ), - TR( - TH(rawHTML("Email (Optional)")), - TD(INPUT(["type"=>'email', "name"=>'email'])) - ), - ), - TFOOT( - TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) - ) - ) - ); - $page->add_block(new Block("Create User", (string)$form, "main", 75)); - } - - public function display_signups_disabled(Page $page) - { - $page->set_title("Signups Disabled"); - $page->set_heading("Signups Disabled"); - $page->add_block(new NavBlock()); - $page->add_block(new Block( - "Signups Disabled", - "The board admin has disabled the ability to create new accounts~" - )); - } - - public function display_login_block(Page $page) - { - global $config, $user; - $form = SHM_SIMPLE_FORM( - "user_admin/login", - TABLE( - ["style"=>"width: 100%", "class"=>"form"], - TBODY( - TR( - TH(LABEL(["for"=>"user"], "Name")), - TD(INPUT(["id"=>"user", "type"=>"text", "name"=>"user", "autocomplete"=>"username"])) - ), - TR( - TH(LABEL(["for"=>"pass"], "Password")), - TD(INPUT(["id"=>"pass", "type"=>"password", "name"=>"pass", "autocomplete"=>"current-password"])) - ) - ), - TFOOT( - TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Log In"]))) - ) - ) - ); - - $html = emptyHTML(); - $html->appendChild($form); - if ($config->get_bool("login_signup_enabled") && $user->can(Permissions::CREATE_USER)) { - $html->appendChild(SMALL(A(["href"=>make_link("user_admin/create")], "Create Account"))); - } - - $page->add_block(new Block("Login", $html, "left", 90)); - } - - private function _ip_list(string $name, array $ips): HTMLElement - { - $td = TD("$name: "); - $n = 0; - foreach ($ips as $ip => $count) { - $td->appendChild(BR()); - $td->appendChild("$ip ($count)"); - if (++$n >= 20) { - $td->appendChild(BR()); - $td->appendChild("..."); - break; - } - } - return $td; - } - - public function display_ip_list(Page $page, array $uploads, array $comments, array $events) - { - $html = TABLE( - ["id"=>"ip-history"], - TR( - $this->_ip_list("Uploaded from", $uploads), - $this->_ip_list("Commented from", $comments), - $this->_ip_list("Logged Events", $events) - ), - TR( - TD(["colspan"=>"3"], "(Most recent at top)") - ) - ); - - $page->add_block(new Block("IPs", $html, "main", 70)); - } - - public function display_user_page(User $duser, $stats) - { - global $page; - assert(is_array($stats)); - $stats[] = 'User ID: '.$duser->id; - - $page->set_title(html_escape($duser->name)."'s Page"); - $page->set_heading(html_escape($duser->name)."'s Page"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Stats", join("
    ", $stats), "main", 10)); - } - - - public function build_operations(User $duser, UserOperationsBuildingEvent $event): string - { - global $config, $user; - $html = emptyHTML(); - - // just a fool-admin protection so they dont mess around with anon users. - if ($duser->id != $config->get_int('anon_id')) { - if ($user->can(Permissions::EDIT_USER_NAME)) { - $html->appendChild(SHM_USER_FORM( - $duser, - "user_admin/change_name", - "Change Name", - TBODY(TR( - TH("New name"), - TD(INPUT(["type"=>'text', "name"=>'name', "value"=>$duser->name])) - )), - "Set" - )); - } - - $html->appendChild(SHM_USER_FORM( - $duser, - "user_admin/change_pass", - "Change Password", - TBODY( - TR( - TH("Password"), - TD(INPUT(["type"=>'password', "name"=>'pass1', "autocomplete"=>'new-password'])) - ), - TR( - TH("Repeat Password"), - TD(INPUT(["type"=>'password', "name"=>'pass2', "autocomplete"=>'new-password'])) - ), - ), - "Set" - )); - - $html->appendChild(SHM_USER_FORM( - $duser, - "user_admin/change_email", - "Change Email", - TBODY(TR( - TH("Address"), - TD(INPUT(["type"=>'text', "name"=>'address', "value"=>$duser->email, "autocomplete"=>'email', "inputmode"=>'email'])) - )), - "Set" - )); - - if ($user->can(Permissions::EDIT_USER_CLASS)) { - global $_shm_user_classes; - $select = SELECT(["name"=>"class"]); - foreach ($_shm_user_classes as $name => $values) { - $select->appendChild( - OPTION(["value"=>$name, "selected"=>$name == $duser->class->name], ucwords($name)) - ); - } - $html->appendChild(SHM_USER_FORM( - $duser, - "user_admin/change_class", - "Change Class", - TBODY(TR(TD($select))), - "Set" - )); - } - - if ($user->can(Permissions::DELETE_USER)) { - $html->appendChild(SHM_USER_FORM( - $duser, - "user_admin/delete_user", - "Delete User", - TBODY( - TR(TD(LABEL(INPUT(["type"=>'checkbox', "name"=>'with_images']), "Delete images"))), - TR(TD(LABEL(INPUT(["type"=>'checkbox', "name"=>'with_comments']), "Delete comments"))), - ), - TFOOT( - TR(TD(INPUT(["type"=>'button', "class"=>'shm-unlocker', "data-unlock-sel"=>'.deluser', "value"=>'Unlock']))), - TR(TD(INPUT(["type"=>'submit', "class"=>'deluser', "value"=>'Delete User', "disabled"=>'true']))), - ) - )); - } - - foreach ($event->parts as $part) { - $html .= $part; - } - } - return (string)$html; - } - - public function get_help_html(): HTMLElement - { - global $user; - $output = emptyHTML(P("Search for posts posted by particular individuals.")); - $output->appendChild(SHM_COMMAND_EXAMPLE( - "poster=username", - 'Returns posts posted by "username".' - )); - $output->appendChild(SHM_COMMAND_EXAMPLE( - "poster_id=123", - 'Returns posts posted by user 123.' - )); - - if ($user->can(Permissions::VIEW_IP)) { - $output->appendChild(SHM_COMMAND_EXAMPLE( - "poster_ip=127.0.0.1", - "Returns posts posted from IP 127.0.0.1." - )); - } - return $output; - } - -<<<<<<< HEAD -<<<<<<< HEAD - /** - * @param Page $page - * @param UserClass[] $classes - * @param \ReflectionClassConstant[] $permissions - */ - public function display_user_classes(Page $page, array $classes, array $permissions): void - { - $table = TABLE(["class"=>"zebra"]); - - $row = TR(); - $row->appendChild(TH("Permission")); - foreach ($classes as $class) { - $n = $class->name; - if ($class->parent) { - $n .= " ({$class->parent->name})"; - } - $row->appendChild(TH($n)); - } - $row->appendChild(TH("Description")); - $table->appendChild($row); - - foreach ($permissions as $perm) { - $row = TR(); - $row->appendChild(TH($perm->getName())); - - foreach ($classes as $class) { - $opacity = array_key_exists($perm->getValue(), $class->abilities) ? 1 : 0.2; - if ($class->can($perm->getValue())) { - $cell = TD(["style"=>"color: green; opacity: $opacity;"], "✔"); - } else { - $cell = TD(["style"=>"color: red; opacity: $opacity;"], "✘"); - } - $row->appendChild($cell); - } - - $doc = $perm->getDocComment(); - if ($doc) { - $doc = preg_replace('/\/\*\*|\n\s*\*\s*|\*\//', '', $doc); - $row->appendChild(TD(["style"=>"text-align: left;"], $doc)); - } else { - $row->appendChild(TD("")); - } - -======= - public function display_user_classes(Page $page, array $classes, array $permissions): void { -======= - public function display_user_classes(Page $page, array $classes, array $permissions): void - { ->>>>>>> ec854036 (format) - $table = TABLE(); - - $row = TR(); - $row->appendChild(TH("")); - foreach ($classes as $class) { - $row->appendChild(TH($class->name)); - } - $table->appendChild($row); - - foreach ($permissions as $k => $perm) { - $row = TR(); - $row->appendChild(TH($perm)); - foreach ($classes as $class) { - if($class->can($perm)) { - $cell = TD(["style"=>"color: green;"], "✔"); - } else { - $cell = TD(["style"=>"color: red;"], "✘"); - } - $row->appendChild($cell); - } ->>>>>>> e51c6bed (show a table of user classes, see #921) - $table->appendChild($row); - } - - $page->set_title("User Classes"); - $page->set_heading("User Classes"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Classes", $table, "main", 10)); - } -} diff --git a/ext/user_config/info.php b/ext/user_config/info.php index d546352a..a6495dbd 100644 --- a/ext/user_config/info.php +++ b/ext/user_config/info.php @@ -10,7 +10,7 @@ class UserConfigInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "User-specific settings"; - 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 system-wide support for user-specific settings"; public ExtensionVisibility $visibility = ExtensionVisibility::HIDDEN; diff --git a/ext/user_config/main.php b/ext/user_config/main.php index 579586c4..cf53dbb0 100644 --- a/ext/user_config/main.php +++ b/ext/user_config/main.php @@ -48,13 +48,13 @@ class UserConfig extends Extension public const ENABLE_API_KEYS = "ext_user_config_enable_api_keys"; public const API_KEY = "api_key"; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_bool(self::ENABLE_API_KEYS, false); } - public function onUserLogin(UserLoginEvent $event): void + public function onUserLogin(UserLoginEvent $event) { global $user_config; @@ -90,15 +90,15 @@ class UserConfig extends Extension } } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { global $user; - if ($event->parent === "user" && !$user->is_anonymous()) { + if ($event->parent==="user" && !$user->is_anonymous()) { $event->add_nav_link("user_config", new Link('user_config'), "User Options", false, 40); } } - public function onUserBlockBuilding(UserBlockBuildingEvent $event): void + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { global $user; if (!$user->is_anonymous()) { @@ -106,7 +106,7 @@ class UserConfig extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $user, $database, $config, $page, $user_config; @@ -154,7 +154,7 @@ class UserConfig extends Extension ]); $duser = User::by_id($input['id']); - if ($user->id != $duser->id && !$user->can(Permissions::CHANGE_OTHER_USER_SETTING)) { + if ($user->id!=$duser->id && !$user->can(Permissions::CHANGE_OTHER_USER_SETTING)) { $this->theme->display_permission_denied(); return; } @@ -170,7 +170,7 @@ class UserConfig extends Extension } } - public function onUserOperationsBuilding(UserOperationsBuildingEvent $event): void + public function onUserOperationsBuilding(UserOperationsBuildingEvent $event) { global $config; diff --git a/ext/user_config/style.css b/ext/user_config/style.css new file mode 100644 index 00000000..bc460fa4 --- /dev/null +++ b/ext/user_config/style.css @@ -0,0 +1,43 @@ +.setupblocks { + column-width: 400px; + -moz-column-width: 400px; + -webkit-column-width: 400px; + max-width: 1200px; + margin: auto; +} +.setupblocks > .setupblock:first-of-type { margin-top: 0; } + +.setupblock { + break-inside: avoid; + -moz-break-inside: avoid; + -webkit-break-inside: avoid; + column-break-inside: avoid; + -moz-column-break-inside: avoid; + -webkit-column-break-inside: avoid; + text-align: center; + width: 90%; +} +.setupblock TEXTAREA { + width: 100%; + font-size: 0.75em; + resize: vertical; +} + +.helpable { + border-bottom: 1px dashed gray; +} + +.ok { + background: #AFA; +} +.bad { + background: #FAA; +} + +#Setupmain .blockbody { + background: none; + border: none; + box-shadow: none; + margin: 0; + padding: 0; +} diff --git a/ext/user_config/test.php b/ext/user_config/test.php index 29d984e3..5e12abbc 100644 --- a/ext/user_config/test.php +++ b/ext/user_config/test.php @@ -8,7 +8,7 @@ class UserConfigTest extends ShimmiePHPUnitTestCase { private const OPTIONS_BLOCK_TITLE = "User Options"; - public function testUserConfigPage(): void + public function testUserConfigPage() { $this->get_page('user_config'); $this->assert_title("Permission Denied"); diff --git a/ext/user_config/theme.php b/ext/user_config/theme.php index 6e8593c3..7d5209da 100644 --- a/ext/user_config/theme.php +++ b/ext/user_config/theme.php @@ -39,7 +39,7 @@ class UserConfigTheme extends Themelet * * The page should wrap all the options in a form which links to setup_save */ - public function display_user_config_page(Page $page, User $user, SetupPanel $panel): void + public function display_user_config_page(Page $page, User $user, SetupPanel $panel) { usort($panel->blocks, "Shimmie2\blockcmp"); diff --git a/ext/varnish/main.php b/ext/varnish/main.php index 1d201c5c..fff2a8c3 100644 --- a/ext/varnish/main.php +++ b/ext/varnish/main.php @@ -6,7 +6,7 @@ namespace Shimmie2; class VarnishPurger extends Extension { - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string('varnish_host', '127.0.0.1'); @@ -14,7 +14,7 @@ class VarnishPurger extends Extension $config->set_default_string('varnish_protocol', 'http'); } - private function curl_purge(string $path): void + private function curl_purge($path) { // waiting for curl timeout adds ~5 minutes to unit tests if (defined("UNITTEST")) { @@ -39,17 +39,17 @@ class VarnishPurger extends Extension curl_close($ch); } - public function onCommentPosting(CommentPostingEvent $event): void + public function onCommentPosting(CommentPostingEvent $event) { $this->curl_purge("post/view/{$event->image_id}"); } - public function onImageInfoSet(ImageInfoSetEvent $event): void + public function onImageInfoSet(ImageInfoSetEvent $event) { $this->curl_purge("post/view/{$event->image->id}"); } - public function onImageDeletion(ImageDeletionEvent $event): void + public function onImageDeletion(ImageDeletionEvent $event) { $this->curl_purge("post/view/{$event->image->id}"); } diff --git a/ext/view/events/image_admin_block_building_event.php b/ext/view/events/image_admin_block_building_event.php index 6a79c8d2..47cb6a5c 100644 --- a/ext/view/events/image_admin_block_building_event.php +++ b/ext/view/events/image_admin_block_building_event.php @@ -4,11 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - class ImageAdminBlockBuildingEvent extends Event { - /** @var HTMLElement[] */ + /** @var string[] */ public array $parts = []; public Image $image; public User $user; @@ -22,7 +20,7 @@ class ImageAdminBlockBuildingEvent extends Event $this->context = $context; } - public function add_part(HTMLElement $html, int $position = 50): void + public function add_part(string $html, int $position=50) { while (isset($this->parts[$position])) { $position++; diff --git a/ext/view/events/image_info_box_building_event.php b/ext/view/events/image_info_box_building_event.php index de2585da..57d2affd 100644 --- a/ext/view/events/image_info_box_building_event.php +++ b/ext/view/events/image_info_box_building_event.php @@ -4,11 +4,8 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - class ImageInfoBoxBuildingEvent extends Event { - /** @var HTMLElement[] */ public array $parts = []; public Image $image; public User $user; @@ -20,7 +17,7 @@ class ImageInfoBoxBuildingEvent extends Event $this->user = $user; } - public function add_part(HTMLElement $html, int $position = 50): void + public function add_part(string $html, int $position=50) { while (isset($this->parts[$position])) { $position++; diff --git a/ext/view/info.php b/ext/view/info.php index e6cbd218..27602aa0 100644 --- a/ext/view/info.php +++ b/ext/view/info.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Shimmie2; -class ViewPostInfo extends ExtensionInfo +class ViewImageInfo extends ExtensionInfo { public const KEY = "view"; diff --git a/ext/view/main.php b/ext/view/main.php index eab1db3b..73ae1394 100644 --- a/ext/view/main.php +++ b/ext/view/main.php @@ -13,20 +13,20 @@ use function MicroHTML\TR; use function MicroHTML\TH; use function MicroHTML\TD; -class ViewPost extends Extension +class ViewImage extends Extension { - /** @var ViewPostTheme */ + /** @var ViewImageTheme */ protected Themelet $theme; - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; - if ($event->page_matches("post/prev") || $event->page_matches("post/next")) { + if ($event->page_matches("post/prev") || $event->page_matches("post/next")) { $image_id = int_escape($event->get_arg(0)); 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 = []; @@ -94,7 +94,7 @@ class ViewPost extends Extension } } - public function onRobotsBuilding(RobotsBuildingEvent $event): void + public function onRobotsBuilding(RobotsBuildingEvent $event) { // next and prev are just CPU-heavier ways of getting // to the same images that the index shows @@ -102,7 +102,7 @@ class ViewPost extends Extension $event->add_disallow("post/prev"); } - public function onDisplayingImage(DisplayingImageEvent $event): void + public function onDisplayingImage(DisplayingImageEvent $event) { global $page, $user; $image = $event->get_image(); @@ -118,12 +118,16 @@ class ViewPost extends Extension $this->theme->display_admin_block($page, $iabbe->parts); } - public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event): void + public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) { global $config; $image_info = $config->get_string(ImageConfig::INFO); if ($image_info) { - $event->add_part(SHM_POST_INFO("Info", $event->image->get_info()), 85); + $html = (string)TR( + TH("Info"), + TD($event->image->get_info()) + ); + $event->add_part($html, 85); } } } diff --git a/ext/view/script.js b/ext/view/script.js index 0123b3eb..3c8799f3 100644 --- a/ext/view/script.js +++ b/ext/view/script.js @@ -6,55 +6,24 @@ function joinUrlSegments(base, query) { return base + separatorChar + query; } -/** - * @param {HTMLElement} el - */ -function autosize(el) { - setTimeout(function() { - if(el.offsetHeight < el.scrollHeight) { - el.style.height = `calc(${el.scrollHeight}px + 0.5em)`; - el.style.width = el.offsetWidth + 'px'; - } - }, 0); -} - -function clearViewMode() { - document.querySelectorAll('.image_info').forEach((element) => { - element.classList.remove('infomode-view'); - }); - document.querySelectorAll('.image_info textarea').forEach((el) => { - autosize(el); - }); -} - -function updateAttr(selector, attr, value) { - document.querySelectorAll(selector).forEach(function(e) { - let current = e.getAttribute(attr); - let newval = joinUrlSegments(current, value); - e.setAttribute(attr, newval); - }); -} - document.addEventListener('DOMContentLoaded', () => { - // find elements with class image_info and set them to view mode - // (by default, with no js, they are in edit mode - so that no-js - // users can still edit them) - document.querySelectorAll('.image_info').forEach((element) => { - element.classList.add('infomode-view'); - }); - - document.querySelectorAll('.image_info textarea').forEach((el) => { - el.addEventListener('keydown', () => autosize(el)); - autosize(el); - }); - if(document.location.hash.length > 3) { var query = document.location.hash.substring(1); - updateAttr("LINK#prevlink", "href", query); - updateAttr("LINK#nextlink", "href", query); - updateAttr("A#prevlink", "href", query); - updateAttr("A#nextlink", "href", query); - updateAttr("form#image_delete_form", "action", query); + $('LINK#prevlink').attr('href', function(i, attr) { + return joinUrlSegments(attr,query); + }); + $('LINK#nextlink').attr('href', function(i, attr) { + return joinUrlSegments(attr,query); + }); + $('A#prevlink').attr('href', function(i, attr) { + return joinUrlSegments(attr,query); + }); + $('A#nextlink').attr('href', function(i, attr) { + return joinUrlSegments(attr,query); + }); + $('span#image_delete_form form').attr('action', function(i, attr) { + return joinUrlSegments(attr,query); + }); } }); diff --git a/ext/view/style.css b/ext/view/style.css index 9ae4043b..d7d7c2c0 100644 --- a/ext/view/style.css +++ b/ext/view/style.css @@ -1,30 +1,15 @@ -TABLE.form.image_info { - width: 550px; - max-width: 100%; -} -.image_info .edit { - display: block; -} -.image_info .view { +.js .image_info .edit { display: none; } - -.image_info.infomode-view .edit { - display: none; -} -.image_info.infomode-view .view { +.js .image_info .view { display: block; } -.image_info TEXTAREA { - min-width: 100%; - min-height: 3rem; +.no-js .image_info .edit { + display: block; +} +.no-js .image_info .view { + display: none; } -.post_controls FORM { - margin-bottom: 0.75em; -} -.post_controls FORM:last-child { - margin-bottom: 0; -} \ No newline at end of file diff --git a/ext/view/test.php b/ext/view/test.php index cbb1e3c0..8faeab77 100644 --- a/ext/view/test.php +++ b/ext/view/test.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Shimmie2; -class ViewPostTest extends ShimmiePHPUnitTestCase +class ViewImageTest extends ShimmiePHPUnitTestCase { public function setUp(): void { @@ -12,7 +12,7 @@ class ViewPostTest extends ShimmiePHPUnitTestCase // FIXME: upload images } - public function testViewPage(): void + public function testViewPage() { $this->log_in_as_user(); $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); @@ -21,7 +21,7 @@ class ViewPostTest extends ShimmiePHPUnitTestCase $this->assert_title("Post $image_id_1: test"); } - public function testViewInfo(): void + public function testViewInfo() { global $config; @@ -30,10 +30,10 @@ class ViewPostTest extends ShimmiePHPUnitTestCase $config->set_string(ImageConfig::INFO, '$size // $filesize // $ext'); $this->get_page("post/view/$image_id_1"); - $this->assert_text("640x480 // 19KB // jpg"); + $this->assert_text("640x480 // 19.3KB // jpg"); } - public function testPrevNext(): void + public function testPrevNext() { $this->log_in_as_user(); $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "test"); @@ -47,10 +47,10 @@ class ViewPostTest extends ShimmiePHPUnitTestCase $this->assertEquals("/test/post/view/$image_id_2", $page->redirect); // When searching, we skip the middle - $page = $this->get_page("post/prev/$image_id_1", ["search" => "test"]); + $page = $this->get_page("post/prev/$image_id_1", ["search"=>"test"]); $this->assertEquals("/test/post/view/$image_id_3?#search=test", $page->redirect); - $page = $this->get_page("post/next/$image_id_3", ["search" => "test"]); + $page = $this->get_page("post/next/$image_id_3", ["search"=>"test"]); $this->assertEquals("/test/post/view/$image_id_1?#search=test", $page->redirect); // Middle image: has next and prev @@ -66,25 +66,7 @@ class ViewPostTest extends ShimmiePHPUnitTestCase $this->assertEquals(404, $page->code); } - public function testPrevNextDisabledWhenOrdered(): void - { - $this->log_in_as_user(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); - - $this->get_page("post/view/$image_id"); - $this->assert_text("Prev"); - - $this->get_page("post/view/$image_id", ["search" => "test"]); - $this->assert_text("Prev"); - - $this->get_page("post/view/$image_id", ["search" => "cake_order:_the_cakening"]); - $this->assert_text("Prev"); - - $this->get_page("post/view/$image_id", ["search" => "order:score"]); - $this->assert_no_text("Prev"); - } - - public function testView404(): void + public function testView404() { $this->log_in_as_user(); $image_id_1 = $this->post_image("tests/favicon.png", "test"); diff --git a/ext/view/theme.php b/ext/view/theme.php index 0f25ad67..f41b17d6 100644 --- a/ext/view/theme.php +++ b/ext/view/theme.php @@ -4,13 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -use function MicroHTML\{A, joinHTML, TABLE, TR, TD, INPUT, emptyHTML, DIV, BR}; - -class ViewPostTheme extends Themelet +class ViewImageTheme extends Themelet { - public function display_meta_headers(Image $image): void + public function display_meta_headers(Image $image) { global $page; @@ -22,12 +18,10 @@ class ViewPostTheme extends Themelet $page->add_html_header("id}"))."\">"); } - /** + /* * Build a page showing $image and some info about it - * - * @param HTMLElement[] $editor_parts */ - public function display_page(Image $image, array $editor_parts): void + public function display_page(Image $image, $editor_parts) { global $page; $page->set_title("Post {$image->id}: ".$image->get_tag_list()); @@ -37,61 +31,35 @@ class ViewPostTheme extends Themelet //$page->add_block(new Block(null, $this->build_pin($image), "main", 11)); $query = $this->get_query(); - if(!$this->is_ordered_search()) { - $page->add_html_header(""); - $page->add_html_header(""); - } + $page->add_html_header(""); + $page->add_html_header(""); } - /** - * @param HTMLElement[] $parts - */ - public function display_admin_block(Page $page, array $parts): void + public function display_admin_block(Page $page, $parts) { if (count($parts) > 0) { - $page->add_block(new Block("Post Controls", DIV(["class" => "post_controls"], joinHTML("", $parts)), "left", 50)); + $page->add_block(new Block("Post Controls", join("
    ", $parts), "left", 50)); } } protected function get_query(): ?string { if (isset($_GET['search'])) { - $query = "search=".url_escape($_GET['search']); + $query = "search=".url_escape(Tag::caret($_GET['search'])); } else { $query = null; } return $query; } - /** - * prev/next only work for default-ordering searches - if the user - * has specified a custom order, we can't show prev/next. - */ - protected function is_ordered_search(): bool - { - if(isset($_GET['search'])) { - $tags = Tag::explode($_GET['search']); - foreach($tags as $tag) { - if(preg_match("/^order[=:]/", $tag) == 1) { - return true; - } - } - } - return false; - } - - protected function build_pin(Image $image): HTMLElement + protected function build_pin(Image $image): string { $query = $this->get_query(); - if($this->is_ordered_search()) { - return A(["href" => make_link()], "Index"); - } else { - return joinHTML(" | ", [ - A(["href" => make_link("post/prev/{$image->id}", $query), "id" => "prevlink"], "Prev"), - A(["href" => make_link()], "Index"), - A(["href" => make_link("post/next/{$image->id}", $query), "id" => "nextlink"], "Next"), - ]); - } + $h_prev = "Prev"; + $h_index = "Index"; + $h_next = "Next"; + + return "$h_prev | $h_index | $h_next"; } protected function build_navigation(Image $image): string @@ -99,8 +67,8 @@ class ViewPostTheme extends Themelet $h_pin = $this->build_pin($image); $h_search = "

    - - + + "; @@ -108,37 +76,36 @@ class ViewPostTheme extends Themelet return "$h_pin
    $h_search"; } - /** - * @param HTMLElement[] $editor_parts - */ - protected function build_info(Image $image, array $editor_parts): HTMLElement + protected function build_info(Image $image, $editor_parts): string { global $user; if (count($editor_parts) == 0) { - return emptyHTML($image->is_locked() ? "[Post Locked]" : ""); + return ($image->is_locked() ? "
    [Post Locked]" : ""); } - if( + $html = make_form(make_link("post/set"))." + +

    + "; + foreach ($editor_parts as $part) { + $html .= $part; + } + if ( (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)) && $user->can(Permissions::EDIT_IMAGE_TAG) ) { - $editor_parts[] = TR(TD( - ["colspan" => 4], - INPUT(["class" => "view", "type" => "button", "value" => "Edit", "onclick" => "clearViewMode()"]), - INPUT(["class" => "edit", "type" => "submit", "value" => "Set"]) - )); + $html .= " + + "; } - - return SHM_SIMPLE_FORM( - "post/set", - INPUT(["type" => "hidden", "name" => "image_id", "value" => $image->id]), - TABLE( - [ - "class" => "image_info form", - ], - ...$editor_parts, - ), - ); + $html .= " +
    + + +
    + + "; + return $html; } } diff --git a/ext/wiki/info.php b/ext/wiki/info.php index 9c28808a..8c2583a1 100644 --- a/ext/wiki/info.php +++ b/ext/wiki/info.php @@ -11,7 +11,7 @@ class WikiInfo extends ExtensionInfo public string $key = self::KEY; public string $name = "Simple Wiki"; public string $url = self::SHIMMIE_URL; - public array $authors = [self::SHISH_NAME => self::SHISH_EMAIL, "Luana Latte" => "luana.latte.cat@gmail.com"]; + public array $authors = [self::SHISH_NAME=>self::SHISH_EMAIL, "Luana Latte"=>"luana.latte.cat@gmail.com"]; public string $license = self::LICENSE_GPLV2; public string $description = "A simple wiki, for those who don't want the hugeness of mediawiki"; public ?string $documentation = "Standard formatting APIs are used (This will be BBCode by default)"; diff --git a/ext/wiki/main.php b/ext/wiki/main.php index bb1cb56f..c115512a 100644 --- a/ext/wiki/main.php +++ b/ext/wiki/main.php @@ -67,10 +67,7 @@ class WikiPage #[Field] public string $body; - /** - * @param array $row - */ - public function __construct(array $row = null) + public function __construct(array $row=null) { //assert(!empty($row)); global $database; @@ -83,7 +80,7 @@ class WikiPage $this->title = $row['title']; $this->revision = (int)$row['revision']; $this->locked = bool_escape($row['locked']); - $this->exists = $database->exists("SELECT id FROM wiki_pages WHERE title = :title", ["title" => $this->title]); + $this->exists = $database->exists("SELECT id FROM wiki_pages WHERE title = :title", ["title"=>$this->title]); $this->body = $row['body']; } } @@ -118,7 +115,7 @@ class Wiki extends Extension /** @var WikiTheme */ protected Themelet $theme; - public function onInitExt(InitExtEvent $event): void + public function onInitExt(InitExtEvent $event) { global $config; $config->set_default_string( @@ -134,16 +131,16 @@ class Wiki extends Extension } // Add a block to the Board Config / Setup - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Wiki"); $sb->add_bool_option(WikiConfig::ENABLE_REVISIONS, "Enable wiki revisions: "); - $sb->add_longtext_option(WikiConfig::TAG_PAGE_TEMPLATE, "
    Tag page template: "); - $sb->add_text_option(WikiConfig::EMPTY_TAGINFO, "
    Empty list text: "); - $sb->add_bool_option(WikiConfig::TAG_SHORTWIKIS, "
    Show shortwiki entry when searching for a single tag: "); + $sb->add_longtext_option(WikiConfig::TAG_PAGE_TEMPLATE, "Tag page template: "); + $sb->add_text_option(WikiConfig::EMPTY_TAGINFO, "Empty list text: "); + $sb->add_bool_option(WikiConfig::TAG_SHORTWIKIS, "Show shortwiki entry when searching for a single tag: "); } - public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event) { global $database; @@ -173,7 +170,7 @@ class Wiki extends Extension } } - public function onPageRequest(PageRequestEvent $event): void + public function onPageRequest(PageRequestEvent $event) { global $page, $user; if ($event->page_matches("wiki")) { @@ -204,10 +201,21 @@ class Wiki extends Extension $wikipage->revision = $rev; $wikipage->body = $body; $wikipage->locked = $lock; - send_event(new WikiUpdateEvent($user, $wikipage)); - $u_title = url_escape($title); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("wiki/$u_title")); + try { + send_event(new WikiUpdateEvent($user, $wikipage)); + + $u_title = url_escape($title); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("wiki/$u_title")); + } catch (WikiUpdateException $e) { + $original = $this->get_page($title); + // @ because arr_diff is full of warnings + $original->body = @$this->arr_diff( + explode("\n", $original->body), + explode("\n", $wikipage->body) + ); + $this->theme->display_page_editor($page, $original); + } } else { $this->theme->display_permission_denied(); } @@ -232,43 +240,43 @@ class Wiki extends Extension } - public function onPageNavBuilding(PageNavBuildingEvent $event): void + public function onPageNavBuilding(PageNavBuildingEvent $event) { $event->add_nav_link("wiki", new Link('wiki'), "Wiki"); } - public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event) { - if ($event->parent == "wiki") { + if ($event->parent=="wiki") { $event->add_nav_link("wiki_rules", new Link('wiki/rules'), "Rules"); $event->add_nav_link("wiki_help", new Link('ext_doc/wiki'), "Help"); } } - public function onWikiUpdate(WikiUpdateEvent $event): void + public function onWikiUpdate(WikiUpdateEvent $event) { global $database, $config; $wpage = $event->wikipage; - $exists = $database->exists("SELECT id FROM wiki_pages WHERE title = :title", ["title" => $wpage->title]); + $exists = $database->exists("SELECT id FROM wiki_pages WHERE title = :title", ["title"=>$wpage->title]); try { - if ($config->get_bool(WikiConfig::ENABLE_REVISIONS) || !$exists) { + if ($config->get_bool(WikiConfig::ENABLE_REVISIONS) || ! $exists) { $database->execute( " INSERT INTO wiki_pages(owner_id, owner_ip, date, title, revision, locked, body) VALUES (:owner_id, :owner_ip, now(), :title, :revision, :locked, :body)", - ["owner_id" => $event->user->id, "owner_ip" => get_real_ip(), - "title" => $wpage->title, "revision" => $wpage->revision, "locked" => $wpage->locked, "body" => $wpage->body] + ["owner_id"=>$event->user->id, "owner_ip"=>get_real_ip(), + "title"=>$wpage->title, "revision"=>$wpage->revision, "locked"=>$wpage->locked, "body"=>$wpage->body] ); } else { $database->execute( " UPDATE wiki_pages SET owner_id=:owner_id, owner_ip=:owner_ip, date=now(), locked=:locked, body=:body WHERE title = :title ORDER BY revision DESC LIMIT 1", - ["owner_id" => $event->user->id, "owner_ip" => get_real_ip(), - "title" => $wpage->title, "locked" => $wpage->locked, "body" => $wpage->body] + ["owner_id"=>$event->user->id, "owner_ip"=>get_real_ip(), + "title"=>$wpage->title, "locked"=>$wpage->locked, "body"=>$wpage->body] ); } } catch (\Exception $e) { @@ -276,16 +284,16 @@ class Wiki extends Extension } } - public function onWikiDeleteRevision(WikiDeleteRevisionEvent $event): void + public function onWikiDeleteRevision(WikiDeleteRevisionEvent $event) { global $database; $database->execute( "DELETE FROM wiki_pages WHERE title=:title AND revision=:rev", - ["title" => $event->title, "rev" => $event->revision] + ["title"=>$event->title, "rev"=>$event->revision] ); } - public function onWikiDeletePage(WikiDeletePageEvent $event): void + public function onWikiDeletePage(WikiDeletePageEvent $event) { global $database; $database->execute( @@ -317,9 +325,6 @@ class Wiki extends Extension return false; } - /** - * @return array - */ public static function get_history(string $title): array { global $database; @@ -331,12 +336,12 @@ class Wiki extends Extension WHERE LOWER(title) LIKE LOWER(:title) ORDER BY revision DESC ", - ["title" => $title] + ["title"=>$title] ); } #[Query(name: "wiki")] - public static function get_page(string $title, ?int $revision = null): WikiPage + public static function get_page(string $title, ?int $revision=null): WikiPage { global $database; // first try and get the actual page @@ -348,7 +353,7 @@ class Wiki extends Extension AND (:revision = -1 OR revision = :revision) ORDER BY revision DESC ", - ["title" => $title, "revision" => $revision ?? -1] + ["title"=>$title, "revision"=>$revision ?? -1] ); // fall back to wiki:default @@ -358,7 +363,7 @@ class Wiki extends Extension FROM wiki_pages WHERE title LIKE :title ORDER BY revision DESC - ", ["title" => "wiki:default"]); + ", ["title"=>"wiki:default"]); // fall further back to manual if (empty($row)) { @@ -392,13 +397,13 @@ class Wiki extends Extension SELECT * FROM tags WHERE tag = :title - ", ["title" => $page->title]); + ", ["title"=>$page->title]); if (!empty($row)) { $template = $config->get_string(WikiConfig::TAG_PAGE_TEMPLATE); //CATEGORIES - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists("Shimmie2\TagCategories")) { $tagcategories = new TagCategories(); $tag_category_dict = $tagcategories->getKeyedDict(); } @@ -409,7 +414,7 @@ class Wiki extends Extension FROM aliases WHERE newtag = :title ORDER BY oldtag ASC - ", ["title" => $row["tag"]]); + ", ["title"=>$row["tag"]]); if (!empty($aliases)) { $template = str_replace("{aliases}", implode(", ", $aliases), $template); @@ -421,12 +426,12 @@ class Wiki extends Extension $template = format_text($template); //Things after this line will NOT be escaped!!! Be careful what you add. - if (Extension::is_enabled(AutoTaggerInfo::KEY)) { + if (class_exists("Shimmie2\AutoTagger")) { $auto_tags = $database->get_one(" SELECT additional_tags FROM auto_tag WHERE tag = :title - ", ["title" => $row["tag"]]); + ", ["title"=>$row["tag"]]); if (!empty($auto_tags)) { $auto_tags = Tag::explode($auto_tags); @@ -439,7 +444,7 @@ class Wiki extends Extension SELECT * FROM tags WHERE tag = :title - ", ["title" => $a_tag]); + ", ["title"=>$a_tag]); $tag_html = $tag_list_t->return_tag($a_row, $tag_category_dict ?? []); $f_auto_tags[] = $tag_html[1]; @@ -455,4 +460,237 @@ class Wiki extends Extension //Insert page body AT LAST to avoid replacing its contents with the actions above. return str_replace("{body}", format_text($page->body), $template ?? "{body}"); } + + /** + Diff implemented in pure php, written from scratch. + Copyright (C) 2003 Daniel Unterberger + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + https://www.gnu.org/licenses/gpl.html + + About: + I searched a function to compare arrays and the array_diff() + was not specific enough. It ignores the order of the array-values. + So I reimplemented the diff-function which is found on unix-systems + but this you can use directly in your code and adopt for your needs. + Simply adopt the formatline-function. with the third-parameter of arr_diff() + you can hide matching lines. Hope someone has use for this. + + Contact: d.u.diff@holomind.de + **/ + + private function arr_diff(array $f1, array $f2, int $show_equal = 0): string + { + $c1 = 0 ; # current line of left + $c2 = 0 ; # current line of right + $max1 = count($f1) ; # maximal lines of left + $max2 = count($f2) ; # maximal lines of right + $outcount = 0; # output counter + $hit1 = []; # hit in left + $hit2 = []; # hit in right + $stop = 0; + $out = ""; + + while ( + $c1 < $max1 # have next line in left + and + $c2 < $max2 # have next line in right + and + ($stop++) < 1000 # don-t have more then 1000 ( loop-stopper ) + and + $outcount < 20 # output count is less then 20 + ) { + /** + * is the trimmed line of the current left and current right line + * the same ? then this is a hit (no difference) + */ + if (trim($f1[$c1]) == trim($f2[$c2])) { + /** + * add to output-string, if "show_equal" is enabled + */ + $out .= ($show_equal==1) + ? $this->formatline(($c1), ($c2), "=", $f1[ $c1 ]) + : "" ; + /** + * increase the out-putcounter, if "show_equal" is enabled + * this ist more for demonstration purpose + */ + if ($show_equal == 1) { + $outcount++ ; + } + + /** + * move the current-pointer in the left and right side + */ + $c1 ++; + $c2 ++; + } + + /** + * the current lines are different so we search in parallel + * on each side for the next matching pair, we walk on both + * sided at the same time comparing with the current-lines + * this should be most probable to find the next matching pair + * we only search in a distance of 10 lines, because then it + * is not the same function most of the time. other algos + * would be very complicated, to detect 'real' block movements. + */ + else { + $b = "" ; + $s1 = 0 ; # search on left + $s2 = 0 ; # search on right + $found = 0 ; # flag, found a matching pair + $b1 = "" ; + $b2 = "" ; + $fstop = 0 ; # distance of maximum search + + #fast search in on both sides for next match. + while ( + $found == 0 # search until we find a pair + and + ($c1 + $s1 <= $max1) # and we are inside of the left lines + and + ($c2 + $s2 <= $max2) # and we are inside of the right lines + and + $fstop++ < 10 # and the distance is lower than 10 lines + ) { + /** + * test the left side for a hit + * + * comparing current line with the searching line on the left + * b1 is a buffer, which collects the line which not match, to + * show the differences later, if one line hits, this buffer will + * be used, else it will be discarded later + */ + #hit + if (trim($f1[$c1+$s1]) == trim($f2[$c2])) { + $found = 1 ; # set flag to stop further search + $s2 = 0 ; # reset right side search-pointer + $c2-- ; # move back the current right, so next loop hits + $b = $b1 ; # set b=output (b)uffer + } + #no hit: move on + else { + /** + * prevent finding a line again, which would show wrong results + * + * add the current line to leftbuffer, if this will be the hit + */ + if ($hit1[ ($c1 + $s1) . "_" . ($c2) ] != 1) { + /** + * add current search-line to diffence-buffer + */ + $b1 .= $this->formatline(($c1 + $s1), ($c2), "-", $f1[ $c1+$s1 ]); + + /** + * mark this line as 'searched' to prevent doubles. + */ + $hit1[ ($c1 + $s1) . "_" . $c2 ] = 1 ; + } + } + + + + /** + * test the right side for a hit + * + * comparing current line with the searching line on the right + */ + if (trim($f1[$c1]) == trim($f2[$c2+$s2])) { + $found = 1 ; # flag to stop search + $s1 = 0 ; # reset pointer for search + $c1-- ; # move current line back, so we hit next loop + $b = $b2 ; # get the buffered difference + } else { + /** + * prevent to find line again + */ + if ($hit2[ ($c1) . "_" . ($c2 + $s2) ] != 1) { + /** + * add current searchline to buffer + */ + $b2 .= $this->formatline(($c1), ($c2 + $s2), "+", $f2[ $c2+$s2 ]); + + /** + * mark current line to prevent double-hits + */ + $hit2[ ($c1) . "_" . ($c2 + $s2) ] = 1; + } + } + + /** + * search in bigger distance + * + * increase the search-pointers (satelites) and try again + */ + $s1++ ; # increase left search-pointer + $s2++ ; # increase right search-pointer + } + + /** + * add line as different on both arrays (no match found) + */ + if ($found == 0) { + $b .= $this->formatline(($c1), ($c2), "-", $f1[ $c1 ]); + $b .= $this->formatline(($c1), ($c2), "+", $f2[ $c2 ]); + } + + /** + * add current buffer to outputstring + */ + $out .= $b; + $outcount++ ; #increase outcounter + + $c1++ ; #move currentline forward + $c2++ ; #move currentline forward + + /** + * comment the lines are tested quite fast, because + * the current line always moves forward + */ + } /*endif*/ + }/*endwhile*/ + + return $out; + }/*end func*/ + + /** + * callback function to format the diffence-lines with your 'style' + */ + private function formatline(int $nr1, int $nr2, string $stat, $value): string + { #change to $value if problems + if (trim($value) == "") { + return ""; + } + + switch ($stat) { + case "=": + // return $nr1. " : $nr2 : = ".htmlentities( $value ) ."
    "; + return "$value\n"; + + case "+": + //return $nr1. " : $nr2 : + ".htmlentities( $value ) ."
    "; + return "+++ $value\n"; + + case "-": + //return $nr1. " : $nr2 : - ".htmlentities( $value ) ."
    "; + return "--- $value\n"; + + default: + throw new \RuntimeException("stat needs to be =, + or -"); + } + } } diff --git a/ext/wiki/test.php b/ext/wiki/test.php index 86ef0f61..8f5551cc 100644 --- a/ext/wiki/test.php +++ b/ext/wiki/test.php @@ -6,14 +6,14 @@ namespace Shimmie2; class WikiTest extends ShimmiePHPUnitTestCase { - public function testIndex(): void + public function testIndex() { $this->get_page("wiki"); $this->assert_title("Index"); $this->assert_text("This is a default page"); } - public function testAccess(): void + public function testAccess() { global $config; foreach (["anon", "user", "admin"] as $user) { @@ -35,7 +35,7 @@ class WikiTest extends ShimmiePHPUnitTestCase $this->assert_text("This is a default page"); if ($allowed || $user == "admin") { - $this->post_page("wiki_admin/edit", ["title" => "test"]); + $this->post_page("wiki_admin/edit", ["title"=>"test"]); $this->assert_text("Editor"); } /* @@ -53,7 +53,7 @@ class WikiTest extends ShimmiePHPUnitTestCase } } - public function testDefault(): void + public function testDefault() { global $user; $this->log_in_as_admin(); @@ -82,7 +82,7 @@ class WikiTest extends ShimmiePHPUnitTestCase $this->assert_text("This is a default page"); } - public function testRevisions(): void + public function testRevisions() { global $user; $this->log_in_as_admin(); diff --git a/ext/wiki/theme.php b/ext/wiki/theme.php index 36f83791..66b315a6 100644 --- a/ext/wiki/theme.php +++ b/ext/wiki/theme.php @@ -12,7 +12,7 @@ class WikiTheme extends Themelet * $wiki_page The wiki page, has ->title and ->body * $nav_page A wiki page object with navigation, has ->body */ - public function display_page(Page $page, WikiPage $wiki_page, ?WikiPage $nav_page = null): void + public function display_page(Page $page, WikiPage $wiki_page, ?WikiPage $nav_page=null) { global $user; @@ -30,7 +30,7 @@ class WikiTheme extends Themelet // see if title is a category'd tag $title_html = html_escape($wiki_page->title); - if (Extension::is_enabled(TagCategoriesInfo::KEY)) { + if (class_exists('Shimmie2\TagCategories')) { $tagcategories = new TagCategories(); $tag_category_dict = $tagcategories->getKeyedDict(); $title_html = $tagcategories->getTagHtml($title_html, $tag_category_dict); @@ -47,11 +47,7 @@ class WikiTheme extends Themelet $page->add_block(new Block($title_html, $this->create_display_html($wiki_page))); } - /** - * @param array $history - */ - - public function display_page_history(Page $page, string $title, array $history): void + public function display_page_history(Page $page, string $title, array $history) { $html = ""; foreach ($history as $row) { @@ -65,7 +61,7 @@ class WikiTheme extends Themelet $page->add_block(new Block(html_escape($title), $html)); } - public function display_page_editor(Page $page, WikiPage $wiki_page): void + public function display_page_editor(Page $page, WikiPage $wiki_page) { $page->set_title(html_escape($wiki_page->title)); $page->set_heading(html_escape($wiki_page->title)); diff --git a/ext/word_filter/main.php b/ext/word_filter/main.php index b456a24f..fd753390 100644 --- a/ext/word_filter/main.php +++ b/ext/word_filter/main.php @@ -12,13 +12,13 @@ class WordFilter extends Extension return 40; } - public function onTextFormatting(TextFormattingEvent $event): void + public function onTextFormatting(TextFormattingEvent $event) { $event->formatted = $this->filter($event->formatted); $event->stripped = $this->filter($event->stripped); } - public function onSetupBuilding(SetupBuildingEvent $event): void + public function onSetupBuilding(SetupBuildingEvent $event) { $sb = $event->panel->create_new_block("Word Filter"); $sb->add_longtext_option("word_filter"); @@ -42,7 +42,7 @@ class WordFilter extends Extension } /** - * @return string[] + * #return string[] */ private function get_map(): array { diff --git a/ext/word_filter/test.php b/ext/word_filter/test.php index 04eccc25..a5ba62c2 100644 --- a/ext/word_filter/test.php +++ b/ext/word_filter/test.php @@ -13,7 +13,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase $config->set_string("word_filter", "whore,nice lady\na duck,a kitten\n white ,\tspace\ninvalid"); } - public function _doThings(string $in, string $out): void + public function _doThings($in, $out) { global $user; $this->log_in_as_user(); @@ -23,7 +23,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase $this->assert_text($out); } - public function testRegular(): void + public function testRegular() { $this->_doThings( "posted by a whore", @@ -31,7 +31,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase ); } - public function testReplaceAll(): void + public function testReplaceAll() { $this->_doThings( "a whore is a whore is a whore", @@ -39,7 +39,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase ); } - public function testMixedCase(): void + public function testMixedCase() { $this->_doThings( "monkey WhorE", @@ -47,7 +47,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase ); } - public function testOnlyWholeWords(): void + public function testOnlyWholeWords() { $this->_doThings( "my name is whoretta", @@ -55,7 +55,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase ); } - public function testMultipleWords(): void + public function testMultipleWords() { $this->_doThings( "I would like a duck", @@ -63,7 +63,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase ); } - public function testWhitespace(): void + public function testWhitespace() { $this->_doThings( "A colour is white", @@ -71,7 +71,7 @@ class WordFilterTest extends ShimmiePHPUnitTestCase ); } - public function testIgnoreInvalid(): void + public function testIgnoreInvalid() { $this->_doThings( "The word was invalid", diff --git a/index.php b/index.php index 8b3c03a2..61b176ca 100644 --- a/index.php +++ b/index.php @@ -23,7 +23,7 @@ if (!file_exists("vendor/")) { ); } -if (!file_exists("data/config/shimmie.conf.php") && !getenv("SHM_DATABASE_DSN")) { +if (!file_exists("data/config/shimmie.conf.php")) { require_once "core/install.php"; install(); exit; @@ -50,7 +50,9 @@ _load_core_files(); $cache = loadCache(CACHE_DSN); $database = new Database(DATABASE_DSN); $config = new DatabaseConfig($database); -_load_extension_files(); +ExtensionInfo::load_all_extension_info(); +Extension::determine_enabled_extensions(); +require_all(zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/main.php")); _load_theme_files(); $page = new Page(); _load_event_listeners(); @@ -65,9 +67,9 @@ try { $_tracer->begin( $_SERVER["REQUEST_URI"] ?? "No Request", [ - "user" => $_COOKIE["shm_user"] ?? "No User", - "ip" => get_real_ip() ?? "No IP", - "user_agent" => $_SERVER['HTTP_USER_AGENT'] ?? "No UA", + "user"=>$_COOKIE["shm_user"] ?? "No User", + "ip"=>get_real_ip() ?? "No IP", + "user_agent"=>$_SERVER['HTTP_USER_AGENT'] ?? "No UA", ] ); @@ -80,15 +82,9 @@ try { $user = _get_user(); send_event(new UserLoginEvent($user)); if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') { - ob_end_flush(); - ob_implicit_flush(true); - $app = new CliApp(); - send_event(new CliGenEvent($app)); - if($app->run() !== 0) { - throw new \Exception("CLI command failed"); - } + send_event(new CommandEvent($argv)); } else { - send_event(new PageRequestEvent($_SERVER['REQUEST_METHOD'], _get_query())); + send_event(new PageRequestEvent(_get_query())); $page->display(); } @@ -100,13 +96,11 @@ try { if (function_exists("fastcgi_finish_request")) { fastcgi_finish_request(); } - $exit_code = 0; } catch (\Exception $e) { - if ($database->is_transaction_open()) { + if ($database && $database->is_transaction_open()) { $database->rollback(); } _fatal_error($e); - $exit_code = 1; } finally { $_tracer->end(); if (TRACE_FILE) { @@ -122,6 +116,3 @@ try { } } } -if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') { - exit($exit_code); -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ec8f9b74..8203ee96 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,16 +1,11 @@ begin("bootstrap"); _load_core_files(); $cache = loadCache(CACHE_DSN); -$database = new Database(getenv("TEST_DSN") ?: "sqlite::memory:"); +$dsn = getenv("TEST_DSN"); +$database = new Database($dsn ? $dsn : "sqlite::memory:"); create_dirs(); create_tables($database); $config = new DatabaseConfig($database); -_load_extension_files(); +ExtensionInfo::load_all_extension_info(); +Extension::determine_enabled_extensions(); +require_all(zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/main.php")); _load_theme_files(); $page = new Page(); _load_event_listeners(); -$config->set_string("thumb_engine", "static"); +$config->set_string("thumb_engine", "static"); # GD has less overhead per-call $config->set_bool("nice_urls", true); send_event(new DatabaseUpgradeEvent()); send_event(new InitExtEvent()); $user = User::by_id($config->get_int("anon_id", 0)); -$userPage = new UserPage(); -$userPage->onUserCreation(new UserCreationEvent("demo", "demo", "demo", "demo@demo.com", false)); -$userPage->onUserCreation(new UserCreationEvent("test", "test", "test", "test@test.com", false)); -// in mysql, CREATE TABLE commits transactions, so after the database -// upgrade we may or may not be inside a transaction depending on if -// any tables were created. -if($database->is_transaction_open()) { - $database->commit(); -} $_tracer->end(); + +abstract class ShimmiePHPUnitTestCase extends TestCase +{ + protected static string $anon_name = "anonymous"; + protected static string $admin_name = "demo"; + protected static string $user_name = "test"; + protected string $wipe_time = "test"; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + global $_tracer; + $_tracer->begin(get_called_class()); + + self::create_user(self::$admin_name); + self::create_user(self::$user_name); + } + + public function setUp(): void + { + global $database, $_tracer; + $_tracer->begin($this->getName()); + $_tracer->begin("setUp"); + $class = str_replace("Test", "", get_class($this)); + if (!ExtensionInfo::get_for_extension_class($class)->is_supported()) { + $this->markTestSkipped("$class not supported with this database"); + } + + // Set up a clean environment for each test + 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; + $_tracer->end(); # test + $_tracer->end(); # $this->getName() + $_tracer->clear(); + $_tracer->flush("tests/trace.json"); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + global $_tracer; + $_tracer->end(); # get_called_class() + } + + protected static function create_user(string $name): void + { + if (is_null(User::by_name($name))) { + $userPage = new UserPage(); + $userPage->onUserCreation(new UserCreationEvent($name, $name, $name, "", false)); + assert(!is_null(User::by_name($name)), "Creation of user $name failed"); + } + } + + 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; + } + + protected static function request($page_name, $get_args=null, $post_args=null): 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($page_name)); + if ($page->mode == PageMode::REDIRECT) { + $page->code = 302; + } + return $page; + } + + protected static function get_page($page_name, $args=null): Page + { + return self::request($page_name, $args, null); + } + + protected static function post_page($page_name, $args=null): Page + { + return self::request($page_name, null, $args); + } + + // page things + protected function assert_title(string $title): void + { + global $page; + $this->assertStringContainsString($title, $page->title); + } + + protected function assert_title_matches($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 not PAGE or DATA"); + } + } + + 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)); + } + + 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); + } + + protected function assert_search_results($tags, $results): void + { + $images = Image::find_images(0, null, $tags); + $ids = []; + foreach ($images as $image) { + $ids[] = $image->id; + } + $this->assertEquals($results, $ids); + } + + // 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($dae->image_id == -1) throw new \Exception("Upload failed :("); + return $dae->image_id; + } + + protected function delete_image(int $image_id): void + { + $img = Image::by_id($image_id); + if ($img) { + send_event(new ImageDeletionEvent($img, true)); + } + } +} diff --git a/tests/defines.php b/tests/defines.php index 3d5a9a4c..dc6ad864 100644 --- a/tests/defines.php +++ b/tests/defines.php @@ -5,9 +5,7 @@ declare(strict_types=1); namespace Shimmie2; define("UNITTEST", true); -$_all_exts = glob('ext/*'); -assert($_all_exts !== false); -define("EXTRA_EXTS", str_replace("ext/", "", implode(',', $_all_exts))); +define("EXTRA_EXTS", str_replace("ext/", "", implode(',', glob('ext/*')))); define("DATABASE_DSN", null); define("DATABASE_TIMEOUT", 10000); @@ -20,6 +18,7 @@ define("VERSION", 'unit-tests'); define("TRACE_FILE", null); define("TRACE_THRESHOLD", 0.0); define("TIMEZONE", 'UTC'); +define("BASE_HREF", "/test"); define("CLI_LOG_LEVEL", 50); define("STATSD_HOST", null); -define("TRUSTED_PROXIES", []); +define("REVERSE_PROXY_X_HEADERS", false); diff --git a/tests/docker-init.sh b/tests/docker-init.sh new file mode 100644 index 00000000..e8385443 --- /dev/null +++ b/tests/docker-init.sh @@ -0,0 +1,12 @@ +#!/bin/sh +groupadd -g $GID shimmie || true +useradd -ms /bin/bash -u $UID -g $GID shimmie +mkdir -p /app/data +chown $UID:$GID /app/data +export PHP_CLI_SERVER_WORKERS=8 +exec gosu shimmie:shimmie \ + /usr/bin/php \ + -d upload_max_filesize=$UPLOAD_MAX_FILESIZE \ + -d post_max_size=$UPLOAD_MAX_FILESIZE \ + -S 0.0.0.0:8000 \ + tests/router.php 2>&1 | grep --line-buffered -vE " (Accepted|Closing)" diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 644b3d7c..2a379a7f 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -1,17 +1,9 @@ parameters: - errorFormat: raw - level: 7 + level: 2 paths: - ../core - ../ext - ../tests - - ../themes - dynamicConstantNames: - - DEBUG - - SPEED_HAX - - TRUSTED_PROXIES - - TIMEZONE - - BASE_HREF - - STATSD_HOST - - TRACE_FILE - - UNITTEST + - ../themes/default + ignoreErrors: + - '#Access to an undefined property Shimmie2\\Image::\$#' diff --git a/tests/phpunit.xml b/tests/phpunit.xml index de47f9ef..2ded9aa8 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,25 +1,18 @@ - - - - - ../core/ - - - ../ext/ - - - + + ../core ../ext ../themes/default - + + + + ../core/ + + + ../ext/ + + diff --git a/tests/router.php b/tests/router.php new file mode 100644 index 00000000..63a522b4 --- /dev/null +++ b/tests/router.php @@ -0,0 +1,36 @@ +> $GITHUB_ENV - echo "INSTALL_DSN=$TEST_DSN" >> $GITHUB_ENV -fi diff --git a/themes/danbooru/comment.theme.php b/themes/danbooru/comment.theme.php index fdf36413..ec3fb85d 100644 --- a/themes/danbooru/comment.theme.php +++ b/themes/danbooru/comment.theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class CustomCommentListTheme extends CommentListTheme { - /** - * @param array $images - */ - public function display_comment_list(array $images, int $page_number, int $total_pages, bool $can_post): void + public function display_comment_list(array $images, int $page_number, int $total_pages, bool $can_post) { global $config, $page, $user; @@ -48,19 +45,19 @@ class CustomCommentListTheme extends CommentListTheme $un = $image->get_owner()->name; $t = ""; foreach ($image->get_tag_array() as $tag) { - $t .= "".html_escape($tag)." "; + $u_tag = url_escape($tag); + $t .= "".html_escape($tag)." "; } $p = autodate($image->posted); - $r = Extension::is_enabled(RatingsInfo::KEY) ? "Rating ".Ratings::rating_to_human($image['rating']) : ""; + $r = Extension::is_enabled(RatingsInfo::KEY) ? "Rating ".Ratings::rating_to_human($image->rating) : ""; $comment_html = "Date $p $s User $un $s $r
    Tags $t

     "; $comment_count = count($comments); if ($comment_limit > 0 && $comment_count > $comment_limit) { //$hidden = $comment_count - $comment_limit; $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; - - $comments = array_slice($comments, negative_int($comment_limit)); + $comments = array_slice($comments, -$comment_limit); } foreach ($comments as $comment) { $comment_html .= $this->comment_to_html($comment); @@ -89,12 +86,12 @@ class CustomCommentListTheme extends CommentListTheme } } - public function display_recent_comments(array $comments): void + public function display_recent_comments(array $comments) { // no recent comments in this theme } - protected function comment_to_html(Comment $comment, bool $trim = false): string + protected function comment_to_html(Comment $comment, bool $trim=false): string { global $user; @@ -112,7 +109,7 @@ class CustomCommentListTheme extends CommentListTheme $h_del = ""; if ($user->can(Permissions::DELETE_COMMENT)) { $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); - $j_delete_confirm_message = json_encode_ex("Delete comment by {$comment->owner_name}:\n$comment_preview"); + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); $h_del = " - Del"; diff --git a/themes/danbooru/index.theme.php b/themes/danbooru/index.theme.php index 13664dd9..9a24cc1b 100644 --- a/themes/danbooru/index.theme.php +++ b/themes/danbooru/index.theme.php @@ -7,9 +7,9 @@ namespace Shimmie2; class CustomIndexTheme extends IndexTheme { /** - * @param Image[] $images + * #param Image[] $images */ - public function display_page(Page $page, array $images): void + public function display_page(Page $page, array $images) { $this->display_shortwiki($page); @@ -20,7 +20,7 @@ class CustomIndexTheme extends IndexTheme $page_title = $config->get_string(SetupConfig::TITLE); } else { $search_string = Tag::implode($this->search_terms); - $query = url_escape($search_string); + $query = url_escape(Tag::caret($search_string)); $page_title = html_escape($search_string); } @@ -42,7 +42,7 @@ class CustomIndexTheme extends IndexTheme } /** - * @param string[] $search_terms + * #param string[] $search_terms */ protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string { @@ -51,7 +51,7 @@ class CustomIndexTheme extends IndexTheme return "

    - +

    "; diff --git a/themes/danbooru/page.class.php b/themes/danbooru/page.class.php index d0bafacc..99009405 100644 --- a/themes/danbooru/page.class.php +++ b/themes/danbooru/page.class.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - /** * Name: Danbooru Theme * Author: Bzchan @@ -50,7 +48,14 @@ Tips * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ class Page extends BasePage { - public function body_html(): string + public bool $left_enabled = true; + + public function disable_left() + { + $this->left_enabled = false; + } + + public function render() { global $config; @@ -123,30 +128,37 @@ class Page extends BasePage } $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); $footer_html = $this->footer_html(); - return << - $title_link - - - - $subheading - $sub_block_html - $left -
    - $flash_html - $main_block_html -
    -
    $footer_html
    + print << + + $head_html + +
    + $title_link + + +
    + $subheading + $sub_block_html + $left +
    + $flash_html + $main_block_html +
    +
    $footer_html
    + + EOD; } - public function navlinks(Link $link, HTMLElement|string $desc, bool $active): ?string + public function navlinks(Link $link, string $desc, bool $active): ?string { $html = null; if ($active) { diff --git a/themes/danbooru/style.css b/themes/danbooru/style.css index 32946a14..598c05f9 100644 --- a/themes/danbooru/style.css +++ b/themes/danbooru/style.css @@ -200,6 +200,13 @@ background:blue none repeat scroll 0 0; border:1px solid #EEEEEE; color:white; } +span.thumb { +display:inline-block; +float:left; +height:220px; +text-align:center; +width:220px; +} #pagelist { margin-top:32px; } @@ -212,6 +219,21 @@ margin:16px; padding:8px; width:350px; } +.helpable { +border-bottom:1px dashed gray; +} +.ok { +-moz-background-clip:border; +-moz-background-inline-policy:continuous; +-moz-background-origin:padding; +background:#AAFFAA none repeat scroll 0 0; +} +.bad { +-moz-background-clip:border; +-moz-background-inline-policy:continuous; +-moz-background-origin:padding; +background:#FFAAAA none repeat scroll 0 0; +} .comment .username { font-size:1.5em; font-weight:bold; diff --git a/themes/danbooru/tag_list.theme.php b/themes/danbooru/tag_list.theme.php index 118019e0..4dd0bb02 100644 --- a/themes/danbooru/tag_list.theme.php +++ b/themes/danbooru/tag_list.theme.php @@ -6,7 +6,7 @@ namespace Shimmie2; class CustomTagListTheme extends TagListTheme { - public function display_page(Page $page): void + public function display_page(Page $page) { $page->disable_left(); parent::display_page($page); diff --git a/themes/danbooru/themelet.class.php b/themes/danbooru/themelet.class.php index 422c62ec..de10b659 100644 --- a/themes/danbooru/themelet.class.php +++ b/themes/danbooru/themelet.class.php @@ -6,11 +6,11 @@ namespace Shimmie2; use MicroHTML\HTMLElement; -use function MicroHTML\{A, B, DIV, joinHTML}; +use function MicroHTML\{A, B, DIV}; class Themelet extends BaseThemelet { - 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; @@ -40,21 +40,21 @@ class Themelet extends BaseThemelet $prev = $current_page - 1; $at_start = ($current_page <= 3 || $total_pages <= 3); - $at_end = ($current_page >= $total_pages - 2); + $at_end = ($current_page >= $total_pages -2); $first_html = $at_start ? "" : $this->gen_page_link($base_url, $query, 1, "1"); $prev_html = $at_start ? "" : $this->gen_page_link($base_url, $query, $prev, "<<"); $next_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $next, ">>"); $last_html = $at_end ? "" : $this->gen_page_link($base_url, $query, $total_pages, "$total_pages"); - $start = $current_page - 2 > 1 ? $current_page - 2 : 1; - $end = $current_page + 2 <= $total_pages ? $current_page + 2 : $total_pages; + $start = $current_page-2 > 1 ? $current_page-2 : 1; + $end = $current_page+2 <= $total_pages ? $current_page+2 : $total_pages; $pages = []; foreach (range($start, $end) as $i) { $pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, (string)$i); } - $pages_html = joinHTML(" ", $pages); + $pages_html = $this->implode(" ", $pages); if ($first_html) { $pdots = "..."; @@ -68,6 +68,6 @@ class Themelet extends BaseThemelet $ndots = ""; } - return DIV(["id" => 'paginator'], joinHTML(" ", [$prev_html, $first_html, $pdots, $pages_html, $ndots, $last_html, $next_html])); + return DIV(["id"=>'paginator'], $this->implode(" ", [$prev_html, $first_html, $pdots, $pages_html, $ndots, $last_html, $next_html])); } } diff --git a/themes/danbooru/user.theme.php b/themes/danbooru/user.theme.php index 3772db16..276dd53f 100644 --- a/themes/danbooru/user.theme.php +++ b/themes/danbooru/user.theme.php @@ -6,7 +6,7 @@ namespace Shimmie2; class CustomUserPageTheme extends UserPageTheme { - public function display_login_page(Page $page): void + public function display_login_page(Page $page) { global $config; $page->set_title("Login"); @@ -33,22 +33,16 @@ class CustomUserPageTheme extends UserPageTheme $page->add_block(new Block("Login", $html, "main", 90)); } - /** - * @param array $parts - */ - public function display_user_links(Page $page, User $user, array $parts): void + public function display_user_links(Page $page, User $user, $parts) { // no block in this theme } - public function display_login_block(Page $page): void + public function display_login_block(Page $page) { // no block in this theme } - /** - * @param array $parts - */ - public function display_user_block(Page $page, User $user, array $parts): void + public function display_user_block(Page $page, User $user, $parts) { $html = ""; $blocked = ["Pools", "Pool Changes", "Alias Editor", "My Profile"]; @@ -63,7 +57,7 @@ class CustomUserPageTheme extends UserPageTheme $page->add_block($b); } - public function display_signup_page(Page $page): void + public function display_signup_page(Page $page) { global $config; $tac = $config->get_string("login_tac", ""); @@ -97,12 +91,7 @@ class CustomUserPageTheme extends UserPageTheme $page->add_block(new Block("Signup", $html)); } - /** - * @param array $uploads - * @param array $comments - * @param array $events - */ - public function display_ip_list(Page $page, array $uploads, array $comments, array $events): void + public function display_ip_list(Page $page, array $uploads, array $comments, array $events) { $html = "
    "; $html .= " + + + + "; + } + + public function get_source_editor_html(Image $image): string + { + global $user; + $h_source = html_escape($image->get_source()); + $f_source = $this->format_source($image->get_source()); + $style = "overflow: hidden; white-space: nowrap; max-width: 350px; text-overflow: ellipsis;"; + return " + + + + + "; + } +} diff --git a/themes/rule34v2/themelet.class.php b/themes/rule34v2/themelet.class.php new file mode 100644 index 00000000..e9902ad8 --- /dev/null +++ b/themes/rule34v2/themelet.class.php @@ -0,0 +1,53 @@ +get("thumb-block:{$image->id}"); + if (!is_null($cached)) { + return rawHTML($cached); + } + + $id = $image->id; + $view_link = make_link('post/view/'.$id); + $image_link = $image->get_image_link(); + $thumb_link = $image->get_thumb_link(); + $tip = $image->get_tooltip(); + $tags = strtolower($image->get_tag_list()); + $ext = strtolower($image->get_ext()); + + // If file is flash or svg then sets thumbnail to max size. + if ($image->get_mime() === MimeType::FLASH || $image->get_mime() === MimeType::SVG) { + $tsize = get_thumbnail_size($config->get_int('thumb_width'), $config->get_int('thumb_height')); + } else { + $tsize = get_thumbnail_size($image->width, $image->height); + } + + $html = DIV( + ['class'=>'shm-thumb thumb', 'data-ext'=>$ext, 'data-tags'=>$tags, 'data-post-id'=>$id], + A( + ['class'=>'shm-thumb-link', 'href'=>$view_link], + IMG(['id'=>"thumb_$id", 'title'=>$tip, 'alt'=>$tip, 'height'=>$tsize[1], 'width'=>$tsize[0], 'src'=>$thumb_link, 'loading'=>'lazy']) + ), + BR(), + A(['href'=>$image_link], 'File Only'), + SPAN(['class'=>'need-del'], ' - ', A(['href'=>'#', 'onclick'=>"image_hash_ban($id); return false;"], 'Ban')) + ); + + // cache for ages; will be cleared in ext/index:onImageInfoSet + $cache->set("thumb-block:{$image->id}", (string)$html, rand(43200, 86400)); + + return $html; + } +} diff --git a/themes/rule34v2/upload.theme.php b/themes/rule34v2/upload.theme.php new file mode 100644 index 00000000..405364e8 --- /dev/null +++ b/themes/rule34v2/upload.theme.php @@ -0,0 +1,113 @@ +add_block(new Block("Upload", $this->build_upload_block(), "head", 20)); + $page->add_block(new Block("Upload", $this->build_upload_block(), "left", 20)); + } + + public function display_full(Page $page): void + { + $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "head", 20)); + } + + public function display_page(Page $page): void + { + global $config, $page; + + $tl_enabled = ($config->get_string(UploadConfig::TRANSLOAD_ENGINE, "none") != "none"); + $max_size = $config->get_int(UploadConfig::SIZE); + $max_kb = to_shorthand_int($max_size); + $upload_list = $this->h_upload_list_1(); + + $form = SHM_FORM("upload", "POST", true, "file_upload"); + $form->appendChild( + TABLE( + ["id"=>"large_upload_form", "class"=>"vert"], + TR( + TD(["width"=>"20"], rawHTML("Common Tags")), + TD(["colspan"=>"5"], INPUT(["name"=>"tags", "type"=>"text", "placeholder"=>"tagme", "autocomplete"=>"off"])) + ), + TR( + TD(["width"=>"20"], rawHTML("Common Source")), + TD(["colspan"=>"5"], INPUT(["name"=>"source", "type"=>"text"])) + ), + $upload_list, + TR( + TD(["colspan"=>"6"], INPUT(["id"=>"uploadbutton", "type"=>"submit", "value"=>"Post"])) + ), + ) + ); + $html = emptyHTML( + $form, + SMALL("(Max file size is $max_kb)") + ); + + $page->set_title("Upload"); + $page->set_heading("Upload"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Upload", $html, "main", 20)); + if ($tl_enabled) { + $page->add_block(new Block("Bookmarklets", (string)$this->h_bookmarklets(), "left", 20)); + } + $html = " + Tagging Guide + "; + $page->add_block(new Block(null, $html, "main", 19)); + } + + protected function build_upload_block(): HTMLElement + { + return A(["href"=>make_link("upload"), "style"=>'font-size: 2em; display: block;'], "Upload"); + } + + protected function h_upload_list_1(): HTMLElement + { + global $config; + $upload_list = emptyHTML(); + $upload_count = $config->get_int(UploadConfig::COUNT); + $tl_enabled = ($config->get_string(UploadConfig::TRANSLOAD_ENGINE, "none") != "none"); + $accept = $this->get_accept(); + + $upload_list->appendChild( + TR( + TD(["colspan"=>$tl_enabled ? 2 : 4], "Files"), + $tl_enabled ? TD(["colspan"=>"2"], "URLs") : emptyHTML(), + TD(["colspan"=>"2"], "Post-Specific Tags"), + ) + ); + + for ($i=0; $i<$upload_count; $i++) { + $upload_list->appendChild( + TR( + TD(["colspan"=>$tl_enabled ? 2 : 4], INPUT(["type"=>"file", "name"=>"data{$i}[]", "accept"=>$accept, "multiple"=>true])), + $tl_enabled ? TD(["colspan"=>"2"], INPUT(["type"=>"text", "name"=>"url{$i}"])) : emptyHTML(), + TD(["colspan"=>"2"], INPUT(["type"=>"text", "name"=>"tags{$i}", "autocomplete"=>"off"])), + ) + ); + } + + return $upload_list; + } +} diff --git a/themes/rule34v2/user.theme.php b/themes/rule34v2/user.theme.php new file mode 100644 index 00000000..56e2219f --- /dev/null +++ b/themes/rule34v2/user.theme.php @@ -0,0 +1,151 @@ +name); + $lines = []; + foreach ($parts as $part) { + if ($part["name"] == "User Options") { + continue; + } + $lines[] = "{$part["name"]}"; + } + if (count($lines) < 6) { + $html = implode("\n
    ", $lines); + } else { + $html = implode(" | \n", $lines); + } + $page->add_block(new Block("Logged in as $h_name", $html, "head", 90, "UserBlockhead")); + $page->add_block(new Block("Logged in as $h_name", $html, "left", 15, "UserBlockleft")); + } + + public function display_login_block(Page $page) + { + global $config; + $html = " + +
    Uploaded from: "; @@ -119,10 +108,7 @@ class CustomUserPageTheme extends UserPageTheme $page->add_block(new Block("IPs", $html)); } - /** - * @param string[] $stats - */ - public function display_user_page(User $duser, array $stats): void + public function display_user_page(User $duser, $stats) { global $page; $page->disable_left(); diff --git a/themes/danbooru/view.theme.php b/themes/danbooru/view.theme.php index eed15aea..7314829e 100644 --- a/themes/danbooru/view.theme.php +++ b/themes/danbooru/view.theme.php @@ -4,14 +4,9 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - -class CustomViewPostTheme extends ViewPostTheme +class CustomViewImageTheme extends ViewImageTheme { - /** - * @param HTMLElement[] $editor_parts - */ - public function display_page(Image $image, array $editor_parts): void + public function display_page(Image $image, $editor_parts) { global $page; $page->set_heading(html_escape($image->get_tag_list())); @@ -42,21 +37,24 @@ class CustomViewPostTheme extends ViewPostTheme
    Filesize: $h_filesize
    Type: $h_type"; - if ($image->length != null) { + if ($image->length!=null) { $h_length = format_milliseconds($image->length); $html .= "
    Length: $h_length"; } if (!is_null($image->source)) { - $h_source = html_escape(make_http($image->source)); + $h_source = html_escape($image->source); + if (substr($image->source, 0, 7) != "http://" && substr($image->source, 0, 8) != "https://") { + $h_source = "http://" . $h_source; + } $html .= "
    Source: link"; } if (Extension::is_enabled(RatingsInfo::KEY)) { - if ($image['rating'] === null || $image['rating'] == "?") { - $image['rating'] = "?"; + if ($image->rating === null || $image->rating == "?") { + $image->rating = "?"; } - $h_rating = Ratings::rating_to_human($image['rating']); + $h_rating = Ratings::rating_to_human($image->rating); $html .= "
    Rating: $h_rating"; } diff --git a/themes/danbooru2/admin.theme.php b/themes/danbooru2/admin.theme.php index 56393863..1d8082f6 100644 --- a/themes/danbooru2/admin.theme.php +++ b/themes/danbooru2/admin.theme.php @@ -6,7 +6,7 @@ namespace Shimmie2; class CustomAdminPageTheme extends AdminPageTheme { - public function display_page(): void + public function display_page() { global $page; $page->disable_left(); diff --git a/themes/danbooru2/comment.theme.php b/themes/danbooru2/comment.theme.php index df45e10a..d61b0786 100644 --- a/themes/danbooru2/comment.theme.php +++ b/themes/danbooru2/comment.theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class CustomCommentListTheme extends CommentListTheme { - /** - * @param array $images - */ - public function display_comment_list(array $images, int $page_number, int $total_pages, bool $can_post): void + public function display_comment_list(array $images, int $page_number, int $total_pages, bool $can_post) { global $config, $page, $user; @@ -48,18 +45,19 @@ class CustomCommentListTheme extends CommentListTheme $un = $image->get_owner()->name; $t = ""; foreach ($image->get_tag_array() as $tag) { - $t .= "".html_escape($tag)." "; + $u_tag = url_escape($tag); + $t .= "".html_escape($tag)." "; } $p = autodate($image->posted); - $r = Extension::is_enabled(RatingsInfo::KEY) ? "Rating ".Ratings::rating_to_human($image['rating']) : ""; + $r = Extension::is_enabled(RatingsInfo::KEY) ? "Rating ".Ratings::rating_to_human($image->rating) : ""; $comment_html = "Date $p $s User $un $s $r
    Tags $t

     "; $comment_count = count($comments); if ($comment_limit > 0 && $comment_count > $comment_limit) { //$hidden = $comment_count - $comment_limit; $comment_html .= "

    showing $comment_limit of $comment_count comments

    "; - $comments = array_slice($comments, negative_int($comment_limit)); + $comments = array_slice($comments, -$comment_limit); } foreach ($comments as $comment) { $comment_html .= $this->comment_to_html($comment); @@ -88,13 +86,13 @@ class CustomCommentListTheme extends CommentListTheme } } - public function display_recent_comments(array $comments): void + public function display_recent_comments(array $comments) { // no recent comments in this theme } - protected function comment_to_html(Comment $comment, bool $trim = false): string + protected function comment_to_html(Comment $comment, bool $trim=false): string { global $user; @@ -112,7 +110,7 @@ class CustomCommentListTheme extends CommentListTheme $h_del = ""; if ($user->can(Permissions::DELETE_COMMENT)) { $comment_preview = substr(html_unescape($tfe->stripped), 0, 50); - $j_delete_confirm_message = json_encode_ex("Delete comment by {$comment->owner_name}:\n$comment_preview"); + $j_delete_confirm_message = json_encode("Delete comment by {$comment->owner_name}:\n$comment_preview"); $h_delete_script = html_escape("return confirm($j_delete_confirm_message);"); $h_delete_link = make_link("comment/delete/$i_comment_id/$i_image_id"); $h_del = " - Del"; diff --git a/themes/danbooru2/ext_manager.theme.php b/themes/danbooru2/ext_manager.theme.php index 1d283157..bdc7b0c2 100644 --- a/themes/danbooru2/ext_manager.theme.php +++ b/themes/danbooru2/ext_manager.theme.php @@ -6,13 +6,13 @@ namespace Shimmie2; class CustomExtManagerTheme extends ExtManagerTheme { - public function display_table(Page $page, array $extensions, bool $editable): void + public function display_table(Page $page, array $extensions, bool $editable) { $page->disable_left(); parent::display_table($page, $extensions, $editable); } - public function display_doc(Page $page, ExtensionInfo $info): void + public function display_doc(Page $page, ExtensionInfo $info) { $page->disable_left(); parent::display_doc($page, $info); diff --git a/themes/danbooru2/index.theme.php b/themes/danbooru2/index.theme.php index 07ecfe5d..51bf4473 100644 --- a/themes/danbooru2/index.theme.php +++ b/themes/danbooru2/index.theme.php @@ -7,9 +7,9 @@ namespace Shimmie2; class CustomIndexTheme extends IndexTheme { /** - * @param Image[] $images + * #param Image[] $images */ - public function display_page(Page $page, array $images): void + public function display_page(Page $page, array $images) { $this->display_shortwiki($page); @@ -26,7 +26,7 @@ class CustomIndexTheme extends IndexTheme } /** - * @param string[] $search_terms + * #param string[] $search_terms */ protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string { @@ -36,13 +36,13 @@ class CustomIndexTheme extends IndexTheme

    - +
    "; } /** - * @param Image[] $images + * #param Image[] $images */ protected function build_table(array $images, ?string $query): string { diff --git a/themes/danbooru2/page.class.php b/themes/danbooru2/page.class.php index d714cbda..d96d84fd 100644 --- a/themes/danbooru2/page.class.php +++ b/themes/danbooru2/page.class.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Shimmie2; -use MicroHTML\HTMLElement; - /** * Name: Danbooru 2 Theme * Author: Bzchan , updated by Daniel Oaks @@ -51,7 +49,13 @@ Tips class Page extends BasePage { - public function body_html(): string + public bool $left_enabled = true; + public function disable_left() + { + $this->left_enabled = false; + } + + public function render() { global $config; @@ -124,9 +128,14 @@ class Page extends BasePage } $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); $footer_html = $this->footer_html(); - return << + + $head_html +
    $title_link
    Tags + +
    Source Link + ".($user->can("edit_image_source") ? " +
    $f_source
    + + " : " +
    $f_source
    + ")." +
    + + + +
    Name
    Password
    + + "; + if ($config->get_bool("login_signup_enabled")) { + $html .= "Create Account"; + } + $page->add_block(new Block("Login", $html, "head", 90)); + $page->add_block(new Block("Login", $html, "left", 15)); + } + + public function display_signup_page(Page $page) + { + global $config; + $tac = $config->get_string("login_tac", ""); + + if ($config->get_bool("login_tac_bbcode")) { + $tac = send_event(new TextFormattingEvent($tac))->formatted; + } + + $form = SHM_SIMPLE_FORM( + "user_admin/create", + TABLE( + ["class"=>"form"], + TBODY( + TR( + TH("Name"), + TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) + ), + TR( + TH("Password"), + TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) + ), + TR( + TH(rawHTML("Repeat Password")), + TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) + ), + TR( + TH(rawHTML("Email")), + TD(INPUT(["type"=>'email', "name"=>'email', "required"=>true])) + ), + TR( + TD(["colspan"=>"2"], rawHTML(captcha_get_html())) + ), + ), + TFOOT( + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) + ) + ) + ); + + $html = emptyHTML( + $tac ? P(rawHTML($tac)) : null, + $form + ); + + $page->set_title("Create Account"); + $page->set_heading("Create Account"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Signup", $html)); + } + + public function display_user_creator() + { + global $page; + + $form = SHM_SIMPLE_FORM( + "user_admin/create_other", + TABLE( + ["class"=>"form"], + TBODY( + TR( + TH("Name"), + TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true])) + ), + TR( + TH("Password"), + TD(INPUT(["type"=>'password', "name"=>'pass1', "required"=>true])) + ), + TR( + TH(rawHTML("Repeat Password")), + TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true])) + ), + TR( + TH(rawHTML("Email")), + TD(INPUT(["type"=>'email', "name"=>'email'])) + ), + TR( + TD(["colspan"=>2], rawHTML("(Email is optional for admin-created accounts)")), + ), + ), + TFOOT( + TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Create Account"]))) + ) + ) + ); + $page->add_block(new Block("Create User", (string)$form, "main", 75)); + } +} diff --git a/themes/warm/page.class.php b/themes/warm/page.class.php index 7e38aa5e..3ffcbab4 100644 --- a/themes/warm/page.class.php +++ b/themes/warm/page.class.php @@ -6,7 +6,7 @@ namespace Shimmie2; class Page extends BasePage { - public function body_html(): string + public function render() { global $config; @@ -40,9 +40,14 @@ class Page extends BasePage } $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $head_html = $this->head_html(); $footer_html = $this->footer_html(); - return << + + $head_html +

    @@ -65,6 +70,8 @@ class Page extends BasePage
    $footer_html
    + + EOD; } } diff --git a/themes/warm/style.css b/themes/warm/style.css index ae5e0c17..616010ee 100644 --- a/themes/warm/style.css +++ b/themes/warm/style.css @@ -3,12 +3,10 @@ * things common to all pages * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -:root { - font-family: "Arial", sans-serif; - font-size: 14px; -} BODY { background: url(bg.png); + font-family: "Arial", sans-serif; + font-size: 14px; margin: 0; } HEADER { @@ -20,7 +18,7 @@ HEADER { text-align: center; } H1 { - font-size: 5rem; + font-size: 5em; margin: 0; padding: 0; } @@ -40,7 +38,16 @@ TD { } CODE { background: #DEDEDE; - font-size: 0.8rem; + font-size: 0.8em; +} +#subtitle { + width: 256px; + font-size: 0.75em; + margin: -16px auto auto; + text-align: center; + border: 1px solid black; + border-top: none; + background: #DDD; } TABLE.zebra {border-spacing: 0; border: 1px solid #B89F7C; } @@ -54,7 +61,7 @@ TABLE.zebra TR:nth-child(even) {background: #DABC92;} FOOTER { clear: both; padding: 8px; - font-size: 0.7rem; + font-size: 0.7em; text-align: center; border-top: 1px solid #B89F7C; background: #FCD9A9; @@ -91,7 +98,7 @@ NAV { margin-left: 16px; } NAV .blockbody { - font-size: 0.85rem; + font-size: 0.85em; text-align: center; } NAV TABLE { @@ -175,7 +182,12 @@ ARTICLE TABLE { } .thumb { + width: 226px; + display: inline-block; + zoom: 1; /* ie6 */ + *display: inline; /* ie6 */ text-align: center; + margin-bottom: 8px; } .thumb IMG { border: 1px solid #B89F7C; diff --git a/themes/warm/user.theme.php b/themes/warm/user.theme.php index c5239876..a58e9ceb 100644 --- a/themes/warm/user.theme.php +++ b/themes/warm/user.theme.php @@ -6,10 +6,7 @@ namespace Shimmie2; class CustomUserPageTheme extends UserPageTheme { - /** - * @param array $parts - */ - public function display_user_block(Page $page, User $user, array $parts): void + public function display_user_block(Page $page, User $user, $parts) { $h_name = html_escape($user->name); $html = " | "; @@ -19,7 +16,7 @@ class CustomUserPageTheme extends UserPageTheme $page->add_block(new Block("Logged in as $h_name", $html, "head", 90)); } - public function display_login_block(Page $page): void + public function display_login_block(Page $page) { global $config; $html = "