diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fb1ad12f3..4d964d6a8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,7 +23,7 @@ }, "updateContentCommand": "composer install && cp ./.devcontainer/bash_history /root/.bash_history", - "postCreateCommand": "./.docker/entrypoint.sh php ./.docker/run.php", + "postStartCommand": "./.docker/entrypoint.sh php ./.docker/run.php", "containerEnv": { "UID": "2000", "GID": "2000", diff --git a/.github/get-tags.py b/.github/get-tags.py index a8ded364e..f2f7402e3 100755 --- a/.github/get-tags.py +++ b/.github/get-tags.py @@ -7,14 +7,17 @@ describe = check_output(["git", "describe", "--tags"], text=True).strip() tag = describe.split("-")[0][1:] a, b, c = tag.split(".") +docker_username = sys.argv[1] +docker_image = sys.argv[2] if len(sys.argv) > 2 else "shimmie2" +image_name = f"{docker_username}/{docker_image}" if branch == "main": - print("tags=latest") + print(f"tags={image_name}:latest") elif branch.startswith("branch-2."): if "-" in describe: - print(f"tags={a},{a}.{b}") + print(f"tags={image_name}:{a},{image_name}:{a}.{b}") else: - print(f"tags={a},{a}.{b},{a}.{b}.{c}") + print(f"tags={image_name}:{a},{image_name}:{a}.{b},{image_name}:{a}.{b}.{c}") else: print("Only run from main or branch-2.X") sys.exit(1) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f157646a4..28b1cd448 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ on: jobs: merge-master-to-main: if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set Git config diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db4156849..6e8868d83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: jobs: build: name: Create Release - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3a3a939b6..adf78e569 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ on: jobs: format: name: Format - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -29,16 +29,12 @@ jobs: run: composer validate - name: Install PHP dependencies run: composer install --prefer-dist --no-progress - - name: Set up PHP - uses: shivammathur/setup-php@master - with: - php-version: 8.3 - name: Format run: composer format && git diff --exit-code static: name: Static Analysis - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -61,7 +57,7 @@ jobs: matrix: php: ['8.3'] database: ['pgsql', 'mysql', 'sqlite'] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout current uses: actions/checkout@v4 @@ -78,7 +74,7 @@ jobs: vendor key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }} - name: Set up PHP - uses: shivammathur/setup-php@master + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - name: Set up database @@ -110,9 +106,9 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.2', '8.3'] database: ['pgsql', 'mysql', 'sqlite'] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -125,7 +121,7 @@ jobs: vendor key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }} - name: Set up PHP - uses: shivammathur/setup-php@master + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: pcov @@ -144,7 +140,7 @@ jobs: publish: name: Publish - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: - format - static @@ -162,14 +158,25 @@ jobs: run: | echo "BUILD_TIME=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_ENV echo "BUILD_HASH=$GITHUB_SHA" >> $GITHUB_ENV - ./.github/get-tags.py | tee -a $GITHUB_OUTPUT - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@main + ./.github/get-tags.py ${{ secrets.DOCKER_USERNAME }} ${{ secrets.DOCKER_IMAGE }} | tee -a $GITHUB_OUTPUT + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker registry + uses: docker/login-action@v3 with: - name: shish2k/shimmie2 username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - cache: ${{ github.event_name != 'schedule' }} - buildoptions: "--build-arg RUN_TESTS=false" - buildargs: BUILD_TIME,BUILD_HASH + - name: Build and push to registry + uses: docker/build-push-action@v6 + with: + push: true + platforms: | + linux/amd64 + linux/arm64 + build-args: | + BUILD_TIME=${{ env.BUILD_TIME }} + BUILD_HASH=${{ env.BUILD_HASH }} + no-cache: ${{ github.event_name == 'schedule' }} tags: "${{ steps.get-vars.outputs.tags }}" diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 9fd655e94..7ae8f8692 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -8,11 +8,13 @@ ; $_phpcs_config = new PhpCsFixer\Config(); -return $_phpcs_config->setRules([ - '@PSR12' => true, - //'strict_param' => true, - 'array_syntax' => ['syntax' => 'short'], - ]) - ->setFinder($_phpcs_finder) +return $_phpcs_config + ->setRules([ + '@PSR12' => true, + //'strict_param' => true, + 'array_syntax' => ['syntax' => 'short'], + ]) + ->setFinder($_phpcs_finder) ->setCacheFile("data/php-cs-fixer.cache") + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) ; diff --git a/Dockerfile b/Dockerfile index ca235d007..2c51317b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,6 @@ ARG PHP_VERSION=8.2 # base # ├── dev-tools # │ ├── build -# │ │ └── tests # │ └── devcontainer # └── run (copies built artifacts out of build) @@ -22,7 +21,7 @@ RUN apt update && \ php${PHP_VERSION}-gd php${PHP_VERSION}-zip php${PHP_VERSION}-xml php${PHP_VERSION}-mbstring php${PHP_VERSION}-curl \ php${PHP_VERSION}-pgsql php${PHP_VERSION}-mysql php${PHP_VERSION}-sqlite3 \ php${PHP_VERSION}-memcached \ - curl imagemagick zip unzip unit unit-php && \ + curl rsync imagemagick zip unzip unit unit-php && \ rm -rf /var/lib/apt/lists/* # Install dev packages @@ -43,19 +42,6 @@ WORKDIR /app RUN composer install --no-dev --no-progress COPY . /app/ -# Tests in their own image. -# Re-run composer install to get dev dependencies -FROM build AS tests -RUN composer install --no-progress -COPY . /app/ -ARG RUN_TESTS=true -RUN [ $RUN_TESTS = false ] || (\ - echo '=== Installing ===' && mkdir -p data/config && INSTALL_DSN="sqlite:data/shimmie.sqlite" php index.php && \ - echo '=== Smoke Test ===' && php index.php get-page /post/list && \ - echo '=== Unit Tests ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml && \ - echo '=== Coverage ===' && ./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \ - echo '=== Cleaning ===' && rm -rf data) - # Devcontainer target # Contains all of the build and debug tools, but no code, since # that's mounted from the host diff --git a/composer.json b/composer.json index 1ee778d75..c8da5a2c9 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "config": { "platform": { - "php": "8.1.0" + "php": "8.2.0" } }, @@ -31,11 +31,10 @@ ], "require" : { - "php" : "^8.1", + "php" : "^8.2", "ext-pdo": "*", "ext-json": "*", "ext-fileinfo": "*", - "flexihash/flexihash": "^2.0", "ifixit/php-akismet": "^1.0", "google/recaptcha": "^1.1", "shish/eventtracer-php": "^2.0", @@ -57,9 +56,9 @@ }, "require-dev" : { - "phpunit/phpunit" : "10.5.3", - "friendsofphp/php-cs-fixer" : "3.41.1", - "phpstan/phpstan": "1.10.50", + "phpunit/phpunit" : "^11.0", + "friendsofphp/php-cs-fixer" : "^3.64", + "phpstan/phpstan": "^1.12", "thecodingmachine/phpstan-safe-rule": "^1.2" }, "suggest": { diff --git a/composer.lock b/composer.lock index 2232453d1..6bde1df20 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "88c9d4afc84972e9540fac69dec768eb", + "content-hash": "ea210c5a35b525e8872550bc7c1cc3a8", "packages": [ { "name": "aws/aws-crt-php", - "version": "v1.2.4", + "version": "v1.2.6", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2" + "reference": "a63485b65b6b3367039306496d49737cf1995408" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2", - "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/a63485b65b6b3367039306496d49737cf1995408", + "reference": "a63485b65b6b3367039306496d49737cf1995408", "shasum": "" }, "require": { @@ -56,22 +56,22 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.6" }, - "time": "2023-11-08T00:42:13+00:00" + "time": "2024-06-13T17:21:28+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.300.4", + "version": "3.321.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "27d59c22c121ce9c0041c563dc9d7270e180925c" + "reference": "c04f8f30891cee8480c132778cd4cc486467e77a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/27d59c22c121ce9c0041c563dc9d7270e180925c", - "reference": "27d59c22c121ce9c0041c563dc9d7270e180925c", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c04f8f30891cee8480c132778cd4cc486467e77a", + "reference": "c04f8f30891cee8480c132778cd4cc486467e77a", "shasum": "" }, "require": { @@ -124,7 +124,10 @@ ], "psr-4": { "Aws\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/data/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -151,9 +154,9 @@ "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.300.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.321.2" }, - "time": "2024-02-23T19:10:30+00:00" + "time": "2024-08-30T18:14:40+00:00" }, { "name": "bower-asset/jquery", @@ -319,75 +322,18 @@ }, "time": "2023-11-17T15:01:25+00:00" }, - { - "name": "flexihash/flexihash", - "version": "v2.0.2", - "source": { - "type": "git", - "url": "https://github.com/pda/flexihash.git", - "reference": "497ba5782606d998f8ab0b4e5942e3a799bec018" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pda/flexihash/zipball/497ba5782606d998f8ab0b4e5942e3a799bec018", - "reference": "497ba5782606d998f8ab0b4e5942e3a799bec018", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8", - "satooshi/php-coveralls": "~1.0", - "squizlabs/php_codesniffer": "^2.3", - "symfony/config": "^2.0.0", - "symfony/console": "^2.0.0", - "symfony/filesystem": "^2.0.0", - "symfony/stopwatch": "^2.0.0", - "symfony/yaml": "^2.0.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Flexihash\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paul Annesley", - "email": "paul@annesley.cc", - "homepage": "http://paul.annesley.cc" - }, - { - "name": "Dom Morgan", - "email": "dom@d3r.com", - "homepage": "https://d3r.com" - } - ], - "description": "Flexihash is a small PHP library which implements consistent hashing", - "homepage": "https://github.com/pda/flexihash", - "support": { - "issues": "https://github.com/pda/flexihash/issues", - "source": "https://github.com/pda/flexihash/tree/v2.0.2" - }, - "time": "2016-04-22T21:03:23+00:00" - }, { "name": "google/recaptcha", - "version": "dev-master", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/google/recaptcha.git", - "reference": "6ffa193021aa0e369a3c5b3909de2b4ed97ac359" + "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/google/recaptcha/zipball/6ffa193021aa0e369a3c5b3909de2b4ed97ac359", - "reference": "6ffa193021aa0e369a3c5b3909de2b4ed97ac359", + "url": "https://api.github.com/repos/google/recaptcha/zipball/d59a801e98a4e9174814a6d71bbc268dff1202df", + "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df", "shasum": "" }, "require": { @@ -398,7 +344,6 @@ "php-coveralls/php-coveralls": "^2.5", "phpunit/phpunit": "^10" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -427,26 +372,26 @@ "issues": "https://github.com/google/recaptcha/issues", "source": "https://github.com/google/recaptcha" }, - "time": "2023-02-20T17:27:30+00:00" + "time": "2023-02-18T17:41:46+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.9.x-dev", + "version": "7.9.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -457,9 +402,9 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -537,7 +482,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" }, "funding": [ { @@ -553,20 +498,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:35:24+00:00" + "time": "2024-07-24T11:22:20+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.x-dev", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", "shasum": "" }, "require": { @@ -574,9 +519,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, - "default-branch": true, "type": "library", "extra": { "bamarni-bin": { @@ -621,7 +565,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" + "source": "https://github.com/guzzle/promises/tree/2.0.3" }, "funding": [ { @@ -637,20 +581,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:19:20+00:00" + "time": "2024-07-18T10:29:17+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.x-dev", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { @@ -665,13 +609,12 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, - "default-branch": true, "type": "library", "extra": { "bamarni-bin": { @@ -738,7 +681,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.7.0" }, "funding": [ { @@ -754,7 +697,7 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:05:35+00:00" + "time": "2024-07-18T11:15:46+00:00" }, { "name": "ifixit/php-akismet", @@ -768,16 +711,16 @@ }, { "name": "mtdowling/jmespath.php", - "version": "dev-master", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "b243cacd2a9803b4cbc259246aa5081208238c10" + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/b243cacd2a9803b4cbc259246aa5081208238c10", - "reference": "b243cacd2a9803b4cbc259246aa5081208238c10", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b", "shasum": "" }, "require": { @@ -788,7 +731,6 @@ "composer/xdebug-handler": "^3.0.3", "phpunit/phpunit": "^8.5.33" }, - "default-branch": true, "bin": [ "bin/jp.php" ], @@ -829,9 +771,9 @@ ], "support": { "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/master" + "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0" }, - "time": "2023-11-30T16:26:47+00:00" + "time": "2023-08-25T10:54:48+00:00" }, { "name": "naroga/redis-cache", @@ -883,16 +825,16 @@ }, { "name": "predis/predis", - "version": "v1.x-dev", + "version": "v1.1.10", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "deee2b6d605eb6401446f6f6354414ab7571a5a0" + "reference": "a2fb02d738bedadcffdbb07efa3a5e7bd57f8d6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/deee2b6d605eb6401446f6f6354414ab7571a5a0", - "reference": "deee2b6d605eb6401446f6f6354414ab7571a5a0", + "url": "https://api.github.com/repos/predis/predis/zipball/a2fb02d738bedadcffdbb07efa3a5e7bd57f8d6e", + "reference": "a2fb02d738bedadcffdbb07efa3a5e7bd57f8d6e", "shasum": "" }, "require": { @@ -937,7 +879,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v1.x" + "source": "https://github.com/predis/predis/tree/v1.1.10" }, "funding": [ { @@ -945,26 +887,25 @@ "type": "github" } ], - "time": "2023-09-19T16:11:21+00:00" + "time": "2022-01-05T17:46:08+00:00" }, { "name": "psr/container", - "version": "dev-master", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "707984727bd5b2b670e59559d3ed2500240cf875" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/707984727bd5b2b670e59559d3ed2500240cf875", - "reference": "707984727bd5b2b670e59559d3ed2500240cf875", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { "php": ">=7.4.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -997,13 +938,13 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2023-09-22T11:11:30+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/http-client", - "version": "dev-master", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", @@ -1019,7 +960,6 @@ "php": "^7.0 || ^8.0", "psr/http-message": "^1.0 || ^2.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1056,23 +996,22 @@ }, { "name": "psr/http-factory", - "version": "dev-master", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/7037f4b0950474e9d1350e8df89b15f1842085f6", - "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1108,11 +1047,11 @@ "support": { "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-09-22T11:16:44+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", - "version": "dev-master", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", @@ -1127,7 +1066,6 @@ "require": { "php": "^7.2 || ^8.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1261,16 +1199,16 @@ }, { "name": "sabre/cache", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sabre-io/cache.git", - "reference": "a843741b85025d8674bf4713121cae60172e6f86" + "reference": "880048a8913094ee53c953f7e849cf2fb1c54815" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/cache/zipball/a843741b85025d8674bf4713121cae60172e6f86", - "reference": "a843741b85025d8674bf4713121cae60172e6f86", + "url": "https://api.github.com/repos/sabre-io/cache/zipball/880048a8913094ee53c953f7e849cf2fb1c54815", + "reference": "880048a8913094ee53c953f7e849cf2fb1c54815", "shasum": "" }, "require": { @@ -1281,11 +1219,11 @@ "psr/simple-cache-implementation": "~1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.14.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-phpunit": "^1.3", - "phpstan/phpstan-strict-rules": "^1.4", + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "^9.6" }, "type": "library", @@ -1323,30 +1261,30 @@ "issues": "https://github.com/sabre-io/cache/issues", "source": "https://github.com/fruux/sabre-skel" }, - "time": "2023-02-09T23:47:10+00:00" + "time": "2024-08-27T16:56:06+00:00" }, { "name": "shish/eventtracer-php", - "version": "v2.1.0", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/shish/eventtracer-php.git", - "reference": "5dfe2c090c8b7df772e982520c36f44b33ead035" + "reference": "b65488e09a014d64c4df46fb852adf9be8e597bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shish/eventtracer-php/zipball/5dfe2c090c8b7df772e982520c36f44b33ead035", - "reference": "5dfe2c090c8b7df772e982520c36f44b33ead035", + "url": "https://api.github.com/repos/shish/eventtracer-php/zipball/b65488e09a014d64c4df46fb852adf9be8e597bb", + "reference": "b65488e09a014d64c4df46fb852adf9be8e597bb", "shasum": "" }, "require": { "ext-json": "*", - "php": "^8.0" + "php": "^8.2" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.12", - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^9.0" + "friendsofphp/php-cs-fixer": "^3.64", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^11.0" }, "type": "library", "autoload": { @@ -1370,32 +1308,32 @@ "homepage": "https://github.com/shish/eventtracer-php", "support": { "issues": "https://github.com/shish/eventtracer-php/issues", - "source": "https://github.com/shish/eventtracer-php/tree/v2.1.0" + "source": "https://github.com/shish/eventtracer-php/tree/v2.1.1" }, - "time": "2023-02-04T12:26:41+00:00" + "time": "2024-08-31T21:49:49+00:00" }, { "name": "shish/ffsphp", - "version": "v1.3.2", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/shish/ffsphp.git", - "reference": "d69223f4317de302b6cd485d0a43709788dd6f69" + "reference": "c386822954dd56fe513d75b22ee651f98a201f6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shish/ffsphp/zipball/d69223f4317de302b6cd485d0a43709788dd6f69", - "reference": "d69223f4317de302b6cd485d0a43709788dd6f69", + "url": "https://api.github.com/repos/shish/ffsphp/zipball/c386822954dd56fe513d75b22ee651f98a201f6e", + "reference": "c386822954dd56fe513d75b22ee651f98a201f6e", "shasum": "" }, "require": { "ext-pdo": "*", - "php": "^8.1" + "php": "^8.2" }, "require-dev": { - "friendsofphp/php-cs-fixer": "3.41.1", - "phpstan/phpstan": "1.10.50", - "phpunit/phpunit": "10.5.3" + "friendsofphp/php-cs-fixer": "^3.64", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^11.0" }, "type": "library", "autoload": { @@ -1419,9 +1357,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.3.3" }, - "time": "2024-01-04T18:38:54+00:00" + "time": "2024-08-31T23:18:43+00:00" }, { "name": "shish/gqla", @@ -1642,12 +1580,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78" + "reference": "42686880adaacdad1835ee8fc2a9ec5b7bd63998" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0d9e4eb5ad413075624378f474c4167ea202de78", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78", + "url": "https://api.github.com/repos/symfony/console/zipball/42686880adaacdad1835ee8fc2a9ec5b7bd63998", + "reference": "42686880adaacdad1835ee8fc2a9ec5b7bd63998", "shasum": "" }, "require": { @@ -1728,26 +1666,25 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:10+00:00" + "time": "2024-08-15T22:48:29+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "dev-main", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/2c438b99bb2753c1628c1e6f523991edea5b03a4", - "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { "php": ">=8.1" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1780,7 +1717,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/main" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -1796,20 +1733,20 @@ "type": "tidelift" } ], - "time": "2024-01-02T14:07:37+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "1.x-dev", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -1821,7 +1758,6 @@ "suggest": { "ext-ctype": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -1860,7 +1796,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -1876,20 +1812,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "1.x-dev", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -1898,7 +1834,6 @@ "suggest": { "ext-intl": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -1939,7 +1874,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -1955,20 +1890,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "1.x-dev", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -1977,7 +1912,6 @@ "suggest": { "ext-intl": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -2021,7 +1955,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -2037,20 +1971,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "1.x-dev", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -2062,7 +1996,6 @@ "suggest": { "ext-mbstring": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -2102,7 +2035,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -2118,30 +2051,30 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/service-contracts", - "version": "dev-main", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", - "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2185,7 +2118,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/main" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -2201,24 +2134,24 @@ "type": "tidelift" } ], - "time": "2024-01-02T14:07:37+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/string", - "version": "6.4.x-dev", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9" + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", @@ -2228,11 +2161,12 @@ "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/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -2271,7 +2205,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/6.4" + "source": "https://github.com/symfony/string/tree/v7.1.4" }, "funding": [ { @@ -2287,7 +2221,7 @@ "type": "tidelift" } ], - "time": "2024-02-01T13:16:41+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "thecodingmachine/safe", @@ -2430,16 +2364,16 @@ }, { "name": "webonyx/graphql-php", - "version": "v15.9.1", + "version": "v15.13.0", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "d6c965ecbd78cd5260ebc083978562f8c9409d63" + "reference": "b3b8c5bba097b0db95098fadb63e8980e184a03b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d6c965ecbd78cd5260ebc083978562f8c9409d63", - "reference": "d6c965ecbd78cd5260ebc083978562f8c9409d63", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/b3b8c5bba097b0db95098fadb63e8980e184a03b", + "reference": "b3b8c5bba097b0db95098fadb63e8980e184a03b", "shasum": "" }, "require": { @@ -2452,19 +2386,19 @@ "amphp/http-server": "^2.1", "dms/phpunit-arraysubset-asserts": "dev-master", "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "3.48.0", + "friendsofphp/php-cs-fixer": "3.63.2", "mll-lab/php-cs-fixer-config": "^5", "nyholm/psr7": "^1.5", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "1.10.57", - "phpstan/phpstan-phpunit": "1.3.15", - "phpstan/phpstan-strict-rules": "1.5.2", - "phpunit/phpunit": "^9.5 || ^10", + "phpstan/phpstan": "1.12.0", + "phpstan/phpstan-phpunit": "1.4.0", + "phpstan/phpstan-strict-rules": "1.6.0", + "phpunit/phpunit": "^9.5 || ^10.5.21", "psr/http-message": "^1 || ^2", "react/http": "^1.6", "react/promise": "^2.0 || ^3.0", - "rector/rector": "^0.19", + "rector/rector": "^1.0", "symfony/polyfill-php81": "^1.23", "symfony/var-exporter": "^5 || ^6 || ^7", "thecodingmachine/safe": "^1.3 || ^2" @@ -2492,7 +2426,7 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v15.9.1" + "source": "https://github.com/webonyx/graphql-php/tree/v15.13.0" }, "funding": [ { @@ -2500,37 +2434,108 @@ "type": "open_collective" } ], - "time": "2024-01-25T09:10:40+00:00" + "time": "2024-08-29T10:55:21+00:00" } ], "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, { "name": "composer/pcre", - "version": "dev-main", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, "require-dev": { - "phpstan/phpstan": "^1.3", + "phpstan/phpstan": "^1.11.10", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" + "phpunit/phpunit": "^8 || ^9" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] } }, "autoload": { @@ -2558,7 +2563,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.3.1" }, "funding": [ { @@ -2574,20 +2579,20 @@ "type": "tidelift" } ], - "time": "2023-10-11T07:11:09+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "composer/semver", - "version": "dev-main", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "1d09200268e7d1052ded8e5da9c73c96a63d18f5" + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/1d09200268e7d1052ded8e5da9c73c96a63d18f5", - "reference": "1d09200268e7d1052ded8e5da9c73c96a63d18f5", + "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", "shasum": "" }, "require": { @@ -2597,7 +2602,6 @@ "phpstan/phpstan": "^1.4", "symfony/phpunit-bridge": "^4.2 || ^5" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2640,7 +2644,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/main" + "source": "https://github.com/composer/semver/tree/3.4.2" }, "funding": [ { @@ -2656,20 +2660,20 @@ "type": "tidelift" } ], - "time": "2023-08-31T12:20:31+00:00" + "time": "2024-07-12T11:35:52+00:00" }, { "name": "composer/xdebug-handler", - "version": "3.0.3", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -2680,7 +2684,7 @@ "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -2704,9 +2708,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -2722,29 +2726,145 @@ "type": "tidelift" } ], - "time": "2022-02-25T21:32:43+00:00" + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.41.1", + "version": "v3.64.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "8b6ae8dcbaf23f09680643ab832a4a3a260265f6" + "reference": "58dd9c931c785a79739310aef5178928305ffa67" }, "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/58dd9c931c785a79739310aef5178928305ffa67", + "reference": "58dd9c931c785a79739310aef5178928305ffa67", "shasum": "" }, "require": { + "clue/ndjson-react": "^1.0", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", "ext-json": "*", "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.0", "php": "^7.4 || ^8.0", - "sebastian/diff": "^4.0 || ^5.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.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", @@ -2757,16 +2877,17 @@ "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "facile-it/paraunit": "^1.3 || ^2.0", + "facile-it/paraunit": "^1.3 || ^2.3", + "infection/infection": "^0.29.5", "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^2.1", "mikey179/vfsstream": "^1.6.11", "php-coveralls/php-coveralls": "^2.7", "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", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", + "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "suggest": { @@ -2780,7 +2901,10 @@ "autoload": { "psr-4": { "PhpCsFixer\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2805,7 +2929,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.64.0" }, "funding": [ { @@ -2813,20 +2937,20 @@ "type": "github" } ], - "time": "2023-12-10T19:59:27+00:00" + "time": "2024-08-30T23:09:38+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.x-dev", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "2f5294676c802a62b0549f6bc8983f14294ce369" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/2f5294676c802a62b0549f6bc8983f14294ce369", - "reference": "2f5294676c802a62b0549f6bc8983f14294ce369", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -2842,7 +2966,6 @@ "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, - "default-branch": true, "type": "library", "autoload": { "files": [ @@ -2866,7 +2989,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.x" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -2874,20 +2997,20 @@ "type": "tidelift" } ], - "time": "2024-02-10T11:10:03+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "dev-master", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "af14fdb282aa0e288bfe7eb3b57893484b68dc27" + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/af14fdb282aa0e288bfe7eb3b57893484b68dc27", - "reference": "af14fdb282aa0e288bfe7eb3b57893484b68dc27", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "shasum": "" }, "require": { @@ -2898,9 +3021,8 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, - "default-branch": true, "bin": [ "bin/php-parse" ], @@ -2931,22 +3053,22 @@ ], "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/v5.1.0" }, - "time": "2024-02-21T20:13:45+00:00" + "time": "2024-07-01T20:03:41+00:00" }, { "name": "phar-io/manifest", - "version": "dev-master", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "67729272c564ab9f953c81f48db44e8b1cb1e1c3" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "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/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { @@ -2955,9 +3077,8 @@ "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", "extra": { "branch-alias": { @@ -2993,7 +3114,7 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, "funding": [ { @@ -3001,7 +3122,7 @@ "type": "github" } ], - "time": "2023-06-01T14:19:47+00:00" + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -3056,16 +3177,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.50", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" + "reference": "384af967d35b2162f69526c7276acadce534d0e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/384af967d35b2162f69526c7276acadce534d0e1", + "reference": "384af967d35b2162f69526c7276acadce534d0e1", "shasum": "" }, "require": { @@ -3108,45 +3229,41 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2023-12-13T10:59:42+00:00" + "time": "2024-08-27T09:18:05+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.x-dev", + "version": "11.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "68f0085d6408a585bf3335cd26b22c0f220c4458" + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/68f0085d6408a585bf3335cd26b22c0f220c4458", - "reference": "68f0085d6408a585bf3335cd26b22c0f220c4458", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45", + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45", "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", - "theseer/tokenizer": "^1.2.0" + "nikic/php-parser": "^5.1.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^10.1" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3155,7 +3272,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -3184,7 +3301,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/11.0.6" }, "funding": [ { @@ -3192,32 +3309,32 @@ "type": "github" } ], - "time": "2024-01-30T13:35:06+00:00" + "time": "2024-08-22T04:37:56+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "4.1.x-dev", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "e7bcadc84d5d1c7ae8c688a38045980303b0a347" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/e7bcadc84d5d1c7ae8c688a38045980303b0a347", - "reference": "e7bcadc84d5d1c7ae8c688a38045980303b0a347", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.1-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3245,7 +3362,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/5.1.0" }, "funding": [ { @@ -3253,28 +3370,28 @@ "type": "github" } ], - "time": "2024-01-30T14:02:58+00:00" + "time": "2024-08-27T05:02:59+00:00" }, { "name": "phpunit/php-invoker", - "version": "4.0.x-dev", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "8bccdd7912fb3c12a8b00f95ca23a5f19133db5d" + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/8bccdd7912fb3c12a8b00f95ca23a5f19133db5d", - "reference": "8bccdd7912fb3c12a8b00f95ca23a5f19133db5d", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-pcntl": "*" @@ -3282,7 +3399,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3309,7 +3426,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/5.0.1" }, "funding": [ { @@ -3317,32 +3434,32 @@ "type": "github" } ], - "time": "2024-01-30T14:03:19+00:00" + "time": "2024-07-03T05:07:44+00:00" }, { "name": "phpunit/php-text-template", - "version": "3.0.x-dev", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "cd963e704878c8a5733dfc5384a96dd68d56026d" + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/cd963e704878c8a5733dfc5384a96dd68d56026d", - "reference": "cd963e704878c8a5733dfc5384a96dd68d56026d", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3369,7 +3486,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/4.0.1" }, "funding": [ { @@ -3377,32 +3494,32 @@ "type": "github" } ], - "time": "2024-01-30T14:04:28+00:00" + "time": "2024-07-03T05:08:43+00:00" }, { "name": "phpunit/php-timer", - "version": "6.0.x-dev", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "d27f17db6559d450b3110da27d67a8bb9df620fa" + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/d27f17db6559d450b3110da27d67a8bb9df620fa", - "reference": "d27f17db6559d450b3110da27d67a8bb9df620fa", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3429,7 +3546,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/7.0.1" }, "funding": [ { @@ -3437,20 +3554,20 @@ "type": "github" } ], - "time": "2024-01-30T14:05:27+00:00" + "time": "2024-07-03T05:09:35+00:00" }, { "name": "phpunit/phpunit", - "version": "10.5.3", + "version": "11.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6fce887c71076a73f32fd3e0774a6833fc5c7f19" + "reference": "fe179875ef0c14e90b75617002767eae0a742641" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6fce887c71076a73f32fd3e0774a6833fc5c7f19", - "reference": "6fce887c71076a73f32fd3e0774a6833fc5c7f19", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fe179875ef0c14e90b75617002767eae0a742641", + "reference": "fe179875ef0c14e90b75617002767eae0a742641", "shasum": "" }, "require": { @@ -3460,26 +3577,25 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "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" + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.5", + "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.1", + "sebastian/comparator": "^6.0.2", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.1.3", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.0.1", + "sebastian/version": "^5.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -3490,7 +3606,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.5-dev" + "dev-main": "11.3-dev" } }, "autoload": { @@ -3522,7 +3638,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/11.3.1" }, "funding": [ { @@ -3538,29 +3654,25 @@ "type": "tidelift" } ], - "time": "2023-12-13T07:25:23+00:00" + "time": "2024-08-13T06:14:23+00:00" }, { "name": "psr/event-dispatcher", - "version": "dev-master", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "977ffcf551e3bfb73d90aac3e8e1583fd8d2f89a" + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "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/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { "php": ">=7.2.0" }, - "suggest": { - "fig/event-dispatcher-util": "Provides some useful PSR-14 utilities" - }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3579,7 +3691,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Standard interfaces for event handling.", @@ -3589,28 +3701,28 @@ "psr-14" ], "support": { - "source": "https://github.com/php-fig/event-dispatcher" + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2023-09-22T11:10:57+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { "name": "psr/log", - "version": "dev-master", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "79dff0b268932c640297f5208d6298f71855c03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", + "reference": "79dff0b268932c640297f5208d6298f71855c03e", "shasum": "" }, "require": { "php": ">=8.0.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3640,92 +3752,622 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.1" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-08-21T13:31:24+00:00" }, { - "name": "sebastian/cli-parser", - "version": "2.0.x-dev", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "b6f26fb2f0242172306356a5cf2e0f4eb7e865fb" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/b6f26fb2f0242172306356a5cf2e0f4eb7e865fb", - "reference": "b6f26fb2f0242172306356a5cf2e0f4eb7e865fb", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Cache\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], "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" + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2024-02-01T13:54:40+00:00" + "time": "2022-11-30T15:59:55+00:00" }, { - "name": "sebastian/code-unit", - "version": "2.0.x-dev", + "name": "react/child-process", + "version": "v0.6.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "4274de3ccfcc8fcc838db485c1dbdb16b10e2303" + "url": "https://github.com/reactphp/child-process.git", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/4274de3ccfcc8fcc838db485c1dbdb16b10e2303", - "reference": "4274de3ccfcc8fcc838db485c1dbdb16b10e2303", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", "shasum": "" }, "require": { - "php": ">=8.1" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.0-dev" - } + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-09-16T13:41:56+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "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", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "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/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6bb7d09d6623567178cf54126afa9c2310114268", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } }, "autoload": { "classmap": [ @@ -3748,7 +4390,7 @@ "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/3.0.1" }, "funding": [ { @@ -3756,32 +4398,32 @@ "type": "github" } ], - "time": "2024-01-30T13:39:38+00:00" + "time": "2024-07-03T04:44:28+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "3.0.x-dev", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "194542d647339394232cea978b4385d09df2bca1" + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/194542d647339394232cea978b4385d09df2bca1", - "reference": "194542d647339394232cea978b4385d09df2bca1", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3804,7 +4446,7 @@ "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/4.0.1" }, "funding": [ { @@ -3812,36 +4454,36 @@ "type": "github" } ], - "time": "2024-01-30T13:40:03+00:00" + "time": "2024-07-03T04:45:54+00:00" }, { "name": "sebastian/comparator", - "version": "5.0.x-dev", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e51e8a1fce956947bb7659ae90536feee35efdf7" + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e51e8a1fce956947bb7659ae90536feee35efdf7", - "reference": "e51e8a1fce956947bb7659ae90536feee35efdf7", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/450d8f237bd611c45b5acf0733ce43e6bb280f81", + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/diff": "^5.0", - "sebastian/exporter": "^5.0" + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^10.4" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3881,7 +4523,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/6.0.2" }, "funding": [ { @@ -3889,33 +4531,33 @@ "type": "github" } ], - "time": "2024-01-30T13:40:38+00:00" + "time": "2024-08-12T06:07:25+00:00" }, { "name": "sebastian/complexity", - "version": "3.2.x-dev", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "4ea4a1fd375c586ab82dfd8c78dd0a75fb3dd94c" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/4ea4a1fd375c586ab82dfd8c78dd0a75fb3dd94c", - "reference": "4ea4a1fd375c586ab82dfd8c78dd0a75fb3dd94c", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.2-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3939,7 +4581,7 @@ "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/4.0.1" }, "funding": [ { @@ -3947,33 +4589,33 @@ "type": "github" } ], - "time": "2024-01-30T13:46:48+00:00" + "time": "2024-07-03T04:49:50+00:00" }, { "name": "sebastian/diff", - "version": "5.1.x-dev", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "86649b78a5c4175cdf281855c7876141bed1968e" + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/86649b78a5c4175cdf281855c7876141bed1968e", - "reference": "86649b78a5c4175cdf281855c7876141bed1968e", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0", - "symfony/process": "^6.4" + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -4006,7 +4648,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/6.0.2" }, "funding": [ { @@ -4014,27 +4656,27 @@ "type": "github" } ], - "time": "2024-01-30T13:46:00+00:00" + "time": "2024-07-03T04:53:05+00:00" }, { "name": "sebastian/environment", - "version": "6.0.x-dev", + "version": "7.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "7e327d8e5334dfabc5eb730b43be4e379273b757" + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7e327d8e5334dfabc5eb730b43be4e379273b757", - "reference": "7e327d8e5334dfabc5eb730b43be4e379273b757", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-posix": "*" @@ -4042,7 +4684,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.2-dev" } }, "autoload": { @@ -4070,7 +4712,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/7.2.0" }, "funding": [ { @@ -4078,34 +4720,34 @@ "type": "github" } ], - "time": "2024-01-30T13:53:00+00:00" + "time": "2024-07-03T04:54:44+00:00" }, { "name": "sebastian/exporter", - "version": "5.1.x-dev", + "version": "6.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "935b7ee9d5d9db69992fa288a92aded230dbe0f3" + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/935b7ee9d5d9db69992fa288a92aded230dbe0f3", - "reference": "935b7ee9d5d9db69992fa288a92aded230dbe0f3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/recursion-context": "^5.0" + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -4148,7 +4790,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/6.1.3" }, "funding": [ { @@ -4156,35 +4798,35 @@ "type": "github" } ], - "time": "2024-01-30T13:55:07+00:00" + "time": "2024-07-03T04:56:19+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.x-dev", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "54eb5dad20775611b81c891259e5d9d7f39250f4" + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/54eb5dad20775611b81c891259e5d9d7f39250f4", - "reference": "54eb5dad20775611b81c891259e5d9d7f39250f4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -4210,7 +4852,7 @@ "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/7.0.2" }, "funding": [ { @@ -4218,33 +4860,33 @@ "type": "github" } ], - "time": "2024-01-30T13:55:32+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { "name": "sebastian/lines-of-code", - "version": "2.0.x-dev", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "0ca20a53385ead5b0827d8fbb4e71175ee411f96" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/0ca20a53385ead5b0827d8fbb4e71175ee411f96", - "reference": "0ca20a53385ead5b0827d8fbb4e71175ee411f96", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -4268,7 +4910,7 @@ "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/3.0.1" }, "funding": [ { @@ -4276,34 +4918,34 @@ "type": "github" } ], - "time": "2024-01-30T13:56:08+00:00" + "time": "2024-07-03T04:58:38+00:00" }, { "name": "sebastian/object-enumerator", - "version": "5.0.x-dev", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "0e29d5f0a545b3293a09b6066c697b016285b2f7" + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/0e29d5f0a545b3293a09b6066c697b016285b2f7", - "reference": "0e29d5f0a545b3293a09b6066c697b016285b2f7", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -4326,7 +4968,7 @@ "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/6.0.1" }, "funding": [ { @@ -4334,32 +4976,32 @@ "type": "github" } ], - "time": "2024-01-30T13:56:27+00:00" + "time": "2024-07-03T05:00:13+00:00" }, { "name": "sebastian/object-reflector", - "version": "3.0.x-dev", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "7c357bb5701a8fbb396eddfc93c75776e102a72f" + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/7c357bb5701a8fbb396eddfc93c75776e102a72f", - "reference": "7c357bb5701a8fbb396eddfc93c75776e102a72f", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -4382,7 +5024,7 @@ "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/4.0.1" }, "funding": [ { @@ -4390,32 +5032,32 @@ "type": "github" } ], - "time": "2024-01-30T14:01:48+00:00" + "time": "2024-07-03T05:01:32+00:00" }, { "name": "sebastian/recursion-context", - "version": "5.0.x-dev", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "12a84c4900b4d65f535d5e9d03daa4ba48fbfda3" + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/12a84c4900b4d65f535d5e9d03daa4ba48fbfda3", - "reference": "12a84c4900b4d65f535d5e9d03daa4ba48fbfda3", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -4446,7 +5088,7 @@ "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/6.0.2" }, "funding": [ { @@ -4454,32 +5096,32 @@ "type": "github" } ], - "time": "2024-01-30T14:06:17+00:00" + "time": "2024-07-03T05:10:34+00:00" }, { "name": "sebastian/type", - "version": "4.0.x-dev", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "6d1976260c1ad3fc44422800ca5ceaae666eea55" + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/6d1976260c1ad3fc44422800ca5ceaae666eea55", - "reference": "6d1976260c1ad3fc44422800ca5ceaae666eea55", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", + "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -4503,7 +5145,7 @@ "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/5.0.1" }, "funding": [ { @@ -4511,29 +5153,29 @@ "type": "github" } ], - "time": "2024-01-30T14:06:51+00:00" + "time": "2024-07-03T05:11:49+00:00" }, { "name": "sebastian/version", - "version": "4.0.x-dev", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "27cfa1750df9e21e1812546c31db8824beb9cd9d" + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/27cfa1750df9e21e1812546c31db8824beb9cd9d", - "reference": "27cfa1750df9e21e1812546c31db8824beb9cd9d", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/45c9debb7d039ce9b97de2f749c2cf5832a06ac4", + "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -4557,7 +5199,7 @@ "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/5.0.1" }, "funding": [ { @@ -4565,28 +5207,28 @@ "type": "github" } ], - "time": "2024-01-30T14:07:31+00:00" + "time": "2024-07-03T05:13:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "6.4.x-dev", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef" + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -4595,13 +5237,13 @@ }, "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/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0|^7.0" + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -4629,7 +5271,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/6.4" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" }, "funding": [ { @@ -4645,27 +5287,26 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "dev-main", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "4d4ea14a8d31bc995e29bdbd566ac07c9fd004f5" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/4d4ea14a8d31bc995e29bdbd566ac07c9fd004f5", - "reference": "4d4ea14a8d31bc995e29bdbd566ac07c9fd004f5", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -4706,7 +5347,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/main" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -4722,27 +5363,30 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:06:13+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/filesystem", - "version": "6.4.x-dev", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" + "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", + "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, "type": "library", "autoload": { "psr-4": { @@ -4769,7 +5413,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/v7.1.2" }, "funding": [ { @@ -4785,27 +5429,27 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-06-28T10:03:55+00:00" }, { "name": "symfony/finder", - "version": "6.4.x-dev", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823", + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -4833,7 +5477,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/6.4" + "source": "https://github.com/symfony/finder/tree/v7.1.4" }, "funding": [ { @@ -4849,24 +5493,24 @@ "type": "tidelift" } ], - "time": "2023-10-31T17:30:12+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/options-resolver", - "version": "6.4.x-dev", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" }, "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/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -4900,7 +5544,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/6.4" + "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" }, "funding": [ { @@ -4916,26 +5560,25 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:16:24+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/polyfill-php80", - "version": "1.x-dev", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { "php": ">=7.1" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -4981,7 +5624,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -4997,26 +5640,25 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php81", - "version": "1.x-dev", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", + "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", "shasum": "" }, "require": { "php": ">=7.1" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -5058,7 +5700,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0" }, "funding": [ { @@ -5074,24 +5716,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/process", - "version": "6.4.x-dev", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "710e27879e9be3395de2b98da3f52a946039f297" + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297", - "reference": "710e27879e9be3395de2b98da3f52a946039f297", + "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -5119,7 +5761,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/v7.1.3" }, "funding": [ { @@ -5135,24 +5777,24 @@ "type": "tidelift" } ], - "time": "2024-02-20T12:31:00+00:00" + "time": "2024-07-26T12:44:47+00:00" }, { "name": "symfony/stopwatch", - "version": "6.4.x-dev", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1" + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/416596166641f1f728b0a64f5b9dd07cceb410c1", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -5181,7 +5823,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/6.3" + "source": "https://github.com/symfony/stopwatch/tree/v7.1.1" }, "funding": [ { @@ -5197,7 +5839,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:35:58+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "thecodingmachine/phpstan-safe-rule", @@ -5258,16 +5900,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -5296,7 +5938,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.3" }, "funding": [ { @@ -5304,7 +5946,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -5316,14 +5958,14 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.1", + "php": "^8.2", "ext-pdo": "*", "ext-json": "*", "ext-fileinfo": "*" }, "platform-dev": [], "platform-overrides": { - "php": "8.1.0" + "php": "8.2.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/core/block.php b/core/block.php index bd4332292..6925d9998 100644 --- a/core/block.php +++ b/core/block.php @@ -4,6 +4,10 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + +use function MicroHTML\{A, DIV, H3, SECTION, rawHTML}; + /** * Class Block * @@ -19,7 +23,7 @@ class Block /** * The content of the block. */ - public ?string $body; + public HTMLElement $body; /** * Where the block should be placed. The default theme supports @@ -45,40 +49,19 @@ 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, HTMLElement $body, string $section = "main", int $position = 50, ?string $id = null) { $this->header = $header; - $this->body = (string)$body; + $this->body = $body; $this->section = $section; $this->position = $position; if (is_null($id)) { - $id = (empty($header) ? md5($this->body ?? '') : $header) . $section; + $id = (empty($header) ? 'unknown' : $header) . $section; } - $str_id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id)); - assert(is_string($str_id)); + $str_id = preg_replace_ex('/[^\w-]/', '', str_replace(' ', '_', $id)); $this->id = $str_id; } - - /** - * Get the HTML for this block. - */ - public function get_html(bool $hidable = false): string - { - $h = $this->header; - $b = $this->body; - $i = $this->id; - $html = "
"; - $h_toggler = $hidable ? " shm-toggler" : ""; - if (!empty($h)) { - $html .= "

$h

"; - } - if (!empty($b)) { - $html .= "
$b
"; - } - $html .= "
\n"; - return $html; - } } @@ -94,6 +77,6 @@ class NavBlock extends Block { public function __construct() { - parent::__construct("Navigation", "Index", "left", 0); + parent::__construct("Navigation", A(["href" => make_link()], "Index"), "left", 0); } } diff --git a/core/cacheengine.php b/core/cacheengine.php index 0ae69a8ea..18706bae9 100644 --- a/core/cacheengine.php +++ b/core/cacheengine.php @@ -116,7 +116,7 @@ function loadCache(?string $dsn): CacheInterface $c = null; if ($dsn && !isset($_GET['DISABLE_CACHE'])) { $url = parse_url($dsn); - if($url) { + if ($url) { if ($url['scheme'] == "memcached" || $url['scheme'] == "memcache") { $memcache = new \Memcached(); $memcache->addServer($url['host'], $url['port']); @@ -135,7 +135,7 @@ function loadCache(?string $dsn): CacheInterface } } } - if(is_null($c)) { + if (is_null($c)) { $c = new \Sabre\Cache\Memory(); } global $_tracer; diff --git a/core/cli_app.php b/core/cli_app.php index 47fec20b3..a3dbdd75e 100644 --- a/core/cli_app.php +++ b/core/cli_app.php @@ -28,7 +28,7 @@ protected function getDefaultInputDefinition(): InputDefinition return $definition; } - public function run(InputInterface $input = null, OutputInterface $output = null): int + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { global $user; @@ -38,11 +38,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null 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)); - } + send_event(new UserLoginEvent($user)); } $log_level = SCORE_LOG_WARNING; diff --git a/core/config.php b/core/config.php index d53eb9972..2f80a7f90 100644 --- a/core/config.php +++ b/core/config.php @@ -5,49 +5,77 @@ namespace Shimmie2; /** - * Interface Config - * - * An abstract interface for altering a name:value pair list. + * Common methods for manipulating a map of config values, + * loading and saving is left to the concrete implementation */ -interface Config +abstract class Config { + /** @var array */ + public array $values = []; + /** * Save the list of name:value pairs to wherever they came from, * so that the next time a page is loaded it will use the new * configuration. */ - public function save(string $name = null): void; + abstract protected function save(string $name): void; - //@{ /*--------------------------------- SET ------------------------------------------------------*/ /** * Set a configuration option to a new value, regardless of what the value is at the moment. */ - public function set_int(string $name, ?int $value): void; + public function set_int(string $name, int $value): void + { + $this->values[$name] = (string)$value; + $this->save($name); + } /** * Set a configuration option to a new value, regardless of what the value is at the moment. */ - public function set_float(string $name, ?float $value): void; + public function set_float(string $name, float $value): void + { + $this->values[$name] = (string)$value; + $this->save($name); + } /** * Set a configuration option to a new value, regardless of what the value is at the moment. */ - public function set_string(string $name, ?string $value): void; + public function set_string(string $name, string $value): void + { + $this->values[$name] = $value; + $this->save($name); + } /** * Set a configuration option to a new value, regardless of what the value is at the moment. */ - public function set_bool(string $name, ?bool $value): void; + public function set_bool(string $name, bool $value): void + { + $this->values[$name] = $value ? 'Y' : 'N'; + $this->save($name); + } /** * 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; - //@} /*--------------------------------------------------------------------------------------------*/ + public function set_array(string $name, array $value): void + { + $this->values[$name] = implode(",", $value); + $this->save($name); + } + + /** + * Delete a configuration option. + */ + public function delete(string $name): void + { + unset($this->values[$name]); + $this->save($name); + } - //@{ /*-------------------------------- SET DEFAULT -----------------------------------------------*/ /** * Set a configuration option to a new value, if there is no value currently. * @@ -56,7 +84,12 @@ public function set_array(string $name, array $value): void; * page where they can be modified, while calling get_* with a "default" * parameter won't show up. */ - public function set_default_int(string $name, int $value): void; + public function set_default_int(string $name, int $value): void + { + if (is_null($this->get($name))) { + $this->values[$name] = (string)$value; + } + } /** * Set a configuration option to a new value, if there is no value currently. @@ -66,7 +99,12 @@ public function set_default_int(string $name, int $value): void; * page where they can be modified, while calling get_* with a "default" * parameter won't show up. */ - public function set_default_float(string $name, float $value): void; + public function set_default_float(string $name, float $value): void + { + if (is_null($this->get($name))) { + $this->values[$name] = (string)$value; + } + } /** * Set a configuration option to a new value, if there is no value currently. @@ -76,7 +114,12 @@ public function set_default_float(string $name, float $value): void; * page where they can be modified, while calling get_* with a "default" * parameter won't show up. */ - public function set_default_string(string $name, string $value): void; + public function set_default_string(string $name, string $value): void + { + if (is_null($this->get($name))) { + $this->values[$name] = $value; + } + } /** * Set a configuration option to a new value, if there is no value currently. @@ -86,7 +129,12 @@ public function set_default_string(string $name, string $value): void; * page where they can be modified, while calling get_* with a "default" * parameter won't show up. */ - public function set_default_bool(string $name, bool $value): void; + public function set_default_bool(string $name, bool $value): void + { + if (is_null($this->get($name))) { + $this->values[$name] = $value ? 'Y' : 'N'; + } + } /** * Set a configuration option to a new value, if there is no value currently. @@ -98,114 +146,6 @@ public function set_default_bool(string $name, bool $value): void; * * @param mixed[] $value */ - public function set_default_array(string $name, array $value): void; - //@} /*--------------------------------------------------------------------------------------------*/ - - //@{ /*--------------------------------- GET ------------------------------------------------------*/ - /** - * Pick a value out of the table by name, cast to the appropriate data type. - */ - public function get_int(string $name, ?int $default = null): ?int; - - /** - * Pick a value out of the table by name, cast to the appropriate data type. - */ - public function get_float(string $name, ?float $default = null): ?float; - - /** - * Pick a value out of the table by name, cast to the appropriate data type. - */ - public function get_string(string $name, ?string $default = null): ?string; - - /** - * Pick a value out of the table by name, cast to the appropriate data type. - */ - public function get_bool(string $name, ?bool $default = null): ?bool; - - /** - * Pick a value out of the table by name, cast to the appropriate data type. - * - * @param mixed[] $default - * @return mixed[] - */ - public function get_array(string $name, ?array $default = []): ?array; - //@} /*--------------------------------------------------------------------------------------------*/ -} - - -/** - * Class BaseConfig - * - * Common methods for manipulating the list, loading and saving is - * left to the concrete implementation - */ -abstract class BaseConfig implements Config -{ - /** @var array */ - public array $values = []; - - public function set_int(string $name, ?int $value): void - { - $this->values[$name] = is_null($value) ? null : $value; - $this->save($name); - } - - public function set_float(string $name, ?float $value): void - { - $this->values[$name] = $value; - $this->save($name); - } - - public function set_string(string $name, ?string $value): void - { - $this->values[$name] = $value; - $this->save($name); - } - - public function set_bool(string $name, ?bool $value): void - { - $this->values[$name] = $value ? 'Y' : 'N'; - $this->save($name); - } - - public function set_array(string $name, ?array $value): void - { - if ($value != null) { - $this->values[$name] = implode(",", $value); - } else { - $this->values[$name] = null; - } - $this->save($name); - } - - public function set_default_int(string $name, int $value): void - { - if (is_null($this->get($name))) { - $this->values[$name] = $value; - } - } - - public function set_default_float(string $name, float $value): void - { - if (is_null($this->get($name))) { - $this->values[$name] = $value; - } - } - - public function set_default_string(string $name, string $value): void - { - if (is_null($this->get($name))) { - $this->values[$name] = $value; - } - } - - public function set_default_bool(string $name, bool $value): void - { - if (is_null($this->get($name))) { - $this->values[$name] = $value ? 'Y' : 'N'; - } - } - public function set_default_array(string $name, array $value): void { if (is_null($this->get($name))) { @@ -214,6 +154,8 @@ public function set_default_array(string $name, array $value): void } /** + * Pick a value out of the table by name, cast to the appropriate data type. + * * @template T of int|null * @param T $default * @return T|int @@ -224,6 +166,8 @@ 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. + * * @template T of float|null * @param T $default * @return T|float @@ -234,20 +178,20 @@ 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. + * * @template T of string|null * @param T $default * @return T|string */ public function get_string(string $name, ?string $default = null): ?string { - $val = $this->get($name, $default); - if (!is_string($val) && !is_null($val)) { - throw new ServerError("$name is not a string: $val"); - } - return $val; + return $this->get($name, $default); } /** + * Pick a value out of the table by name, cast to the appropriate data type. + * * @template T of bool|null * @param T $default * @return T|bool @@ -258,6 +202,8 @@ 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. + * * @template T of array|null * @param T $default * @return T|array @@ -265,10 +211,10 @@ public function get_bool(string $name, ?bool $default = null): ?bool public function get_array(string $name, ?array $default = null): ?array { $val = $this->get($name); - if(is_null($val)) { + if (is_null($val)) { return $default; } - if(empty($val)) { + if (empty($val)) { return []; } return explode(",", $val); @@ -286,8 +232,6 @@ private function get(string $name, mixed $default = null): mixed /** - * Class DatabaseConfig - * * Loads the config list from a table in a given database, the table should * be called config and have the schema: * @@ -298,7 +242,7 @@ private function get(string $name, mixed $default = null): mixed * ); * \endcode */ -class DatabaseConfig extends BaseConfig +class DatabaseConfig extends Config { private Database $database; private string $table_name; @@ -309,8 +253,8 @@ class DatabaseConfig extends BaseConfig public function __construct( Database $database, string $table_name = "config", - string $sub_column = null, - string $sub_value = null + ?string $sub_column = null, + ?string $sub_value = null ) { global $cache; @@ -335,41 +279,41 @@ private function get_values(): mixed } foreach ($this->database->get_all($query, $args) as $row) { - $values[$row["name"]] = $row["value"]; + // versions prior to 2.12 would store null + // instead of deleting the row + if (!is_null($row["value"])) { + $values[$row["name"]] = $row["value"]; + } } return $values; } - public function save(string $name = null): void + protected function save(string $name): void { global $cache; - if (is_null($name)) { - reset($this->values); // rewind the array to the first element - foreach ($this->values as $name => $value) { - $this->save($name); - } - } else { - $query = "DELETE FROM {$this->table_name} WHERE name = :name"; - $args = ["name" => $name]; - $cols = ["name","value"]; - $params = [":name",":value"]; - if (!empty($this->sub_column) && !empty($this->sub_value)) { - $query .= " AND $this->sub_column = :sub_value"; - $args["sub_value"] = $this->sub_value; - $cols[] = $this->sub_column; - $params[] = ":sub_value"; - } + $query = "DELETE FROM {$this->table_name} WHERE name = :name"; + $args = ["name" => $name]; + $cols = ["name","value"]; + $params = [":name",":value"]; + if (!empty($this->sub_column) && !empty($this->sub_value)) { + $query .= " AND $this->sub_column = :sub_value"; + $args["sub_value"] = $this->sub_value; + $cols[] = $this->sub_column; + $params[] = ":sub_value"; + } - $this->database->execute($query, $args); + $this->database->execute($query, $args); + if (isset($this->values[$name])) { $args["value"] = $this->values[$name]; $this->database->execute( "INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")", $args ); } + // rather than deleting and having some other request(s) do a thundering // herd of race-conditioned updates, just save the updated version once here $cache->set($this->cache_name, $this->values); diff --git a/core/database.php b/core/database.php index acfdd904f..b4ab8bbb6 100644 --- a/core/database.php +++ b/core/database.php @@ -8,6 +8,7 @@ use FFSPHP\PDOStatement; require_once __DIR__ . '/exceptions.php'; +require_once __DIR__ . '/stdlib_ex.php'; enum DatabaseDriverID: string { @@ -68,11 +69,13 @@ public function __construct(string $dsn) private function get_db(): PDO { - if(is_null($this->db)) { + if (is_null($this->db)) { $this->db = new PDO($this->dsn); $this->connect_engine(); + assert(!is_null($this->db)); $this->get_engine()->init($this->db); $this->begin_transaction(); + assert(!is_null($this->db)); } return $this->db; } @@ -155,6 +158,7 @@ private function get_engine(): DBEngine { if (is_null($this->engine)) { $this->connect_engine(); + assert(!is_null($this->engine)); } return $this->engine; } @@ -182,7 +186,7 @@ private function count_time(string $method, float $start, string $query, ?array global $_tracer, $tracer_enabled; $dur = ftime() - $start; // trim whitespace - $query = preg_replace('/[\n\t ]+/m', ' ', $query); + $query = preg_replace_ex('/[\n\t ]+/m', ' ', $query); $query = trim($query); if ($tracer_enabled) { $_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query" => $query, "args" => $args, "method" => $method]); @@ -452,6 +456,6 @@ public function seeded_random(int $seed, string $id_column): string } // As fallback, use MD5 as a DRBG. - return "MD5(CONCAT($seed, CONCAT('+', $id_column)))"; + return "MD5($seed || '+' || $id_column)"; } } diff --git a/core/dbengine.php b/core/dbengine.php index 6376bd5bf..c17176f3c 100644 --- a/core/dbengine.php +++ b/core/dbengine.php @@ -44,6 +44,7 @@ class MySQL extends DBEngine public function init(PDO $db): void { $db->exec("SET NAMES utf8;"); + $db->exec("SET SESSION sql_mode='ANSI,TRADITIONAL';"); } public function scoreql_to_sql(string $data): string @@ -146,18 +147,10 @@ function _log(float $a, ?float $b = null): float return log($b, $a); } } -function _isnull(mixed $a): bool -{ - return is_null($a); -} function _md5(string $a): string { return md5($a); } -function _concat(string $a, string $b): string -{ - return $a . $b; -} function _lower(string $a): string { return strtolower($a); @@ -183,9 +176,7 @@ public function init(PDO $db): void $db->sqliteCreateFunction('now', 'Shimmie2\_now', 0); $db->sqliteCreateFunction('floor', 'Shimmie2\_floor', 1); $db->sqliteCreateFunction('log', 'Shimmie2\_log'); - $db->sqliteCreateFunction('isnull', 'Shimmie2\_isnull', 1); $db->sqliteCreateFunction('md5', 'Shimmie2\_md5', 1); - $db->sqliteCreateFunction('concat', 'Shimmie2\_concat', 2); $db->sqliteCreateFunction('lower', 'Shimmie2\_lower', 1); $db->sqliteCreateFunction('rand', 'Shimmie2\_rand', 0); $db->sqliteCreateFunction('ln', 'Shimmie2\_ln', 1); diff --git a/core/event.php b/core/event.php index 5b545eed0..b8fe1ba6f 100644 --- a/core/event.php +++ b/core/event.php @@ -63,7 +63,6 @@ class PageRequestEvent extends Event */ private array $named_args = []; public int $page_num; - private bool $is_authed; /** * @param string $method The HTTP method used to make the request @@ -87,10 +86,6 @@ public function __construct(string $method, string $path, array $get, array $pos $this->path = $path; $this->GET = $get; $this->POST = $post; - $this->is_authed = ( - defined("UNITTEST") - || (isset($_POST["auth_token"]) && $_POST["auth_token"] == $user->get_auth_token()) - ); // break the path into parts $this->args = explode('/', $path); @@ -98,8 +93,8 @@ public function __construct(string $method, string $path, array $get, array $pos public function get_GET(string $key): ?string { - if(array_key_exists($key, $this->GET)) { - if(is_array($this->GET[$key])) { + if (array_key_exists($key, $this->GET)) { + if (is_array($this->GET[$key])) { throw new UserError("GET parameter {$key} is an array, expected single value"); } return $this->GET[$key]; @@ -111,7 +106,7 @@ public function get_GET(string $key): ?string public function req_GET(string $key): string { $value = $this->get_GET($key); - if($value === null) { + if ($value === null) { throw new UserError("Missing GET parameter {$key}"); } return $value; @@ -119,8 +114,8 @@ public function req_GET(string $key): string public function get_POST(string $key): ?string { - if(array_key_exists($key, $this->POST)) { - if(is_array($this->POST[$key])) { + if (array_key_exists($key, $this->POST)) { + if (is_array($this->POST[$key])) { throw new UserError("POST parameter {$key} is an array, expected single value"); } return $this->POST[$key]; @@ -132,7 +127,7 @@ public function get_POST(string $key): ?string public function req_POST(string $key): string { $value = $this->get_POST($key); - if($value === null) { + if ($value === null) { throw new UserError("Missing POST parameter {$key}"); } return $value; @@ -143,8 +138,8 @@ public function req_POST(string $key): string */ public function get_POST_array(string $key): ?array { - if(array_key_exists($key, $this->POST)) { - if(!is_array($this->POST[$key])) { + if (array_key_exists($key, $this->POST)) { + if (!is_array($this->POST[$key])) { throw new UserError("POST parameter {$key} is a single value, expected array"); } return $this->POST[$key]; @@ -159,7 +154,7 @@ public function get_POST_array(string $key): ?array public function req_POST_array(string $key): array { $value = $this->get_POST_array($key); - if($value === null) { + if ($value === null) { throw new UserError("Missing POST parameter {$key}"); } return $value; @@ -167,7 +162,7 @@ public function req_POST_array(string $key): array public function page_starts_with(string $name): bool { - return (count($this->args) >= 1) && ($this->args[0] == $name); + return str_starts_with($this->path, $name); } /** @@ -184,10 +179,10 @@ public function page_matches( ): bool { global $user; - if($paged) { - if($this->page_matches("$name/{page_num}", $method, $authed, $permission, false)) { + if ($paged) { + if ($this->page_matches("$name/{page_num}", $method, $authed, $permission, false)) { $pn = $this->get_arg("page_num"); - if(is_numberish($pn)) { + if (is_numberish($pn)) { return true; } } @@ -197,7 +192,7 @@ public function page_matches( $authed = $authed ?? $method == "POST"; // method check is fast so do that first - if($method !== null && $this->method !== $method) { + if ($method !== null && $this->method !== $method) { return false; } @@ -218,10 +213,15 @@ public function page_matches( // if we matched the method and the path, but the page requires // authentication and the user is not authenticated, then complain - if($authed && $this->is_authed === false) { - throw new PermissionDenied("Permission Denied: Missing CSRF Token"); + if ($authed && !defined("UNITTEST")) { + if (!isset($this->POST["auth_token"])) { + throw new PermissionDenied("Permission Denied: Missing CSRF Token"); + } + if ($this->POST["auth_token"] != $user->get_auth_token()) { + throw new PermissionDenied("Permission Denied: Invalid CSRF Token (Go back, refresh the page, and try again?)"); + } } - if($permission !== null && !$user->can($permission)) { + if ($permission !== null && !$user->can($permission)) { throw new PermissionDenied("Permission Denied: {$user->name} lacks permission {$permission}"); } @@ -233,9 +233,9 @@ public function page_matches( */ public function get_arg(string $n, ?string $default = null): string { - if(array_key_exists($n, $this->named_args)) { + if (array_key_exists($n, $this->named_args)) { return rawurldecode($this->named_args[$n]); - } elseif($default !== null) { + } elseif ($default !== null) { return $default; } else { throw new UserError("Page argument {$n} is missing"); @@ -244,12 +244,12 @@ public function get_arg(string $n, ?string $default = null): string public function get_iarg(string $n, ?int $default = null): int { - if(array_key_exists($n, $this->named_args)) { - if(is_numberish($this->named_args[$n]) === false) { + if (array_key_exists($n, $this->named_args)) { + if (is_numberish($this->named_args[$n]) === false) { throw new UserError("Page argument {$n} exists but is not numeric"); } return int_escape($this->named_args[$n]); - } elseif($default !== null) { + } elseif ($default !== null) { return $default; } else { throw new UserError("Page argument {$n} is missing"); diff --git a/core/exceptions.php b/core/exceptions.php index d0edc8394..9b096b2c0 100644 --- a/core/exceptions.php +++ b/core/exceptions.php @@ -57,7 +57,7 @@ class ObjectNotFound extends UserError public int $http_code = 404; } -class ImageNotFound extends ObjectNotFound +class PostNotFound extends ObjectNotFound { } diff --git a/core/extension.php b/core/extension.php index 06aed4274..e4823c461 100644 --- a/core/extension.php +++ b/core/extension.php @@ -28,33 +28,11 @@ abstract class Extension public function __construct(?string $class = null) { $class = $class ?? get_called_class(); - $this->theme = $this->get_theme_object($class); + $this->theme = Themelet::get_for_extension_class($class); $this->info = ExtensionInfo::get_for_extension_class($class); $this->key = $this->info->key; } - /** - * Find the theme object for a given extension. - */ - private function get_theme_object(string $base): Themelet - { - $base = str_replace("Shimmie2\\", "", $base); - $custom = "Shimmie2\Custom{$base}Theme"; - $normal = "Shimmie2\\{$base}Theme"; - - if (class_exists($custom)) { - $c = new $custom(); - assert(is_a($c, Themelet::class)); - return $c; - } elseif (class_exists($normal)) { - $n = new $normal(); - assert(is_a($n, Themelet::class)); - return $n; - } else { - return new Themelet(); - } - } - /** * Override this to change the priority of the extension, * lower numbered ones will receive events first. @@ -181,14 +159,16 @@ public function is_supported(): bool { if ($this->supported === null) { $this->check_support(); + assert(!is_null($this->supported)); } return $this->supported; } public function get_support_info(): string { - if ($this->supported === null) { + if ($this->support_info === null) { $this->check_support(); + assert(!is_null($this->support_info)); } return $this->support_info; } @@ -340,7 +320,7 @@ public function onDataUpload(DataUploadEvent $event): void // 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'])) { + if (!empty($event->metadata['tags'])) { $tags = Tag::explode($existing->get_tag_list() . " " . $event->metadata['tags']); send_event(new TagSetEvent($existing, $tags)); } @@ -373,12 +353,10 @@ public function onDataUpload(DataUploadEvent $event): void // 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']}" - ); + try { + \Safe\copy($event->tmpname, $filename); + } catch (\Exception $e) { + throw new UploadException("Failed to copy file from uploads ({$event->tmpname}) to archive ($filename): ".$e->getMessage()); } $event->images[] = $iae->image; diff --git a/core/imageboard/image.php b/core/imageboard/image.php index 6bab17638..8f2ae083a 100644 --- a/core/imageboard/image.php +++ b/core/imageboard/image.php @@ -86,11 +86,11 @@ public function __construct(?array $row = null) // we only want the key=>value ones if (is_numeric($name)) { continue; - } elseif(property_exists($this, $name)) { + } 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)) { + if (is_a($t, \ReflectionNamedType::class)) { + if (is_null($value)) { $this->$name = null; } else { $this->$name = match($t->getName()) { @@ -102,7 +102,7 @@ public function __construct(?array $row = null) } } - } elseif(array_key_exists($name, static::$prop_types)) { + } elseif (array_key_exists($name, static::$prop_types)) { if (is_null($value)) { $value = null; } else { @@ -118,7 +118,7 @@ public function __construct(?array $row = null) // 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')) { + if (defined('UNITTEST')) { throw new \Exception("Unknown column $name in images table"); } } @@ -135,7 +135,7 @@ public function offsetExists(mixed $offset): bool public function offsetGet(mixed $offset): mixed { assert(is_string($offset)); - if(!$this->offsetExists($offset)) { + if (!$this->offsetExists($offset)) { $known = implode(", ", array_keys(static::$prop_types)); throw new \OutOfBoundsException("Undefined dynamic property: $offset (Known: $known)"); } @@ -178,10 +178,10 @@ public static function by_id(int $post_id): ?Image public static function by_id_ex(int $post_id): Image { $maybe_post = static::by_id($post_id); - if(!is_null($maybe_post)) { + if (!is_null($maybe_post)) { return $maybe_post; } - throw new ImageNotFound("Image $post_id not found"); + throw new PostNotFound("Image $post_id not found"); } public static function by_hash(string $hash): ?Image @@ -265,7 +265,6 @@ public function get_prev(array $tags = []): ?Image public function get_owner(): User { $user = User::by_id($this->owner_id); - assert(!is_null($user)); return $user; } @@ -450,7 +449,7 @@ public function get_info(): string */ public function get_image_filename(): string { - if(!is_null($this->tmp_file)) { + if (!is_null($this->tmp_file)) { return $this->tmp_file; } return warehouse_path(self::IMAGE_DIR, $this->hash); @@ -632,8 +631,8 @@ public function remove_image_only(bool $quiet = false): void { $img_del = @unlink($this->get_image_filename()); $thumb_del = @unlink($this->get_thumb_filename()); - if($img_del && $thumb_del) { - if(!$quiet) { + if ($img_del && $thumb_del) { + if (!$quiet) { log_info("core_image", "Deleted files for Post #{$this->id} ({$this->hash})"); } } else { diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php index d267aa396..1d1d1adbc 100644 --- a/core/imageboard/misc.php +++ b/core/imageboard/misc.php @@ -32,7 +32,7 @@ function add_dir(string $base, array $extra_tags = []): array 'tags' => Tag::implode($tags), ])); $results = []; - foreach($dae->images as $image) { + foreach ($dae->images as $image) { $results[] = new UploadSuccess($filename, $image->id); } return $results; @@ -136,7 +136,7 @@ function get_thumbnail_max_size_scaled(): array } -function create_image_thumb(Image $image, string $engine = null): void +function create_image_thumb(Image $image, ?string $engine = null): void { global $config; create_scaled_image( diff --git a/core/imageboard/search.php b/core/imageboard/search.php index b89b70a68..40143c835 100644 --- a/core/imageboard/search.php +++ b/core/imageboard/search.php @@ -84,7 +84,7 @@ class Search */ private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags = []): \FFSPHP\PDOStatement { - global $database, $user; + global $config, $database, $user; if ($start < 0) { $start = 0; @@ -93,9 +93,10 @@ private static function find_images_internal(int $start = 0, ?int $limit = null, $limit = 1; } - if (SPEED_HAX) { - if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) { - throw new PermissionDenied("Anonymous users may only search for up to 3 tags at a time"); + if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_int(SpeedHaxConfig::BIG_SEARCH) > 0) { + $anon_limit = $config->get_int(SpeedHaxConfig::BIG_SEARCH); + if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > $anon_limit) { + throw new PermissionDenied("Anonymous users may only search for up to $anon_limit tags at a time"); } } @@ -146,7 +147,7 @@ public static function find_images_iterable(int $start = 0, ?int $limit = null, public static function get_images(array $ids): array { $visible_images = []; - foreach(Search::find_images(tags: ["id=" . implode(",", $ids)]) as $image) { + foreach (Search::find_images(tags: ["id=" . implode(",", $ids)]) as $image) { $visible_images[$image->id] = $image; } $visible_ids = array_keys($visible_images); @@ -182,15 +183,16 @@ private static function count_total_images(): int */ public static function count_images(array $tags = []): int { - global $cache, $database; + global $cache, $config, $database; $tag_count = count($tags); - // SPEED_HAX ignores the fact that extensions can add img_conditions + // 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) { + $speed_hax = (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::LIMIT_COMPLEX)); + 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])) { + } 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]); @@ -207,7 +209,7 @@ public static function count_images(array $tags = []): int [$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) { + 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); @@ -451,7 +453,7 @@ public static function build_search_querylet( $query->append(new Querylet($img_sql, $img_vars)); } - if(!is_null($order)) { + if (!is_null($order)) { $query->append(new Querylet(" ORDER BY ".$order)); } diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php index fab6b1f2c..95c266ccb 100644 --- a/core/imageboard/tag.php +++ b/core/imageboard/tag.php @@ -196,13 +196,13 @@ public static function explode(string $tags, bool $tagme = true): array public static function sanitize(string $tag): string { - $tag = preg_replace("/\s/", "", $tag); # whitespace + $tag = preg_replace_ex("/\s/", "", $tag); # whitespace assert($tag !== null); - $tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL + $tag = preg_replace_ex('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL assert($tag !== null); - $tag = preg_replace("/\.+/", ".", $tag); # strings of dots? + $tag = preg_replace_ex("/\.+/", ".", $tag); # strings of dots? assert($tag !== null); - $tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes? + $tag = preg_replace_ex("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes? assert($tag !== null); $tag = trim($tag, ", \t\n\r\0\x0B"); diff --git a/core/install.php b/core/install.php index d5326dbe3..77aa07c3e 100644 --- a/core/install.php +++ b/core/install.php @@ -314,7 +314,8 @@ class VARCHAR(32) NOT NULL DEFAULT 'user', function write_config(string $dsn): void { - $file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\n"; + $secret = bin2hex(random_bytes(16)); + $file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\ndefine('SECRET', '$secret');\n"; if (!file_exists("data/config")) { mkdir("data/config", 0755, true); diff --git a/core/microhtml.php b/core/microhtml.php index 7dd8662e3..5847ffd89 100644 --- a/core/microhtml.php +++ b/core/microhtml.php @@ -8,11 +8,11 @@ use function MicroHTML\{emptyHTML}; use function MicroHTML\A; +use function MicroHTML\CODE; +use function MicroHTML\DIV; use function MicroHTML\FORM; use function MicroHTML\INPUT; -use function MicroHTML\DIV; use function MicroHTML\OPTION; -use function MicroHTML\PRE; use function MicroHTML\P; use function MicroHTML\SELECT; use function MicroHTML\SPAN; @@ -87,7 +87,7 @@ function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement { return DIV( ["class" => "command_example"], - PRE($ex), + CODE($ex), P($desc) ); } @@ -163,14 +163,14 @@ function SHM_POST_INFO( HTMLElement|string|null $edit = null, string|null $link = null, ): HTMLElement { - if(!is_null($view) && !is_null($edit)) { + if (!is_null($view) && !is_null($edit)) { $show = emptyHTML( SPAN(["class" => "view"], $view), SPAN(["class" => "edit"], $edit), ); - } elseif(!is_null($edit)) { + } elseif (!is_null($edit)) { $show = $edit; - } elseif(!is_null($view)) { + } elseif (!is_null($view)) { $show = $view; } else { $show = "???"; diff --git a/core/basepage.php b/core/page.php similarity index 82% rename from core/basepage.php rename to core/page.php index b8ffffc95..ba1bbba5d 100644 --- a/core/basepage.php +++ b/core/page.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\{emptyHTML,rawHTML,HTML,HEAD,BODY}; +use function MicroHTML\{emptyHTML, rawHTML, HTML, HEAD, BODY, TITLE, LINK, SCRIPT, A, B, joinHTML, BR, H1, HEADER as HTML_HEADER, NAV, ARTICLE, FOOTER, SECTION, H3, DIV}; require_once "core/event.php"; @@ -36,14 +36,12 @@ public function __construct(string $name, string $value, int $time, string $path } /** - * Class Page - * * A data structure for holding all the bits of data that make up a page. * * The various extensions all add whatever they want to this structure, * then Layout turns it into HTML. */ -class BasePage +class Page { public PageMode $mode = PageMode::PAGE; private string $mime; @@ -101,7 +99,7 @@ public function set_file(string $file, bool $delete = false): void public function set_filename(string $filename, string $disposition = "attachment"): void { $max_len = 250; - if(strlen($filename) > $max_len) { + if (strlen($filename) > $max_len) { // remove extension, truncate filename, apply extension $ext = pathinfo($filename, PATHINFO_EXTENSION); $filename = substr($filename, 0, $max_len - strlen($ext) - 1) . '.' . $ext; @@ -131,7 +129,7 @@ public function set_redirect(string $redirect): void public string $subheading = ""; public bool $left_enabled = true; - /** @var string[] */ + /** @var HTMLElement[] */ public array $html_headers = []; /** @var string[] */ @@ -157,6 +155,9 @@ public function set_code(int $code): void public function set_title(string $title): void { $this->title = $title; + if ($this->heading === "") { + $this->heading = $title; + } } public function set_heading(string $heading): void @@ -179,17 +180,6 @@ public function disable_left(): void $this->left_enabled = false; } - /** - * Add a line to the HTML head section. - */ - public function add_html_header(string $line, int $position = 50): void - { - while (isset($this->html_headers[$position])) { - $position++; - } - $this->html_headers[$position] = $line; - } - /** * Add a http header to be sent to the client. */ @@ -222,17 +212,26 @@ public function get_cookie(string $name): ?string } } + /** + * Add a line to the HTML head section. + */ + public function add_html_header(HTMLElement $line, int $position = 50): void + { + while (isset($this->html_headers[$position])) { + $position++; + } + $this->html_headers[$position] = $line; + } + /** * Get all the HTML headers that are currently set and return as a string. */ - public function get_all_html_headers(): string + public function get_all_html_headers(): HTMLElement { - $data = ''; ksort($this->html_headers); - foreach ($this->html_headers as $line) { - $data .= "\t\t" . $line . "\n"; - } - return $data; + return emptyHTML( + ...$this->html_headers + ); } /** @@ -247,14 +246,14 @@ public function add_block(Block $block): void * Find a block which contains the given text * (Useful for unit tests) */ - public function find_block(string $text): ?Block + public function find_block(string $text): Block { foreach ($this->blocks as $block) { if ($block->header == $text) { return $block; } } - return null; + throw new \Exception("Block not found: $text"); } // ============================================== @@ -305,7 +304,7 @@ public function display(): void if (!is_null($this->filename)) { header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename); } - assert($this->file, "file should not be null with PageMode::FILE"); + assert(!is_null($this->file), "file should not be null with PageMode::FILE"); // https://gist.github.com/codler/3906826 $size = \Safe\filesize($this->file); // File size @@ -384,8 +383,15 @@ public function add_auto_html_headers(): void $theme_name = $config->get_string(SetupConfig::THEME, 'default'); # 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); + $this->add_html_header(LINK([ + 'rel' => 'icon', + 'type' => 'image/x-icon', + 'href' => "$data_href/favicon.ico" + ]), 41); + $this->add_html_header(LINK([ + 'rel' => 'apple-touch-icon', + 'href' => "$data_href/apple-touch-icon.png" + ]), 42); //We use $config_latest to make sure cache is reset if config is ever updated. $config_latest = 0; @@ -394,13 +400,24 @@ public function add_auto_html_headers(): void } $css_cache_file = $this->get_css_cache_file($theme_name, $config_latest); - $this->add_html_header("", 43); + $this->add_html_header(LINK([ + 'rel' => 'stylesheet', + 'href' => "$data_href/$css_cache_file", + 'type' => 'text/css' + ]), 43); $initjs_cache_file = $this->get_initjs_cache_file($theme_name, $config_latest); - $this->add_html_header("", 44); + $this->add_html_header(SCRIPT([ + 'src' => "$data_href/$initjs_cache_file", + 'type' => 'text/javascript' + ])); $js_cache_file = $this->get_js_cache_file($theme_name, $config_latest); - $this->add_html_header("", 44); + $this->add_html_header(SCRIPT([ + 'defer' => true, + 'src' => "$data_href/$js_cache_file", + 'type' => 'text/javascript' + ])); } private function get_css_cache_file(string $theme_name, int $config_latest): string @@ -417,7 +434,7 @@ private function get_css_cache_file(string $theme_name, int $config_latest): str $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) { + foreach ($css_files as $css) { $mcss->addSource($css); } $mcss->save($css_cache_file); @@ -440,7 +457,7 @@ private function get_initjs_cache_file(string $theme_name, int $config_latest): $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) { + foreach ($js_files as $js) { $mcss->addSource($js); } $mcss->save($js_cache_file); @@ -468,7 +485,7 @@ private function get_js_cache_file(string $theme_name, int $config_latest): stri $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) { + foreach ($js_files as $js) { $mcss->addSource($js); } $mcss->save($js_cache_file); @@ -549,52 +566,56 @@ protected function get_nav_links(): array */ public function render(): void { - global $config, $user; + print (string)$this->html_html( + $this->head_html(), + $this->body_html() + ); + } - $head = $this->head_html(); - $body = $this->body_html(); + public function html_html(HTMLElement $head, string|HTMLElement $body): HTMLElement + { + global $user; $body_attrs = [ "data-userclass" => $user->class->name, "data-base-href" => get_base_href(), + "data-base-link" => make_link(""), ]; - print emptyHTML( + return emptyHTML( rawHTML(""), HTML( ["lang" => "en"], - HEAD(rawHTML($head)), - BODY($body_attrs, rawHTML($body)) + HEAD($head), + BODY($body_attrs, $body) ) ); } - protected function head_html(): string + protected function head_html(): HTMLElement { - $html_header_html = $this->get_all_html_headers(); - - return " - {$this->title} - $html_header_html - "; + return emptyHTML( + TITLE($this->title), + $this->get_all_html_headers(), + ); } - protected function body_html(): string + protected function body_html(): HTMLElement { - $left_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; + $left_block_html = []; + $main_block_html = []; + $sub_block_html = []; foreach ($this->blocks as $block) { switch ($block->section) { case "left": - $left_block_html .= $block->get_html(true); + $left_block_html[] = $this->block_html($block, true); break; case "main": - $main_block_html .= $block->get_html(false); + $main_block_html[] = $this->block_html($block, false); break; case "subheading": - $sub_block_html .= $block->get_html(false); + $sub_block_html[] = $this->block_html($block, false); break; default: print "

error: {$block->header} using an unknown section ({$block->section})"; @@ -603,41 +624,60 @@ protected function body_html(): string } $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 -
-
+ $flash_html = $this->flash_html(); + return emptyHTML( + HTML_HEADER( + H1($this->heading), + ...$sub_block_html + ), + NAV( + ...$left_block_html + ), + ARTICLE( + $flash_html, + ...$main_block_html + ), + FOOTER( $footer_html -
- "; + ) + ); + } + + protected function block_html(Block $block, bool $hidable): HTMLElement + { + $html = SECTION(['id' => $block->id]); + if (!empty($block->header)) { + $html->appendChild(H3(["data-toggle-sel" => "#{$block->id}", "class" => $hidable ? "shm-toggler" : ""], $block->header)); + } + if (!empty($block->body)) { + $html->appendChild(DIV(['class' => "blockbody"], $block->body)); + } + return $html; + } + + protected function flash_html(): HTMLElement + { + if ($this->flash) { + return B(["id" => "flash"], rawHTML(nl2br(html_escape(implode("\n", $this->flash))))); + } + return emptyHTML(); } - protected function footer_html(): string + protected function footer_html(): HTMLElement { $debug = get_debug_info(); $contact_link = contact_link(); - $contact = empty($contact_link) ? "" : "
Contact"; - - return " - Media © their respective owners, - Shimmie © - Shish & - The Team - 2007-2024, - based on the Danbooru concept. - $debug - $contact - "; + return joinHTML("", [ + "Media © their respective owners, ", + A(["href" => "https://code.shishnet.org/shimmie2/"], "Shimmie"), + " © ", + A(["href" => "https://www.shishnet.org/"], "Shish"), + " & ", + A(["href" => "https://github.com/shish/shimmie2/graphs/contributors"], "The Team"), + " 2007-2024, based on the Danbooru concept.", + BR(), $debug, + $contact_link ? emptyHTML(BR(), A(["href" => $contact_link], "Contact")) : "" + ]); } } @@ -711,7 +751,7 @@ public function __construct(string $name, Link $link, string|HTMLElement $descri /** * @param string[] $pages_matched */ - public static function is_active(array $pages_matched, string $url = null): bool + 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.) diff --git a/core/permissions.php b/core/permissions.php index f0592f5ef..7a0814be1 100644 --- a/core/permissions.php +++ b/core/permissions.php @@ -20,7 +20,7 @@ abstract class Permissions public const CHANGE_USER_SETTING = "change_user_setting"; public const CHANGE_OTHER_USER_SETTING = "change_other_user_setting"; - /** search for more than 3 tags at once (only applies if SPEED_HAX is active) */ + /** search for more than 3 tags at once (only applies if Speed Hax is active) */ public const BIG_SEARCH = "big_search"; /** enable or disable extensions */ diff --git a/core/polyfills.php b/core/polyfills.php index 4e11f1677..6d6515221 100644 --- a/core/polyfills.php +++ b/core/polyfills.php @@ -40,7 +40,7 @@ function array_iunique(array $array): array function ip_in_range(string $IP, string $CIDR): bool { $parts = explode("/", $CIDR); - if(count($parts) == 1) { + if (count($parts) == 1) { $parts[1] = "32"; } list($net, $mask) = $parts; @@ -159,7 +159,7 @@ function flush_output(): void function stream_file(string $file, int $start, int $end): void { $fp = fopen($file, 'r'); - if(!$fp) { + if (!$fp) { throw new \Exception("Failed to open $file"); } try { @@ -168,7 +168,7 @@ function stream_file(string $file, int $start, int $end): void while (!feof($fp) && ($p = ftell($fp)) <= $end) { if ($p + $buffer > $end) { $buffer = $end - $p + 1; - assert($buffer >= 0); + assert($buffer >= 1); } echo fread($fp, $buffer); flush_output(); @@ -423,7 +423,7 @@ function truncate(string $string, int $limit, string $break = " ", string $pad = assert($limit > $padlen, "Can't truncate to a length less than the padding length"); // if string is shorter or equal to limit, leave it alone - if($strlen <= $limit) { + if ($strlen <= $limit) { return $string; } @@ -628,7 +628,9 @@ function validate_input(array $inputs): array if (in_array('user_id', $flags)) { $id = int_escape($value); if (in_array('exists', $flags)) { - if (is_null(User::by_id($id))) { + try { + User::by_id($id); + } catch (UserNotFound $e) { throw new InvalidInput("User #$id does not exist"); } } @@ -646,7 +648,7 @@ function validate_input(array $inputs): array $outputs[$key] = $value; } elseif (in_array('user_class', $flags)) { if (!array_key_exists($value, UserClass::$known_classes)) { - throw new InvalidInput("Invalid user class: ".html_escape($value)); + throw new InvalidInput("Invalid user class: $value"); } $outputs[$key] = $value; } elseif (in_array('email', $flags)) { @@ -656,7 +658,7 @@ function validate_input(array $inputs): array } elseif (in_array('int', $flags)) { $value = trim($value); if (empty($value) || !is_numeric($value)) { - throw new InvalidInput("Invalid int: ".html_escape($value)); + throw new InvalidInput("Invalid int: $value"); } $outputs[$key] = (int)$value; } elseif (in_array('bool', $flags)) { @@ -693,7 +695,8 @@ function validate_input(array $inputs): array */ function sanitize_path(string $path): string { - return preg_replace('|[\\\\/]+|S', DIRECTORY_SEPARATOR, $path); + $r = preg_replace_ex('|[\\\\/]+|S', DIRECTORY_SEPARATOR, $path); + return $r; } /** diff --git a/core/send_event.php b/core/send_event.php index 553a4b15c..bd7a18afb 100644 --- a/core/send_event.php +++ b/core/send_event.php @@ -19,18 +19,19 @@ class TimeoutException extends \RuntimeException function _load_event_listeners(): void { - global $_shm_event_listeners; + global $_shm_event_listeners, $config; - $ver = preg_replace("/[^a-zA-Z0-9\.]/", "_", VERSION); + $ver = preg_replace_ex("/[^a-zA-Z0-9\.]/", "_", VERSION); $key = md5(Extension::get_enabled_extensions_as_string()); + $speed_hax = (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::CACHE_EVENT_LISTENERS)); $cache_path = data_path("cache/event_listeners/el.$ver.$key.php"); - if (SPEED_HAX && file_exists($cache_path)) { + if ($speed_hax && file_exists($cache_path)) { require_once($cache_path); } else { _set_event_listeners(); - if (SPEED_HAX) { + if ($speed_hax) { _dump_event_listeners($_shm_event_listeners, $cache_path); } } diff --git a/core/stdlib_ex.php b/core/stdlib_ex.php index 3f9514ac6..69c87b33f 100644 --- a/core/stdlib_ex.php +++ b/core/stdlib_ex.php @@ -7,9 +7,9 @@ */ function false_throws(mixed $x, ?callable $errorgen = null): mixed { - if($x === false) { + if ($x === false) { $msg = "Unexpected false"; - if($errorgen) { + if ($errorgen) { $msg = $errorgen(); } throw new \Exception($msg); @@ -32,3 +32,12 @@ function filter_var_ex(mixed $variable, int $filter = FILTER_DEFAULT, mixed $opt { return false_throws(filter_var($variable, $filter, $options)); } + +function preg_replace_ex(string $pattern, string $replacement, string $subject, int $limit = -1, ?int &$count = null): string +{ + $res = preg_replace($pattern, $replacement, $subject, $limit, $count); + if (is_null($res)) { + throw new \Exception("preg_replace failed"); + } + return $res; +} diff --git a/core/sys_config.php b/core/sys_config.php index 2af3ac692..a36eb24c0 100644 --- a/core/sys_config.php +++ b/core/sys_config.php @@ -14,7 +14,7 @@ * Do NOT change them in this file. These are the defaults only! * * Example: - * define("SPEED_HAX", true); + * define("DEBUG", true); */ function _d(string $name, mixed $value): void @@ -29,12 +29,12 @@ function _d(string $name, mixed $value): void _d("CACHE_DSN", null); // string cache connection details _d("DEBUG", false); // boolean print various debugging details _d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes -_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance _d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse -_d("VERSION", "2.11.0-alpha"); // string shimmie version +_d("VERSION", "2.12.0-alpha"); // 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("SECRET", DATABASE_DSN); // string A secret bit of data used to salt some hashes diff --git a/core/testcase.php b/core/testcase.php index 9764896bf..e1e420ab3 100644 --- a/core/testcase.php +++ b/core/testcase.php @@ -4,7 +4,7 @@ namespace Shimmie2; -if(class_exists("\\PHPUnit\\Framework\\TestCase")) { +if (class_exists("\\PHPUnit\\Framework\\TestCase")) { abstract class ShimmiePHPUnitTestCase extends \PHPUnit\Framework\TestCase { protected static string $anon_name = "anonymous"; @@ -28,7 +28,7 @@ public static function setUpBeforeClass(): void */ public function setUp(): void { - global $database, $_tracer; + global $database, $_tracer, $page; $_tracer->begin($this->name()); $_tracer->begin("setUp"); $class = str_replace("Test", "", get_class($this)); @@ -44,10 +44,11 @@ public function setUp(): void $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)); + send_event(new ImageDeletionEvent(Image::by_id_ex((int)$image_id), true)); } // Reload users from the database in case they were modified UserClass::loadClasses(); + $page = new Page(); $_tracer->end(); # setUp $_tracer->begin("test"); @@ -93,12 +94,14 @@ private static function check_args(array $args): array /** * @param array $get_args * @param array $post_args + * @param array $cookies */ protected static function request( string $method, string $page_name, array $get_args = [], - array $post_args = [] + array $post_args = [], + array $cookies = ["shm_accepted_terms" => "true"], ): Page { // use a fresh page global $page; @@ -112,6 +115,7 @@ protected static function request( $_SERVER['REQUEST_URI'] = make_link($page_name, http_build_query($get_args)); $_GET = $get_args; $_POST = $post_args; + $_COOKIE = $cookies; $page = new Page(); send_event(new PageRequestEvent($method, $page_name, $get_args, $post_args)); if ($page->mode == PageMode::REDIRECT) { @@ -161,34 +165,46 @@ protected function assert_response(int $code): void $this->assertEquals($code, $page->code); } - protected function page_to_text(string $section = null): string + /** + * @param array $blocks + * @param ?string $section + * @return string + */ + private function blocks_to_text(array $blocks, ?string $section): 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"; - } + $text = ""; + foreach ($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)"); } + return $text; + } + + protected function page_to_text(?string $section = null): string + { + global $page; + + return match($page->mode) { + PageMode::PAGE => $page->title . "\n" . $this->blocks_to_text($page->blocks, $section), + PageMode::DATA => $page->data, + PageMode::REDIRECT => $this->fail("Page mode is REDIRECT ($page->redirect) (only PAGE and DATA are supported)"), + PageMode::FILE => $this->fail("Page mode is FILE ($page->file) (only PAGE and DATA are supported)"), + PageMode::MANUAL => $this->fail("Page mode is MANUAL (only PAGE and DATA are supported)"), + default => $this->fail("Unknown page mode {$page->mode->name}"), // just for phpstan + }; } /** * Assert that the page contains the given text somewhere in the blocks */ - protected function assert_text(string $text, string $section = null): void + 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 + protected function assert_no_text(string $text, ?string $section = null): void { $this->assertStringNotContainsString($text, $this->page_to_text($section)); } @@ -222,21 +238,19 @@ protected function assert_search_results(array $tags, array $results): void $this->assertEquals($results, $ids); } - protected function assertException(string $type, callable $function): \Exception|null + protected function assertException(string $type, callable $function): \Exception { - $exception = null; try { call_user_func($function); - } catch (\Exception $e) { - $exception = $e; + self::fail("Expected exception of type $type, but none was thrown"); + } catch (\Exception $exception) { + self::assertThat( + $exception, + new \PHPUnit\Framework\Constraint\Exception($type), + "Expected exception of type $type, but got " . get_class($exception) + ); + return $exception; } - - 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 @@ -263,7 +277,7 @@ protected function post_image(string $filename, string $tags): int "filename" => $filename, "tags" => $tags, ])); - if(count($dae->images) == 0) { + if (count($dae->images) == 0) { throw new \Exception("Upload failed :("); } return $dae->images[0]->id; diff --git a/core/tests/BlockTest.php b/core/tests/BlockTest.php deleted file mode 100644 index b8ae0b78a..000000000 --- a/core/tests/BlockTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertEquals( - "

head

body
\n", - $b->get_html() - ); - } -} diff --git a/core/tests/BasePageTest.php b/core/tests/PageTest.php similarity index 87% rename from core/tests/BasePageTest.php rename to core/tests/PageTest.php index 64ed41e5f..4b76b5774 100644 --- a/core/tests/BasePageTest.php +++ b/core/tests/PageTest.php @@ -6,13 +6,13 @@ use PHPUnit\Framework\TestCase; -require_once "core/basepage.php"; +require_once "core/page.php"; -class BasePageTest extends TestCase +class PageTest extends TestCase { public function test_page(): void { - $page = new BasePage(); + $page = new Page(); $page->set_mode(PageMode::PAGE); ob_start(); $page->display(); @@ -22,7 +22,7 @@ public function test_page(): void public function test_file(): void { - $page = new BasePage(); + $page = new Page(); $page->set_mode(PageMode::FILE); $page->set_file("tests/pbx_screenshot.jpg"); ob_start(); @@ -33,7 +33,7 @@ public function test_file(): void public function test_data(): void { - $page = new BasePage(); + $page = new Page(); $page->set_mode(PageMode::DATA); $page->set_data("hello world"); ob_start(); @@ -44,7 +44,7 @@ public function test_data(): void public function test_redirect(): void { - $page = new BasePage(); + $page = new Page(); $page->set_mode(PageMode::REDIRECT); $page->set_redirect("/new/page"); ob_start(); diff --git a/core/tests/PolyfillsTest.php b/core/tests/PolyfillsTest.php index 03f95fe6c..06ea0212b 100644 --- a/core/tests/PolyfillsTest.php +++ b/core/tests/PolyfillsTest.php @@ -46,6 +46,8 @@ public function test_bool_escape(): void $this->assertTrue(bool_escape(true)); $this->assertFalse(bool_escape(false)); + $this->assertFalse(bool_escape(null)); + $this->assertTrue(bool_escape("true")); $this->assertFalse(bool_escape("false")); diff --git a/core/tests/SQLTest.php b/core/tests/SQLTest.php new file mode 100644 index 000000000..630435b58 --- /dev/null +++ b/core/tests/SQLTest.php @@ -0,0 +1,23 @@ +assertEquals( + "foobar", + $database->get_one("SELECT 'foo' || 'bar'") + ); + } +} diff --git a/core/tests/SearchTest.php b/core/tests/SearchTest.php index 9a346d617..5abafe1ad 100644 --- a/core/tests/SearchTest.php +++ b/core/tests/SearchTest.php @@ -190,7 +190,7 @@ private function assert_BSQ( int $limit = 9999, int $start = 0, array $res = [], - array $path = null, + ?array $path = null, ): void { global $database; diff --git a/core/tests/UrlsTest.php b/core/tests/UrlsTest.php index 598533630..a9ab22689 100644 --- a/core/tests/UrlsTest.php +++ b/core/tests/UrlsTest.php @@ -32,7 +32,7 @@ public function test_get_search_terms_from_search_link(): void }; global $config; - foreach([true, false] as $nice_urls) { + foreach ([true, false] as $nice_urls) { $config->set_bool(SetupConfig::NICE_URLS, $nice_urls); $this->assertEquals( @@ -54,7 +54,7 @@ public function test_get_search_terms_from_search_link(): void public function test_make_link(): void { global $config; - foreach([true, false] as $nice_urls) { + foreach ([true, false] as $nice_urls) { $config->set_bool(SetupConfig::NICE_URLS, $nice_urls); // basic @@ -93,7 +93,7 @@ public function test_make_link(): void public function test_search_link(): void { global $config; - foreach([true, false] as $nice_urls) { + foreach ([true, false] as $nice_urls) { $config->set_bool(SetupConfig::NICE_URLS, $nice_urls); $this->assertEquals( @@ -128,6 +128,20 @@ public function test_get_query(): void 'http://$SERVER/$INSTALL_DIR/index.php?q=$PATH should return $PATH' ); + // even when we are /test/... publicly, and generating /test/... URLs, + // we should still be able to handle URLs at the root because that's + // what apache sends us when it is reverse-proxying a subdirectory + $this->assertEquals( + "tasty/cake", + _get_query("/tasty/cake"), + 'http://$SERVER/$INSTALL_DIR/$PATH should return $PATH' + ); + $this->assertEquals( + "tasty/cake", + _get_query("/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"), diff --git a/core/tests/UtilTest.php b/core/tests/UtilTest.php index d99b1fa53..7846d2923 100644 --- a/core/tests/UtilTest.php +++ b/core/tests/UtilTest.php @@ -105,21 +105,57 @@ public function test_warehouse_path(): void ); } - public function test_load_balance_url(): void + public function test_load_balancing_parse(): void + { + $this->assertEquals( + ["foo" => 10, "bar" => 5, "baz" => 5, "quux" => 0], + parse_load_balancer_config("foo=10,bar=5,baz=5,quux=0") + ); + } + + public function test_load_balancing_choose(): void + { + $string_config = "foo=10,bar=5,baz=5,quux=0"; + $array_config = ["foo" => 10, "bar" => 5, "baz" => 5, "quux" => 0]; + $hash = "7ac19c10d6859415"; + + $this->assertEquals( + $array_config, + parse_load_balancer_config($string_config) + ); + $this->assertEquals( + "foo", + choose_load_balancer_node($array_config, $hash) + ); + + // Check that the balancing gives results in approximately + // the right ratio (compatible implmentations should give + // exactly these results) + $results = ["foo" => 0, "bar" => 0, "baz" => 0, "quux" => 0]; + for ($i = 0; $i < 2000; $i++) { + $results[choose_load_balancer_node($array_config, (string)$i)]++; + } + $this->assertEquals( + ["foo" => 1001, "bar" => 502, "baz" => 497, "quux" => 0], + $results + ); + } + + public function test_load_balancing_url(): void { $hash = "7ac19c10d6859415"; $ext = "jpg"; // pseudo-randomly select one of the image servers, balanced in given ratio $this->assertEquals( - "https://baz.mycdn.com/7ac19c10d6859415.jpg", - load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash) + "https://foo.mycdn.com/7ac19c10d6859415.jpg", + load_balance_url("https://{foo=10,bar=5,baz=5,quux=0}.mycdn.com/$hash.$ext", $hash) ); // N'th and N+1'th results should be different $this->assertNotEquals( - load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 0), - load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 1) + load_balance_url("https://{foo=10,bar=5,baz=5,quux=0}.mycdn.com/$hash.$ext", $hash, 0), + load_balance_url("https://{foo=10,bar=5,baz=5,quux=0}.mycdn.com/$hash.$ext", $hash, 1) ); } diff --git a/core/themelet.php b/core/themelet.php new file mode 100644 index 000000000..3195564b8 --- /dev/null +++ b/core/themelet.php @@ -0,0 +1,44 @@ +build_thumb_html($image); + } + + public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false): void + { + $c = self::get_common(); + assert(is_a($c, CommonElementsTheme::class)); + $c->display_paginator($page, $base, $query, $page_number, $total_pages, $show_random); + } +} diff --git a/core/urls.php b/core/urls.php index b6ecca4d4..8208dd5b9 100644 --- a/core/urls.php +++ b/core/urls.php @@ -6,10 +6,10 @@ class Link { - public ?string $page; + public string $page; public ?string $query; - public function __construct(?string $page = null, ?string $query = null) + public function __construct(string $page, ?string $query = null) { $this->page = $page; $this->query = $query; @@ -29,7 +29,7 @@ public function make_link(): string */ function search_link(array $terms = [], int $page = 1): string { - if($terms) { + if ($terms) { $q = url_escape(Tag::implode($terms)); return make_link("post/list/$q/$page"); } else { @@ -55,14 +55,18 @@ function make_link(?string $page = null, ?string $query = null, ?string $fragmen $parts = []; $install_dir = get_base_href(); - if (SPEED_HAX || $config->get_bool(SetupConfig::NICE_URLS, false)) { + if ($config->get_bool(SetupConfig::NICE_URLS, false)) { $parts['path'] = "$install_dir/$page"; } else { $parts['path'] = "$install_dir/index.php"; $query = empty($query) ? "q=$page" : "q=$page&$query"; } - $parts['query'] = $query; // http_build_query($query); - $parts['fragment'] = $fragment; // http_build_query($hash); + if (!is_null($query)) { + $parts['query'] = $query; // http_build_query($query); + } + if (!is_null($fragment)) { + $parts['fragment'] = $fragment; // http_build_query($hash); + } return unparse_url($parts); } @@ -83,6 +87,9 @@ function make_link(?string $page = null, ?string $query = null, ?string $fragmen * can parse it for ourselves * - generates * q=post%2Flist + * - When apache is reverse-proxying https://external.com/img/index.php + * to http://internal:8000/index.php, get_base_href() should return + * /img, however the URL in REQUEST_URI is /index.php, not /img/index.php * * This function should always return strings with no leading slashes */ @@ -90,23 +97,23 @@ 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, + // if we're looking at http://site.com/.../index.php, // then get the query from the "q" parameter - if(($parsed_url["path"] ?? "") == (get_base_href() . "/index.php")) { - // $q = $_GET["q"] ?? ""; + if (str_ends_with($parsed_url["path"] ?? "", "/index.php")) { // 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) { + // We can't just do `$q = $_GET["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") { + 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")) { + if (!str_contains($q, "/") && str_contains($q, "%2F")) { $q = rawurldecode($q); } } @@ -114,7 +121,19 @@ function _get_query(?string $uri = null): string // 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() . "/")); + $base = get_base_href(); + $q = $parsed_url["path"] ?? ""; + + // sometimes our public URL is /img/foo/bar but after + // reverse-proxying shimmie only sees /foo/bar, so only + // strip off the /img if it's actually there + if (str_starts_with($q, $base)) { + $q = substr($q, strlen($base)); + } + + // whether we are /img/foo/bar or /foo/bar, we still + // want to remove the leading slash + $q = ltrim($q, "/"); } assert(!str_starts_with($q, "/")); @@ -140,9 +159,9 @@ function get_base_href(?array $server_settings = null): string return BASE_HREF; } $server_settings = $server_settings ?? $_SERVER; - if(str_ends_with($server_settings['PHP_SELF'], 'index.php')) { + 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'])) { + } 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"); diff --git a/core/user.php b/core/user.php index ea0c3cafb..65ac3ec61 100644 --- a/core/user.php +++ b/core/user.php @@ -83,20 +83,27 @@ public function graphql_guid(): string public static function by_session(string $name, string $session): ?User { global $cache, $config, $database; - $row = $cache->get("user-session:$name-$session"); - if (is_null($row)) { - if ($database->get_driver_id() === DatabaseDriverID::MYSQL) { - $query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess"; - } else { - $query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess"; + $user = $cache->get("user-session-obj:$name-$session"); + if (is_null($user)) { + try { + $user_by_name = User::by_name($name); + } catch (UserNotFound $e) { + return null; + } + if ($user_by_name->get_session_id() === $session) { + $user = $user_by_name; + } + // For 2.12, check old session IDs and convert to new IDs + if (md5($user_by_name->passhash . get_session_ip($config)) === $session) { + $user = $user_by_name; + $user->set_login_cookie(); } - $row = $database->get_row($query, ["name" => $name, "ip" => get_session_ip($config), "sess" => $session]); - $cache->set("user-session:$name-$session", $row, 600); + $cache->set("user-session-obj:$name-$session", $user, 600); } - return is_null($row) ? null : new User($row); + return $user; } - public static function by_id(int $id): ?User + public static function by_id(int $id): User { global $cache, $database; if ($id === 1) { @@ -109,51 +116,55 @@ public static function by_id(int $id): ?User if ($id === 1) { $cache->set('user-id:'.$id, $row, 600); } - return is_null($row) ? null : new User($row); + if (is_null($row)) { + throw new UserNotFound("Can't find any user with ID $id"); + } + return new User($row); } #[Query(name: "user")] - public static function by_name(string $name): ?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]); - return is_null($row) ? null : new User($row); - } - - public static function name_to_id(string $name): int - { - $u = User::by_name($name); - if (is_null($u)) { + if (is_null($row)) { throw new UserNotFound("Can't find any user named $name"); } else { - return $u->id; + return new User($row); } } - public static function by_name_and_pass(string $name, string $pass): ?User + public static function name_to_id(string $name): int { - $my_user = User::by_name($name); + return User::by_name($name)->id; + } - // If user tried to log in as "foo bar" and failed, try "foo_bar" - if (!$my_user && str_contains($name, " ")) { - $my_user = User::by_name(str_replace(" ", "_", $name)); + public static function by_name_and_pass(string $name, string $pass): User + { + try { + $my_user = User::by_name($name); + } catch (UserNotFound $e) { + // If user tried to log in as "foo bar" and failed, try "foo_bar" + try { + $my_user = User::by_name(str_replace(" ", "_", $name)); + } catch (UserNotFound $e) { + log_warning("core-user", "Failed to log in as $name (Invalid username)"); + throw $e; + } } - if ($my_user) { - if ($my_user->passhash == md5(strtolower($name) . $pass)) { - log_info("core-user", "Migrating from md5 to bcrypt for $name"); - $my_user->set_password($pass); - } - if (password_verify($pass, $my_user->passhash)) { - log_info("core-user", "Logged in as $name ({$my_user->class->name})"); - return $my_user; - } else { - log_warning("core-user", "Failed to log in as $name (Invalid password)"); - } + if ($my_user->passhash == md5(strtolower($name) . $pass)) { + log_info("core-user", "Migrating from md5 to bcrypt for $name"); + $my_user->set_password($pass); + } + assert(!is_null($my_user->passhash)); + if (password_verify($pass, $my_user->passhash)) { + log_info("core-user", "Logged in as $name ({$my_user->class->name})"); + return $my_user; } else { - log_warning("core-user", "Failed to log in as $name (Invalid username)"); + log_warning("core-user", "Failed to log in as $name (Invalid password)"); + throw new UserNotFound("Can't find anybody with that username and password"); } - return null; } @@ -171,12 +182,6 @@ public function is_anonymous(): bool return ($this->id === $config->get_int('anon_id')); } - public function is_logged_in(): bool - { - global $config; - return ($this->id !== $config->get_int('anon_id')); - } - public function set_class(string $class): void { global $database; @@ -187,8 +192,11 @@ public function set_class(string $class): void public function set_name(string $name): void { global $database; - if (User::by_name($name)) { + try { + User::by_name($name); throw new InvalidInput("Desired username is already in use"); + } catch (UserNotFound $e) { + // if user is not found, we're good } $old_name = $this->name; $this->name = $name; @@ -246,19 +254,41 @@ public function get_avatar_url(): ?string /** * Get an auth token to be used in POST forms * - * password = secret, avoid storing directly - * passhash = bcrypt(password), so someone who gets to the database can't get passwords - * sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP, - * and it can't be used to get the passhash to generate new sesskeys - * authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that - * the form was generated within the session. Salted and re-hashed so that - * reading a web page from the user's cache doesn't give access to the session key + * the token is based on + * - the user's password, so that only this user can use the token + * - the session IP, to reduce the blast radius of guessed passwords + * - a salt known only to the server, so that clients or attackers + * can't generate their own tokens even if they know the first two */ public function get_auth_token(): string { global $config; - $salt = DATABASE_DSN; - $addr = get_session_ip($config); - return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt); + return hash("sha3-256", $this->passhash . get_session_ip($config) . SECRET); + } + + + public function get_session_id(): string + { + global $config; + return hash("sha3-256", $this->passhash . get_session_ip($config) . SECRET); + } + + public function set_login_cookie(): void + { + global $config, $page; + + $page->add_cookie( + "user", + $this->name, + time() + 60 * 60 * 24 * 365, + '/' + ); + $page->add_cookie( + "session", + $this->get_session_id(), + time() + 60 * 60 * 24 * $config->get_int('login_memory'), + '/' + ); } + } diff --git a/core/userclass.php b/core/userclass.php index 44c2b956b..b1cdda78f 100644 --- a/core/userclass.php +++ b/core/userclass.php @@ -19,7 +19,7 @@ class UserClass public static array $known_classes = []; #[Field] - public ?string $name = null; + public string $name; public ?UserClass $parent = null; public bool $core = false; diff --git a/core/util.php b/core/util.php index aa4efb598..23f3a3190 100644 --- a/core/util.php +++ b/core/util.php @@ -21,6 +21,23 @@ function get_theme(): string return $theme; } +function get_theme_class(string $class): ?object +{ + $theme = ucfirst(get_theme()); + $options = [ + "\\Shimmie2\\$theme$class", + "\\Shimmie2\\Custom$class", + "\\Shimmie2\\$class", + ]; + foreach ($options as $option) { + if (class_exists($option)) { + return new $option(); + } + } + return null; +} + + function contact_link(?string $contact = null): ?string { global $config; @@ -41,7 +58,7 @@ function contact_link(?string $contact = null): ?string return "mailto:$text"; } - if (str_contains($text, "/")) { + if (str_contains($text, "/") && mb_substr($text, 0, 1) != "/") { return "https://$text"; } @@ -151,21 +168,34 @@ function check_im_version(): int function is_trusted_proxy(): bool { $ra = $_SERVER['REMOTE_ADDR'] ?? "0.0.0.0"; - if(!defined("TRUSTED_PROXIES")) { + if (!defined("TRUSTED_PROXIES")) { return false; } // @phpstan-ignore-next-line - TRUSTED_PROXIES is defined in config - foreach(TRUSTED_PROXIES as $proxy) { - if($ra === $proxy) { // check for "unix:" before checking IPs + foreach (TRUSTED_PROXIES as $proxy) { + // @phpstan-ignore-next-line - TRUSTED_PROXIES is defined in config + if ($ra === $proxy) { // check for "unix:" before checking IPs return true; } - if(ip_in_range($ra, $proxy)) { + if (ip_in_range($ra, $proxy)) { return true; } } return false; } +function is_bot(): bool +{ + $ua = $_SERVER["HTTP_USER_AGENT"] ?? "No UA"; + return ( + str_contains($ua, "Googlebot") + || str_contains($ua, "YandexBot") + || str_contains($ua, "bingbot") + || str_contains($ua, "msnbot") + || str_contains($ua, "PetalBot") + ); +} + /** * Get real IP if behind a reverse proxy */ @@ -173,13 +203,13 @@ function get_real_ip(): string { $ip = $_SERVER['REMOTE_ADDR']; - if($ip == "unix:") { + if ($ip == "unix:") { $ip = "0.0.0.0"; } - if(is_trusted_proxy()) { + if (is_trusted_proxy()) { if (isset($_SERVER['HTTP_X_REAL_IP'])) { - if(filter_var_ex($ip, FILTER_VALIDATE_IP)) { + if (filter_var_ex($ip, FILTER_VALIDATE_IP)) { $ip = $_SERVER['HTTP_X_REAL_IP']; } } @@ -187,7 +217,7 @@ function get_real_ip(): string 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)) { + if (filter_var_ex($last_ip, FILTER_VALIDATE_IP)) { $ip = $last_ip; } } @@ -273,44 +303,6 @@ function data_path(string $filename, bool $create = true): string return $filename; } -function load_balance_url(string $tmpl, string $hash, int $n = 0): string -{ - static $flexihashes = []; - $matches = []; - if (preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) { - $pre = $matches[1]; - $opts = $matches[2]; - $post = $matches[3]; - - if (isset($flexihashes[$opts])) { - $flexihash = $flexihashes[$opts]; - } else { - $flexihash = new \Flexihash\Flexihash(); - foreach (explode(",", $opts) as $opt) { - $parts = explode("=", $opt); - $parts_count = count($parts); - $opt_val = ""; - $opt_weight = 0; - if ($parts_count === 2) { - $opt_val = $parts[0]; - $opt_weight = (int)$parts[1]; - } elseif ($parts_count === 1) { - $opt_val = $parts[0]; - $opt_weight = 1; - } - $flexihash->addTarget($opt_val, $opt_weight); - } - $flexihashes[$opts] = $flexihash; - } - - // $choice = $flexihash->lookup($pre.$post); - $choices = $flexihash->lookupList($hash, $n + 1); // hash doesn't change - $choice = $choices[$n]; - $tmpl = $pre . $choice . $post; - } - return $tmpl; -} - class FetchException extends \Exception { } @@ -354,7 +346,7 @@ function fetch_url(string $url, string $mfile): array $s_url = escapeshellarg($url); $s_mfile = escapeshellarg($mfile); system("wget --no-check-certificate $s_url --output-document=$s_mfile"); - if(!file_exists($mfile)) { + if (!file_exists($mfile)) { throw new FetchException("wget failed"); } $headers = []; @@ -549,7 +541,7 @@ function get_debug_info(): string { $d = get_debug_info_arr(); - $debug = "
Took {$d['time']} seconds (db:{$d['dbtime']}) and {$d['mem_mb']}MB of RAM"; + $debug = "Took {$d['time']} seconds (db:{$d['dbtime']}) and {$d['mem_mb']}MB of RAM"; $debug .= "; Used {$d['files']} files and {$d['query_count']} queries"; $debug .= "; Sent {$d['event_count']} events"; $debug .= "; {$d['cache_hits']} cache hits and {$d['cache_misses']} misses"; @@ -622,7 +614,6 @@ function _load_theme_files(): void { $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')); } @@ -689,7 +680,7 @@ function _fatal_error(\Exception $e): void $code = is_a($e, SCoreException::class) ? $e->http_code : 500; $q = ""; - if(is_a($e, DatabaseException::class)) { + if (is_a($e, DatabaseException::class)) { $q .= "

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

Args: " . html_escape(var_export($e->args, true)); } @@ -708,7 +699,7 @@ function _fatal_error(\Exception $e): void

Message: '.html_escape($message).' '.$q.'

Version: '.$version.' (on '.$phpver.') -

Stack Trace:

'.$e->getTraceAsString().'
+

Stack Trace:

'.$e->getTraceAsString().'
'; @@ -734,7 +725,6 @@ function _get_user(): User if (is_null($my_user)) { $my_user = User::by_id($config->get_int("anon_id", 0)); } - assert(!is_null($my_user)); return $my_user; } @@ -806,9 +796,93 @@ function generate_key(int $length = 20): string function shm_tempnam(string $prefix = ""): string { - if(!is_dir("data/temp")) { + if (!is_dir("data/temp")) { mkdir("data/temp"); } $temp = \Safe\realpath("data/temp"); return \Safe\tempnam($temp, $prefix); } + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\ +* Load balancing * +\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +function load_balance_url(string $tmpl, string $hash, int $n = 0): string +{ + $matches = []; + if (preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) { + $pre = $matches[1]; + $opts = $matches[2]; + $post = $matches[3]; + + $nodes = parse_load_balancer_config($opts); + $choice = choose_load_balancer_node($nodes, $hash, $n); + + $tmpl = $pre . $choice . $post; + } + return $tmpl; +} + +/** + * "foo=1,bar=2,baz=3" -> ['foo' => 1, 'bar' => 2, 'baz' => 3] + * + * @param string $s + * @throws \Shimmie2\InvalidInput + * @return array + */ +function parse_load_balancer_config(string $s): array +{ + $nodes = []; + + foreach (explode(",", $s) as $opt) { + $parts = explode("=", $opt); + $parts_count = count($parts); + if ($parts_count === 2) { + $opt_val = $parts[0]; + $opt_weight = (int)$parts[1]; + } elseif ($parts_count === 1) { + $opt_val = $parts[0]; + $opt_weight = 1; + } else { + throw new InvalidInput("Invalid load balancer weights: $s"); + } + $nodes[$opt_val] = $opt_weight; + } + + return $nodes; +} + +/** + * Choose a node from a list of nodes based on a key. + * + * @param array $nodes + * @param string $key + * @param int $n + * @return string + */ +function choose_load_balancer_node(array $nodes, string $key, int $n = 0): string +{ + if (count($nodes) === 0) { + throw new InvalidInput("No load balancer nodes to choose from"); + } + + // create a list of [score, node] pairs + $results = []; + foreach ($nodes as $node => $weight) { + // hash the node + key as an unsigned 32-bit integer + $u32hash = hexdec(hash("murmur3a", "$node: $key")); + // turn that into a float between 0 and 1 + $f32hash = ($u32hash + 1) / (1 << 32); + // $hash * $weight gives an exponential bias to higher-weighted nodes, + // 1/log($hash)*$weight gives a uniform distribution across the range + $score = (1.0 / -log($f32hash)) * $weight; + $results[] = [$score, $node]; + } + + // sort by score, highest first + rsort($results); + + // return the highest node, fall back to the second-highest, etc + return $results[$n % count($results)][1]; +} diff --git a/ext/admin/theme.php b/ext/admin/theme.php index 521e0104f..784135646 100644 --- a/ext/admin/theme.php +++ b/ext/admin/theme.php @@ -14,7 +14,6 @@ public function display_page(): void global $page; $page->set_title("Admin Tools"); - $page->set_heading("Admin Tools"); $page->add_block(new NavBlock()); } } diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php index 1957ff667..a23c78ccd 100644 --- a/ext/alias_editor/main.php +++ b/ext/alias_editor/main.php @@ -103,7 +103,7 @@ public function onPageRequest(PageRequestEvent $event): void $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("alias/list")); } else { - $this->theme->display_error(400, "No File Specified", "You have to upload a file"); + throw new InvalidInput("No File Specified"); } } } @@ -175,6 +175,8 @@ private function add_alias_csv(string $csv): int foreach (explode("\n", $csv) as $line) { $parts = str_getcsv($line); if (count($parts) == 2) { + assert(is_string($parts[0])); + assert(is_string($parts[1])); send_event(new AddAliasEvent($parts[0], $parts[1])); $i++; } diff --git a/ext/alias_editor/theme.php b/ext/alias_editor/theme.php index af21f8ff9..7a3604f42 100644 --- a/ext/alias_editor/theme.php +++ b/ext/alias_editor/theme.php @@ -7,7 +7,8 @@ use MicroHTML\HTMLElement; use function MicroHTML\emptyHTML; -use function MicroHTML\{BR,INPUT}; +use function MicroHTML\rawHTML; +use function MicroHTML\{BR,CODE,INPUT}; class AliasEditorTheme extends Themelet { @@ -18,7 +19,9 @@ public function display_aliases(HTMLElement $table, HTMLElement $paginator): voi { global $page, $user; - $html = emptyHTML($table, BR(), $paginator, BR(), SHM_A("alias/export/aliases.csv", "Download as CSV", args: ["download" => "aliases.csv"])); + $info_html = rawHTML("A tag alias replaces a tag with another tag or tags.".BR()."A tag implication (where the old tag stays and adds a new tag) is made by including the old tag in the list of new tags (".CODE("fox")." → ".CODE("fox canine").")".BR()); + + $html = emptyHTML($info_html, BR(), $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( @@ -28,7 +31,6 @@ public function display_aliases(HTMLElement $table, HTMLElement $paginator): voi $bulk_html = emptyHTML($bulk_form); $page->set_title("Alias List"); - $page->set_heading("Alias List"); $page->add_block(new NavBlock()); $page->add_block(new Block("Aliases", $html)); diff --git a/ext/approval/main.php b/ext/approval/main.php index 200d559ba..c0023d71c 100644 --- a/ext/approval/main.php +++ b/ext/approval/main.php @@ -153,7 +153,7 @@ public function onHelpPageBuilding(HelpPageBuildingEvent $event): void global $user, $config; 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())); + $event->add_section("Approval", $this->theme->get_help_html()); } } } diff --git a/ext/approval/theme.php b/ext/approval/theme.php index 28aee0352..e612e8f21 100644 --- a/ext/approval/theme.php +++ b/ext/approval/theme.php @@ -7,7 +7,6 @@ use MicroHTML\HTMLElement; use function MicroHTML\emptyHTML; - use function MicroHTML\{BUTTON,P}; class ApprovalTheme extends Themelet diff --git a/ext/artists/main.php b/ext/artists/main.php index 9ea279cea..11ae7b74f 100644 --- a/ext/artists/main.php +++ b/ext/artists/main.php @@ -32,7 +32,9 @@ class Artists extends Extension public function onInitExt(InitExtEvent $event): void { + global $config; Image::$prop_types["author"] = ImagePropType::STRING; + $config->set_default_int("artistsPerPage", 20); } public function onImageInfoSet(ImageInfoSetEvent $event): void @@ -69,7 +71,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $event->add_block(new Block("Artist", $this->theme->get_help_html())); + $event->add_section("Artist", $this->theme->get_help_html()); } } @@ -120,7 +122,6 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void "); $database->execute("ALTER TABLE images ADD COLUMN author VARCHAR(255) NULL"); - $config->set_int("artistsPerPage", 20); $this->set_version("ext_artists_version", 1); } } @@ -178,7 +179,7 @@ public function onPageRequest(PageRequestEvent $event): void if (!$user->is_anonymous()) { $this->theme->new_artist_composer(); } else { - $this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist."); + throw new PermissionDenied("You must be registered and logged in to create a new artist."); } } if ($event->page_matches("artist/new_artist")) { @@ -188,14 +189,10 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("artist/create")) { if (!$user->is_anonymous()) { $newArtistID = $this->add_artist(); - if ($newArtistID == -1) { - $this->theme->display_error(400, "Error", "Error when entering artist data."); - } else { - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("artist/view/" . $newArtistID)); - } + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("artist/view/" . $newArtistID)); } else { - $this->theme->display_error(401, "Error", "You must be registered and logged in to create a new artist."); + throw new PermissionDenied("You must be registered and logged in to create a new artist."); } } if ($event->page_matches("artist/view/{artistID}")) { @@ -234,7 +231,7 @@ public function onPageRequest(PageRequestEvent $event): void $userIsAdmin = $user->can(Permissions::ARTISTS_ADMIN); $this->theme->sidebar_options("editor", $artistID, $userIsAdmin); } else { - $this->theme->display_error(401, "Error", "You must be registered and logged in to edit an artist."); + throw new PermissionDenied("You must be registered and logged in to edit an artist."); } } if ($event->page_matches("artist/edit_artist")) { @@ -636,7 +633,7 @@ private function add_artist(): int $name = $inputs["name"]; if (str_contains($name, " ")) { - return -1; + throw new InvalidInput("Artist name cannot contain spaces"); } $notes = $inputs["notes"]; diff --git a/ext/artists/theme.php b/ext/artists/theme.php index 8f52ef117..bb2a0f40f 100644 --- a/ext/artists/theme.php +++ b/ext/artists/theme.php @@ -6,8 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\emptyHTML; -use function MicroHTML\{INPUT,P}; +use function MicroHTML\{INPUT,P,rawHTML,emptyHTML}; /** * @phpstan-type ArtistArtist array{id:int,artist_id:int,user_name:string,name:string,notes:string,type:string,posts:int} @@ -72,7 +71,7 @@ public function sidebar_options(string $mode, ?int $artistID = null, bool $is_ad } if ($html) { - $page->add_block(new Block("Manage Artists", $html, "left", 10)); + $page->add_block(new Block("Manage Artists", rawHTML($html), "left", 10)); } } @@ -137,7 +136,7 @@ public function show_artist_editor(array $artist, array $aliases, array $members '; global $page; - $page->add_block(new Block("Edit artist", $html, "main", 10)); + $page->add_block(new Block("Edit artist", rawHTML($html), "main", 10)); } public function new_artist_composer(): void @@ -156,8 +155,7 @@ public function new_artist_composer(): void "; $page->set_title("Artists"); - $page->set_heading("Artists"); - $page->add_block(new Block("Artists", $html, "main", 10)); + $page->add_block(new Block("Artists", rawHTML($html), "main", 10)); } /** @@ -236,8 +234,7 @@ public function list_artists(array $artists, int $pageNumber, int $totalPages): $html .= ""; $page->set_title("Artists"); - $page->set_heading("Artists"); - $page->add_block(new Block("Artists", $html, "main", 10)); + $page->add_block(new Block("Artists", rawHTML($html), "main", 10)); $this->display_paginator($page, "artist/list", null, $pageNumber, $totalPages); } @@ -256,7 +253,7 @@ public function show_new_alias_composer(int $artistID): void '; global $page; - $page->add_block(new Block("Artist Aliases", $html, "main", 20)); + $page->add_block(new Block("Artist Aliases", rawHTML($html), "main", 20)); } public function show_new_member_composer(int $artistID): void @@ -273,7 +270,7 @@ public function show_new_member_composer(int $artistID): void '; global $page; - $page->add_block(new Block("Artist members", $html, "main", 30)); + $page->add_block(new Block("Artist members", rawHTML($html), "main", 30)); } public function show_new_url_composer(int $artistID): void @@ -290,7 +287,7 @@ public function show_new_url_composer(int $artistID): void '; global $page; - $page->add_block(new Block("Artist URLs", $html, "main", 40)); + $page->add_block(new Block("Artist URLs", rawHTML($html), "main", 40)); } /** @@ -309,7 +306,7 @@ public function show_alias_editor(array $alias): void '; global $page; - $page->add_block(new Block("Edit Alias", $html, "main", 10)); + $page->add_block(new Block("Edit Alias", rawHTML($html), "main", 10)); } /** @@ -328,7 +325,7 @@ public function show_url_editor(array $url): void '; global $page; - $page->add_block(new Block("Edit URL", $html, "main", 10)); + $page->add_block(new Block("Edit URL", rawHTML($html), "main", 10)); } /** @@ -347,7 +344,7 @@ public function show_member_editor(array $member): void '; global $page; - $page->add_block(new Block("Edit Member", $html, "main", 10)); + $page->add_block(new Block("Edit Member", rawHTML($html), "main", 10)); } /** @@ -409,8 +406,7 @@ public function show_artist(array $artist, array $aliases, array $members, array "; $page->set_title("Artist"); - $page->set_heading("Artist"); - $page->add_block(new Block("Artist", $html, "main", 10)); + $page->add_block(new Block("Artist", rawHTML($html), "main", 10)); //we show the images for the artist $artist_images = ""; @@ -422,7 +418,7 @@ public function show_artist(array $artist, array $aliases, array $members, array ''; } - $page->add_block(new Block("Artist Posts", $artist_images, "main", 20)); + $page->add_block(new Block("Artist Posts", rawHTML($artist_images), "main", 20)); } /** diff --git a/ext/auto_tagger/main.php b/ext/auto_tagger/main.php index eca01d144..46bec3a0e 100644 --- a/ext/auto_tagger/main.php +++ b/ext/auto_tagger/main.php @@ -109,7 +109,7 @@ public function onPageRequest(PageRequestEvent $event): void $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("auto_tag/list")); } else { - $this->theme->display_error(400, "No File Specified", "You have to upload a file"); + throw new InvalidInput("No File Specified"); } } } @@ -187,6 +187,8 @@ private function add_auto_tag_csv(string $csv): int foreach (explode("\n", $csv) as $line) { $parts = str_getcsv($line); if (count($parts) == 2) { + assert(is_string($parts[0])); + assert(is_string($parts[1])); send_event(new AddAutoTagEvent($parts[0], $parts[1])); $i++; } diff --git a/ext/auto_tagger/theme.php b/ext/auto_tagger/theme.php index c0e012a07..e2f9c3f3d 100644 --- a/ext/auto_tagger/theme.php +++ b/ext/auto_tagger/theme.php @@ -6,6 +6,8 @@ use MicroHTML\HTMLElement; +use function MicroHTML\rawHTML; + class AutoTaggerTheme extends Themelet { /** @@ -32,11 +34,10 @@ public function display_auto_tagtable(HTMLElement $table, HTMLElement $paginator "; $page->set_title("Auto-Tag List"); - $page->set_heading("Auto-Tag List"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Auto-Tag", $html)); + $page->add_block(new Block("Auto-Tag", rawHTML($html))); if ($can_manage) { - $page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51)); + $page->add_block(new Block("Bulk Upload", rawHTML($bulk_html), "main", 51)); } } } diff --git a/ext/autocomplete/main.php b/ext/autocomplete/main.php index f57e85fea..83a0ceef6 100644 --- a/ext/autocomplete/main.php +++ b/ext/autocomplete/main.php @@ -87,7 +87,7 @@ private function complete(string $search, int $limit): array $SQLarr ); $ret = []; - foreach($rows as $row) { + foreach ($rows as $row) { $ret[(string)$row['tag']] = [ "newtag" => $row["newtag"], "count" => $row["count"], diff --git a/ext/autocomplete/script.js b/ext/autocomplete/script.js index 910026ddd..289643e04 100644 --- a/ext/autocomplete/script.js +++ b/ext/autocomplete/script.js @@ -43,7 +43,7 @@ function updateCompletions(element) { else { element.completer_timeout = setTimeout(() => { const wordWithoutMinus = word.replace(/^-/, ''); - fetch((document.body.getAttribute("data-base-href") ?? "") + '/api/internal/autocomplete?s=' + wordWithoutMinus).then( + fetch(shm_make_link('api/internal/autocomplete', {s: wordWithoutMinus})).then( (response) => response.json() ).then((json) => { if(element.selected_completion !== -1) { diff --git a/ext/ban_words/info.php b/ext/ban_words/info.php index 0c9f549e1..fb438dd7b 100644 --- a/ext/ban_words/info.php +++ b/ext/ban_words/info.php @@ -15,7 +15,7 @@ class BanWordsInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "For stopping spam and other comment abuse"; public ?string $documentation = -"Allows an administrator to ban certain words + "Allows an administrator to ban certain words from comments. This can be a very simple but effective way of stopping spam; just add \"viagra\", \"porn\", etc to the banned words list. diff --git a/ext/bbcode/info.php b/ext/bbcode/info.php index c0b46095d..f893ad864 100644 --- a/ext/bbcode/info.php +++ b/ext/bbcode/info.php @@ -16,7 +16,7 @@ class BBCodeInfo extends ExtensionInfo public bool $core = true; public string $description = "Turns BBCode into HTML"; public ?string $documentation = -" Basic formatting tags: + " Basic formatting tags:
  • [b]bold[/b]
  • [i]italic[/i] @@ -34,6 +34,7 @@ class BBCodeInfo extends ExtensionInfo Link tags:
    • [img]url[/img] +
    • [img]site://_images/image.jpg[/img]
    • [url]https://code.shishnet.org/[/url]
    • [url=https://code.shishnet.org/]some text[/url]
    • [url]site://ext_doc/bbcode[/url] @@ -51,7 +52,7 @@ class BBCodeInfo extends ExtensionInfo
    • [ul]Unordered list[/ul]
    • [ol]Ordered list[/ol]
    • [li]List Item[/li] -
    • [code]
      print(\"Hello World!\");
      [/code] +
    • [code]
      print(\"Hello World!\");
      [/code]
    • [spoiler]Voldemort is bad[/spoiler]
    • [quote]
      To be or not to be...
      [/quote]
    • [quote=Shakespeare]
      Shakespeare said:
      ... That is the question
      [/quote] diff --git a/ext/bbcode/main.php b/ext/bbcode/main.php index 8746a0504..358a43d5b 100644 --- a/ext/bbcode/main.php +++ b/ext/bbcode/main.php @@ -18,36 +18,37 @@ public function _format(string $text): string foreach ([ "b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4", ] as $el) { - $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", "<$el>$1", $text); + $text = preg_replace_ex("!\[$el\](.*?)\[/$el\]!s", "<$el>$1", $text); } - $text = preg_replace('!^>>([^\d].+)!', '
      $1
      ', $text); - $text = preg_replace('!>>(\d+)(#c?\d+)?!s', '>>$1$2', $text); - $text = preg_replace('!\[anchor=(.*?)\](.*?)\[/anchor\]!s', '$2 ', $text); // add "bb-" to avoid clashing with eg #top - $text = preg_replace('!\[url=site://(.*?)(#c\d+)?\](.*?)\[/url\]!s', '$3', $text); - $text = preg_replace('!\[url\]site://(.*?)(#c\d+)?\[/url\]!s', '$1$2', $text); - $text = preg_replace('!\[url=((?:https?|ftp|irc|mailto)://.*?)\](.*?)\[/url\]!s', '$2', $text); - $text = preg_replace('!\[url\]((?:https?|ftp|irc|mailto)://.*?)\[/url\]!s', '$1', $text); - $text = preg_replace('!\[email\](.*?)\[/email\]!s', '$1', $text); - $text = preg_replace('!\[img\](https?:\/\/.*?)\[/img\]!s', 'user image', $text); - $text = preg_replace('!\[\[([^\|\]]+)\|([^\]]+)\]\]!s', '$2', $text); - $text = preg_replace('!\[\[([^\]]+)\]\]!s', '$1', $text); - $text = preg_replace("!\n\s*\n!", "\n\n", $text); + $text = preg_replace_ex('!^>>([^\d].+)!', '
      $1
      ', $text); + $text = preg_replace_ex('!>>(\d+)(#c?\d+)?!s', '>>$1$2', $text); + $text = preg_replace_ex('!\[anchor=(.*?)\](.*?)\[/anchor\]!s', '$2 ', $text); // add "bb-" to avoid clashing with eg #top + $text = preg_replace_ex('!\[url=site://(.*?)(#c\d+)?\](.*?)\[/url\]!s', '$3', $text); + $text = preg_replace_ex('!\[url\]site://(.*?)(#c\d+)?\[/url\]!s', '$1$2', $text); + $text = preg_replace_ex('!\[url=((?:https?|ftp|irc|mailto)://.*?)\](.*?)\[/url\]!s', '$2', $text); + $text = preg_replace_ex('!\[url\]((?:https?|ftp|irc|mailto)://.*?)\[/url\]!s', '$1', $text); + $text = preg_replace_ex('!\[email\](.*?)\[/email\]!s', '$1', $text); + $text = preg_replace_ex('!\[img\](https?:\/\/.*?)\[/img\]!s', 'user image', $text); + $text = preg_replace_ex('!\[img\]site://(.*?)(#c\d+)?\[/img\]!s', 'user image', $text); + $text = preg_replace_ex('!\[\[([^\|\]]+)\|([^\]]+)\]\]!s', '$2', $text); + $text = preg_replace_ex('!\[\[([^\]]+)\]\]!s', '$1', $text); + $text = preg_replace_ex("!\n\s*\n!", "\n\n", $text); $text = str_replace("\n", "\n
      ", $text); - $text = preg_replace("/\[quote\](.*?)\[\/quote\]/s", "
      \\1
      ", $text); - $text = preg_replace("/\[quote=(.*?)\](.*?)\[\/quote\]/s", "
      \\1 said:
      \\2
      ", $text); + $text = preg_replace_ex("/\[quote\](.*?)\[\/quote\]/s", "
      \\1
      ", $text); + $text = preg_replace_ex("/\[quote=(.*?)\](.*?)\[\/quote\]/s", "
      \\1 said:
      \\2
      ", $text); while (preg_match("/\[list\](.*?)\[\/list\]/s", $text)) { - $text = preg_replace("/\[list\](.*?)\[\/list\]/s", "
        \\1
      ", $text); + $text = preg_replace_ex("/\[list\](.*?)\[\/list\]/s", "
        \\1
      ", $text); } while (preg_match("/\[ul\](.*?)\[\/ul\]/s", $text)) { - $text = preg_replace("/\[ul\](.*?)\[\/ul\]/s", "
        \\1
      ", $text); + $text = preg_replace_ex("/\[ul\](.*?)\[\/ul\]/s", "
        \\1
      ", $text); } while (preg_match("/\[ol\](.*?)\[\/ol\]/s", $text)) { - $text = preg_replace("/\[ol\](.*?)\[\/ol\]/s", "
        \\1
      ", $text); + $text = preg_replace_ex("/\[ol\](.*?)\[\/ol\]/s", "
        \\1
      ", $text); } - $text = preg_replace("/\[li\](.*?)\[\/li\]/s", "
    • \\1
    • ", $text); - $text = preg_replace("#\[\*\]#s", "
    • ", $text); - $text = preg_replace("#
      <(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text); - $text = preg_replace("#\[align=(left|center|right)\](.*?)\[\/align\]#s", "
      \\2
      ", $text); + $text = preg_replace_ex("/\[li\](.*?)\[\/li\]/s", "
    • \\1
    • ", $text); + $text = preg_replace_ex("#\[\*\]#s", "
    • ", $text); + $text = preg_replace_ex("#
      <(li|ul|ol|/ul|/ol)>#s", "<\\1>", $text); + $text = preg_replace_ex("#\[align=(left|center|right)\](.*?)\[\/align\]#s", "
      \\2
      ", $text); $text = $this->filter_spoiler($text); $text = $this->insert_code($text); return $text; @@ -59,17 +60,17 @@ public function strip(string $text): string "b", "i", "u", "s", "sup", "sub", "h1", "h2", "h3", "h4", "code", "url", "email", "li", ] as $el) { - $text = preg_replace("!\[$el\](.*?)\[/$el\]!s", '$1', $text); + $text = preg_replace_ex("!\[$el\](.*?)\[/$el\]!s", '$1', $text); } - $text = preg_replace("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text); - $text = preg_replace("!\[url=(.*?)\](.*?)\[/url\]!s", '$2', $text); - $text = preg_replace("!\[img\](.*?)\[/img\]!s", "", $text); - $text = preg_replace("!\[\[([^\|\]]+)\|([^\]]+)\]\]!s", '$2', $text); - $text = preg_replace("!\[\[([^\]]+)\]\]!s", '$1', $text); - $text = preg_replace("!\[quote\](.*?)\[/quote\]!s", "", $text); - $text = preg_replace("!\[quote=(.*?)\](.*?)\[/quote\]!s", "", $text); - $text = preg_replace("!\[/?(list|ul|ol)\]!", "", $text); - $text = preg_replace("!\[\*\](.*?)!s", '$1', $text); + $text = preg_replace_ex("!\[anchor=(.*?)\](.*?)\[/anchor\]!s", '$2', $text); + $text = preg_replace_ex("!\[url=(.*?)\](.*?)\[/url\]!s", '$2', $text); + $text = preg_replace_ex("!\[img\](.*?)\[/img\]!s", "", $text); + $text = preg_replace_ex("!\[\[([^\|\]]+)\|([^\]]+)\]\]!s", '$2', $text); + $text = preg_replace_ex("!\[\[([^\]]+)\]\]!s", '$1', $text); + $text = preg_replace_ex("!\[quote\](.*?)\[/quote\]!s", "", $text); + $text = preg_replace_ex("!\[quote=(.*?)\](.*?)\[/quote\]!s", "", $text); + $text = preg_replace_ex("!\[/?(list|ul|ol)\]!", "", $text); + $text = preg_replace_ex("!\[\*\](.*?)!s", '$1', $text); $text = $this->strip_spoiler($text); return $text; } @@ -164,7 +165,7 @@ private function insert_code(string $text): string $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/style.css b/ext/bbcode/style.css index 525ff5c54..692cdf8d4 100644 --- a/ext/bbcode/style.css +++ b/ext/bbcode/style.css @@ -1,7 +1,3 @@ -.bbcode PRE.code { - background: #DEDEDE; - font-size: 0.9rem; -} .bbcode BLOCKQUOTE { border: 1px solid black; padding: 8px; diff --git a/ext/bbcode/test.php b/ext/bbcode/test.php index 790948410..bf6684559 100644 --- a/ext/bbcode/test.php +++ b/ext/bbcode/test.php @@ -37,7 +37,7 @@ public function testFailure(): void public function testCode(): void { $this->assertEquals( - "
      [b]bold[/b]
      ", + "
      [b]bold[/b]
      ", $this->filter("[code][b]bold[/b][/code]") ); } diff --git a/ext/biography/main.php b/ext/biography/main.php index cb91b1542..312a07bf2 100644 --- a/ext/biography/main.php +++ b/ext/biography/main.php @@ -27,7 +27,9 @@ public function onPageRequest(PageRequestEvent $event): void { global $page, $user, $user_config; if ($event->page_matches("biography", method: "POST")) { - $user_config->set_string("biography", $event->get_POST('biography')); + $bio = $event->get_POST('biography'); + log_info("biography", "Set biography to $bio"); + $user_config->set_string("biography", $bio); $page->flash("Bio Updated"); $page->set_mode(PageMode::REDIRECT); $page->set_redirect(referer_or(make_link())); diff --git a/ext/biography/theme.php b/ext/biography/theme.php index 37e91e862..44eb78fb9 100644 --- a/ext/biography/theme.php +++ b/ext/biography/theme.php @@ -4,13 +4,13 @@ namespace Shimmie2; -use function MicroHTML\TEXTAREA; +use function MicroHTML\{TEXTAREA,rawHTML}; class BiographyTheme extends Themelet { public function display_biography(Page $page, string $bio): void { - $page->add_block(new Block("About Me", format_text($bio), "main", 30, "about-me")); + $page->add_block(new Block("About Me", rawHTML(format_text($bio)), "main", 30, "about-me")); } public function display_composer(Page $page, string $bio): void diff --git a/ext/blocks/info.php b/ext/blocks/info.php index 7dca5bd37..0c1dd9a3c 100644 --- a/ext/blocks/info.php +++ b/ext/blocks/info.php @@ -14,4 +14,13 @@ class BlocksInfo extends ExtensionInfo public array $authors = self::SHISH_AUTHOR; public string $license = self::LICENSE_GPLV2; public string $description = "Add HTML to some space (News, Ads, etc)"; + public ?string $documentation = + "Blocks with lower priority number appear higher up the page.

      + The userclass parameter can be left empty for blocks to show to everyone, or specified as a comma-separated, case-insensitive list of user classes that will see that block. Spaces around the comma get stripped.

      + For example, to show ads only to regular users and anonymous user classes: +
        +
      • \"<ad here>\": user, anonymous
      • +
      • \"Thanks for supporting us!\": supporter
      • +
      + "; } diff --git a/ext/blocks/theme.php b/ext/blocks/theme.php index fc225d537..4404703a6 100644 --- a/ext/blocks/theme.php +++ b/ext/blocks/theme.php @@ -73,7 +73,6 @@ public function display_blocks(array $blocks): void )); $page->set_title("Blocks"); - $page->set_heading("Blocks"); $page->add_block(new NavBlock()); $page->add_block(new Block("Block Editor", $html)); } diff --git a/ext/blotter/theme.php b/ext/blotter/theme.php index 5c076b71c..6ef228794 100644 --- a/ext/blotter/theme.php +++ b/ext/blotter/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + /** * @phpstan-type BlotterEntry array{id:int,entry_date:string,entry_text:string,important:bool} */ @@ -17,9 +19,8 @@ public function display_editor(array $entries): void global $page; $html = $this->get_html_for_blotter_editor($entries); $page->set_title("Blotter Editor"); - $page->set_heading("Blotter Editor"); - $page->add_block(new Block("Welcome to the Blotter Editor!", $html, "main", 10)); - $page->add_block(new Block("Navigation", "Index", "left", 0)); + $page->add_block(new Block("Welcome to the Blotter Editor!", rawHTML($html), "main", 10)); + $page->add_block(new NavBlock()); } /** @@ -30,8 +31,7 @@ public function display_blotter_page(array $entries): void global $page; $html = $this->get_html_for_blotter_page($entries); $page->set_title("Blotter"); - $page->set_heading("Blotter"); - $page->add_block(new Block("Blotter Entries", $html, "main", 10)); + $page->add_block(new Block("Blotter Entries", rawHTML($html), "main", 10)); } /** @@ -42,7 +42,7 @@ public function display_blotter(array $entries): void global $page, $config; $html = $this->get_html_for_blotter($entries); $position = $config->get_string("blotter_position", "subheading"); - $page->add_block(new Block(null, $html, $position, 20)); + $page->add_block(new Block(null, rawHTML($html), $position, 20)); } /** @@ -161,7 +161,7 @@ private function get_html_for_blotter(array $entries): string $i_color = $config->get_string("blotter_color", "#FF0000"); $position = $config->get_string("blotter_position", "subheading"); $entries_list = ""; - foreach($entries as $entry) { + foreach ($entries as $entry) { /** * Blotter entries */ diff --git a/ext/browser_search/info.php b/ext/browser_search/info.php index b5a3b30bb..75898afd6 100644 --- a/ext/browser_search/info.php +++ b/ext/browser_search/info.php @@ -16,7 +16,7 @@ class BrowserSearchInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::INTEGRATION; public string $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions"; public ?string $documentation = -"Once installed, users with an opensearch compatible browser should see their search box light up with whatever \"click here to add a search engine\" notification they have + "Once installed, users with an opensearch compatible browser should see their search box light up with whatever \"click here to add a search engine\" notification they have

      Some code (and lots of help) by Artanis (Erik Youngren) from the 'tagger' extension - Used with permission"; } diff --git a/ext/browser_search/main.php b/ext/browser_search/main.php index 74a5e9711..5ca67efc3 100644 --- a/ext/browser_search/main.php +++ b/ext/browser_search/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\LINK; + class BrowserSearch extends Extension { public function onInitExt(InitExtEvent $event): void @@ -20,7 +22,12 @@ public function onPageRequest(PageRequestEvent $event): void // We need to build the data for the header $search_title = $config->get_string(SetupConfig::TITLE); $search_file_url = make_link('browser_search.xml'); - $page->add_html_header(""); + $page->add_html_header(LINK([ + 'rel' => 'search', + 'type' => 'application/opensearchdescription+xml', + 'title' => $search_title, + 'href' => $search_file_url + ])); // The search.xml file that is generated on the fly if ($event->page_matches("browser_search.xml")) { diff --git a/ext/bulk_actions/main.php b/ext/bulk_actions/main.php index 11b68fafb..d9968c20f 100644 --- a/ext/bulk_actions/main.php +++ b/ext/bulk_actions/main.php @@ -17,7 +17,7 @@ class BulkActionBlockBuildingEvent extends Event /** @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): void { if (!empty($access_key)) { assert(strlen($access_key) == 1); @@ -68,20 +68,18 @@ public function onPostListBuilding(PostListBuildingEvent $event): void { global $page, $user; - if ($user->is_logged_in()) { - $babbe = new BulkActionBlockBuildingEvent(); - $babbe->search_terms = $event->search_terms; + $babbe = new BulkActionBlockBuildingEvent(); + $babbe->search_terms = $event->search_terms; - send_event($babbe); + send_event($babbe); - if (sizeof($babbe->actions) == 0) { - return; - } + if (sizeof($babbe->actions) == 0) { + return; + } - usort($babbe->actions, [$this, "sort_blocks"]); + usort($babbe->actions, [$this, "sort_blocks"]); - $this->theme->display_selector($page, $babbe->actions, Tag::implode($event->search_terms)); - } + $this->theme->display_selector($page, $babbe->actions, Tag::implode($event->search_terms)); } public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void diff --git a/ext/bulk_actions/theme.php b/ext/bulk_actions/theme.php index 5e1cc24f1..58beb8611 100644 --- a/ext/bulk_actions/theme.php +++ b/ext/bulk_actions/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class BulkActionsTheme extends Themelet { /** @@ -48,7 +50,7 @@ public function display_selector(Page $page, array $actions, string $query): voi if (!$hasQuery) { $body .= ""; } - $block = new Block("Bulk Actions", $body, "left", 30); + $block = new Block("Bulk Actions", rawHTML($body), "left", 30); $page->add_block($block); } diff --git a/ext/bulk_add/info.php b/ext/bulk_add/info.php index e5bbed565..d8e4a6cc9 100644 --- a/ext/bulk_add/info.php +++ b/ext/bulk_add/info.php @@ -15,7 +15,7 @@ class BulkAddInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "Bulk add server-side images"; public ?string $documentation = -"Upload the images into a new directory via ftp or similar, go to + "Upload the images into a new directory via ftp or similar, go to shimmie's admin page and put that directory in the bulk add box. If there are subdirectories, they get used as tags (eg if you upload into /home/bob/uploads/holiday/2008/ and point diff --git a/ext/bulk_add/main.php b/ext/bulk_add/main.php index d10076245..91f425063 100644 --- a/ext/bulk_add/main.php +++ b/ext/bulk_add/main.php @@ -47,7 +47,7 @@ public function onCliGen(CliGenEvent $event): void $dir = $input->getArgument('directory'); $bae = send_event(new BulkAddEvent($dir)); foreach ($bae->results as $r) { - if(is_a($r, UploadError::class)) { + if (is_a($r, UploadError::class)) { $output->writeln($r->name." failed: ".$r->error); } else { $output->writeln($r->name." ok"); diff --git a/ext/bulk_add/theme.php b/ext/bulk_add/theme.php index 339bfd5b8..b2e680e03 100644 --- a/ext/bulk_add/theme.php +++ b/ext/bulk_add/theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -use function MicroHTML\{UL, LI}; +use function MicroHTML\{UL, LI, rawHTML}; class BulkAddTheme extends Themelet { @@ -16,7 +16,6 @@ class BulkAddTheme extends Themelet public function display_upload_results(Page $page, array $results): void { $page->set_title("Adding folder"); - $page->set_heading("Adding folder"); $page->add_block(new NavBlock()); $html = UL(); foreach ($results as $r) { @@ -50,6 +49,6 @@ public function display_admin_block(): void "; - $page->add_block(new Block("Bulk Add", $html)); + $page->add_block(new Block("Bulk Add", rawHTML($html))); } } diff --git a/ext/bulk_add_csv/info.php b/ext/bulk_add_csv/info.php index 416344128..da970e091 100644 --- a/ext/bulk_add_csv/info.php +++ b/ext/bulk_add_csv/info.php @@ -15,14 +15,12 @@ class BulkAddCSVInfo extends ExtensionInfo 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\"
      + "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\",\"\",\"\",\"\",\"\"
      +e.g.
      \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"
      Useful for importing tagged posts without having to do database manipulation. diff --git a/ext/bulk_add_csv/theme.php b/ext/bulk_add_csv/theme.php index a29777f94..5960fe729 100644 --- a/ext/bulk_add_csv/theme.php +++ b/ext/bulk_add_csv/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class BulkAddCSVTheme extends Themelet { /** @var Block[] */ @@ -15,7 +17,6 @@ class BulkAddCSVTheme extends Themelet public function display_upload_results(Page $page): void { $page->set_title("Adding posts from csv"); - $page->set_heading("Adding posts from csv"); $page->add_block(new NavBlock()); foreach ($this->messages as $block) { $page->add_block($block); @@ -42,11 +43,11 @@ public function display_admin_block(): void "; - $page->add_block(new Block("Bulk Add CSV", $html)); + $page->add_block(new Block("Bulk Add CSV", rawHTML($html))); } public function add_status(string $title, string $body): void { - $this->messages[] = new Block($title, $body); + $this->messages[] = new Block($title, rawHTML($body)); } } diff --git a/ext/comment/main.php b/ext/comment/main.php index ddb77b662..83adcbecf 100644 --- a/ext/comment/main.php +++ b/ext/comment/main.php @@ -236,7 +236,8 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("comment/list", paged: true)) { $threads_per_page = 10; - $where = SPEED_HAX ? "WHERE posted > now() - interval '24 hours'" : ""; + $speed_hax = (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::CACHE_TAG_LISTS)); + $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) @@ -397,10 +398,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Comments"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Comments", $this->theme->get_help_html()); } } diff --git a/ext/comment/theme.php b/ext/comment/theme.php index adf398484..7b9141472 100644 --- a/ext/comment/theme.php +++ b/ext/comment/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class CommentListTheme extends Themelet { private bool $show_anon_id = false; @@ -39,8 +41,7 @@ public function display_comment_list(array $images, int $page_number, int $total $nav = $h_prev.' | '.$h_index.' | '.$h_next; $page->set_title("Comments"); - $page->set_heading("Comments"); - $page->add_block(new Block("Navigation", $nav, "left", 0)); + $page->add_block(new Block("Navigation", rawHTML($nav), "left", 0)); $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); // parts for each image @@ -90,7 +91,7 @@ public function display_comment_list(array $images, int $page_number, int $total '; - $page->add_block(new Block($image->id.': '.$image->get_tag_list(), $html, "main", $position++, "comment-list-list")); + $page->add_block(new Block($image->id.': '.$image->get_tag_list(), rawHTML($html), "main", $position++, "comment-list-list")); } } @@ -108,7 +109,7 @@ public function display_admin_block(): void "; - $page->add_block(new Block("Mass Comment Delete", $html)); + $page->add_block(new Block("Mass Comment Delete", rawHTML($html))); } /** @@ -125,7 +126,7 @@ public function display_recent_comments(array $comments): void $html .= $this->comment_to_html($comment, true); } $html .= "Full List"; - $page->add_block(new Block("Comments", $html, "left", 50, "comment-list-recent")); + $page->add_block(new Block("Comments", rawHTML($html), "left", 70, "comment-list-recent")); } /** @@ -144,7 +145,7 @@ public function display_image_comments(Image $image, array $comments, bool $post if ($postbox) { $html .= $this->build_postbox($image->id); } - $page->add_block(new Block("Comments", $html, "main", 30, "comment-list-image")); + $page->add_block(new Block("Comments", rawHTML($html), "main", 30, "comment-list-image")); } /** @@ -164,7 +165,7 @@ public function display_recent_user_comments(array $comments, User $user): void } else { $html .= "

      More

      "; } - $page->add_block(new Block("Comments", $html, "left", 70, "comment-list-user")); + $page->add_block(new Block("Comments", rawHTML($html), "left", 70, "comment-list-user")); } /** @@ -181,7 +182,7 @@ public function display_all_user_comments(array $comments, int $page_number, int if (empty($html)) { $html = '

      No comments by this user.

      '; } - $page->add_block(new Block("Comments", $html, "main", 70, "comment-list-user")); + $page->add_block(new Block("Comments", rawHTML($html), "main", 70, "comment-list-user")); $prev = $page_number - 1; @@ -196,7 +197,7 @@ public function display_all_user_comments(array $comments, int $page_number, int $h_next = ($page_number >= $total_pages) ? "Next" : "Next"; $page->set_title(html_escape($user->name)."'s comments"); - $page->add_block(new Block("Navigation", $h_prev.' | '.$h_index.' | '.$h_next, "left", 0)); + $page->add_block(new Block("Navigation", rawHTML($h_prev.' | '.$h_index.' | '.$h_next), "left", 0)); $this->display_paginator($page, "comment/beta-search/{$user->name}", null, $page_number, $total_pages); } @@ -306,20 +307,20 @@ public function get_help_html(): string { return '

      Search for posts containing a certain number of comments, or comments by a particular individual.

      -
      comments=1
      + comments=1

      Returns posts with exactly 1 comment.

      -
      comments>0
      + comments>0

      Returns posts with 1 or more comments.

      Can use <, <=, >, >=, or =.

      -
      commented_by:username
      + commented_by:username

      Returns posts that have been commented on by "username".

      -
      commented_by_userno:123
      + commented_by_userno:123

      Returns posts that have been commented on by user 123.

      '; diff --git a/ext/common_elements/info.php b/ext/common_elements/info.php new file mode 100644 index 000000000..49eb24ee9 --- /dev/null +++ b/ext/common_elements/info.php @@ -0,0 +1,18 @@ +set_code($code); - $page->set_title($title); - $page->set_heading($title); - $has_nav = false; - foreach ($page->blocks as $block) { - if ($block->header == "Navigation") { - $has_nav = true; - break; - } - } - if (!$has_nav) { - $page->add_block(new NavBlock()); - } - $page->add_block(new Block("Error", $message)); - } +use function MicroHTML\{A,B,BR,IMG,emptyHTML,joinHTML,LINK}; +class CommonElementsTheme extends Themelet +{ /** * Generic thumbnail code; returns HTML rather than adding * a block since thumbs tend to go inside blocks... @@ -69,6 +42,12 @@ public function build_thumb_html(Image $image): HTMLElement $custom_classes .= "shm-thumb-has_child "; } } + if (Extension::is_enabled(RatingsInfo::KEY) && Extension::is_enabled(RatingsBlurInfo::KEY)) { + $rb = new RatingsBlur(); + if ($rb->blur($image['rating'])) { + $custom_classes .= "blur "; + } + } $attrs = [ "href" => $view_link, @@ -79,7 +58,7 @@ public function build_thumb_html(Image $image): HTMLElement "data-mime" => $image->get_mime(), "data-post-id" => $id, ]; - if(Extension::is_enabled(RatingsInfo::KEY)) { + if (Extension::is_enabled(RatingsInfo::KEY)) { $attrs["data-rating"] = $image['rating']; } @@ -106,15 +85,15 @@ public function display_paginator(Page $page, string $base, ?string $query, int $body = $this->build_paginator($page_number, $total_pages, $base, $query, $show_random); $page->add_block(new Block(null, $body, "main", 90, "paginator")); - $page->add_html_header(""); + $page->add_html_header(LINK(['rel' => 'first', 'href' => make_link($base.'/1', $query)])); if ($page_number < $total_pages) { - $page->add_html_header(""); - $page->add_html_header(""); + $page->add_html_header(LINK(['rel' => 'prefetch', 'href' => make_link($base.'/'.($page_number + 1), $query)])); + $page->add_html_header(LINK(['rel' => 'next', 'href' => make_link($base.'/'.($page_number + 1), $query)])); } if ($page_number > 1) { - $page->add_html_header(""); + $page->add_html_header(LINK(['rel' => 'previous', 'href' => make_link($base.'/'.($page_number - 1), $query)])); } - $page->add_html_header(""); + $page->add_html_header(LINK(['rel' => 'last', 'href' => make_link($base.'/'.$total_pages, $query)])); } private function gen_page_link(string $base_url, ?string $query, int $page, string $name): HTMLElement @@ -174,13 +153,4 @@ private function build_paginator(int $current_page, int $total_pages, string $ba ' >>' ); } - - public function display_crud(string $title, HTMLElement $table, HTMLElement $paginator): void - { - global $page; - $page->set_title($title); - $page->set_heading($title); - $page->add_block(new NavBlock()); - $page->add_block(new Block("$title Table", emptyHTML($table, $paginator))); - } } diff --git a/ext/cron_uploader/theme.php b/ext/cron_uploader/theme.php index c5f9a99d5..201fbe03e 100644 --- a/ext/cron_uploader/theme.php +++ b/ext/cron_uploader/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + use function MicroHTML\LABEL; use function MicroHTML\TABLE; use function MicroHTML\TBODY; @@ -37,7 +39,6 @@ public function display_documentation( $info_html = ""; $page->set_title("Cron Uploader"); - $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
      "; @@ -102,7 +103,7 @@ public function display_documentation(
    • You can inherit categories by creating a folder that ends with \";\". For instance category;\\tag1 would result in the tag category:tag1. This allows creating a category folder, then creating many subfolders that will use that category.
    • The cron uploader works by importing files from the queue folder whenever this url is visited: -
      $cron_url
      +
      $cron_url
      • If an import is already running, another cannot start until it is done.
      • @@ -113,9 +114,9 @@ public function display_documentation( "; - $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 = new Block("Cron Uploader", rawHTML($info_html), "main", 10); + $block_install = new Block("Setup Guide", rawHTML($install_html), "main", 30); + $block_usage = new Block("Usage Guide", rawHTML($usage_html), "main", 20); $page->add_block($block); $page->add_block($block_install); $page->add_block($block_usage); @@ -126,12 +127,12 @@ public function display_documentation( $log_html .= "{$entry["date_sent"]}{$entry["message"]}"; } $log_html .= ""; - $block = new Block("Log", $log_html, "main", 40); + $block = new Block("Log", rawHTML($log_html), "main", 40); $page->add_block($block); } } - public function get_user_options(): string + public function get_user_options(): HTMLElement { $form = SHM_SIMPLE_FORM( "user_admin/cron_uploader", @@ -162,7 +163,7 @@ public function get_user_options(): string ) ); $html = emptyHTML($form); - return (string)$html; + return $html; } /** @@ -197,6 +198,6 @@ public function display_form(array $failed_dirs): void ."
        " ."
        "; $html .= "\n"; - $page->add_block(new Block("Cron Upload", $html)); + $page->add_block(new Block("Cron Upload", rawHTML($html))); } } diff --git a/ext/custom_html_headers/info.php b/ext/custom_html_headers/info.php index cddad4f43..1af46e732 100644 --- a/ext/custom_html_headers/info.php +++ b/ext/custom_html_headers/info.php @@ -16,7 +16,7 @@ class CustomHtmlHeadersInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::ADMIN; public string $description = "Allows admins to modify & set custom content"; public ?string $documentation = -"When you go to board config you can find a block named Custom HTML Headers. + "When you go to board config you can find a block named Custom HTML Headers.
        In that block you can simply place any thing you can place within <head></head>

        This can be useful if you want to add website tracking code or other javascript. diff --git a/ext/custom_html_headers/main.php b/ext/custom_html_headers/main.php index 87f90033e..975095238 100644 --- a/ext/custom_html_headers/main.php +++ b/ext/custom_html_headers/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class CustomHtmlHeaders extends Extension { # Adds setup block for custom content @@ -44,7 +46,7 @@ private function handle_custom_html_headers(): void $header = $config->get_string('custom_html_headers', ''); if ($header != '') { - $page->add_html_header($header); + $page->add_html_header(rawHTML($header)); } } diff --git a/ext/danbooru_api/info.php b/ext/danbooru_api/info.php index 2025923c0..5a2192a73 100644 --- a/ext/danbooru_api/info.php +++ b/ext/danbooru_api/info.php @@ -14,7 +14,7 @@ class DanbooruApiInfo extends ExtensionInfo public string $description = "Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie"; public ExtensionCategory $category = ExtensionCategory::INTEGRATION; public ?string $documentation = -"Notes: + "Notes:
        danbooru API based on documentation from danbooru 1.0 - http://attachr.com/7569
        I've only been able to test add_post and find_tags because I use the diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php index 7c380cccd..5a275d99e 100644 --- a/ext/danbooru_api/main.php +++ b/ext/danbooru_api/main.php @@ -80,14 +80,12 @@ private function authenticate_user(): void if (isset($_REQUEST['login']) && isset($_REQUEST['password'])) { // Get this user from the db, if it fails the user becomes anonymous // Code borrowed from /ext/user - $name = $_REQUEST['login']; - $pass = $_REQUEST['password']; - $duser = User::by_name_and_pass($name, $pass); - if (!is_null($duser)) { - $user = $duser; - } else { + try { + $name = $_REQUEST['login']; + $pass = $_REQUEST['password']; + $user = User::by_name_and_pass($name, $pass); + } catch (UserNotFound $e) { $user = User::by_id($config->get_int("anon_id", 0)); - assert(!is_null($user)); } send_event(new UserLoginEvent($user)); } @@ -139,8 +137,7 @@ private function api_find_tags(array $GET): HTMLElement $tags = Tag::explode($GET['tags']); assert(!is_null($start) && !is_null($tags)); } - */ - else { + */ else { $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", @@ -319,7 +316,7 @@ private function api_add_post(): void assert($file !== false); try { fetch_url($source, $file); - } catch(FetchException $e) { + } catch (FetchException $e) { $page->set_code(409); $page->add_http_header("X-Danbooru-Errors: $e"); return; diff --git a/ext/downtime/info.php b/ext/downtime/info.php index 8aae676a7..37f1493f3 100644 --- a/ext/downtime/info.php +++ b/ext/downtime/info.php @@ -16,7 +16,7 @@ class DowntimeInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::ADMIN; public string $description = "Show a \"down for maintenance\" page"; public ?string $documentation = -"Once installed there will be some more options on the config page -- + "Once installed there will be some more options on the config page -- Ticking \"disable non-admin access\" will mean that regular and anonymous users will be blocked from accessing the site, only able to view the message specified in the box."; diff --git a/ext/downtime/theme.php b/ext/downtime/theme.php index b0298b508..d5e21698a 100644 --- a/ext/downtime/theme.php +++ b/ext/downtime/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\{emptyHTML, rawHTML, TITLE, LINK}; + class DowntimeTheme extends Themelet { /** @@ -13,7 +15,7 @@ public function display_notification(Page $page): void { $page->add_block(new Block( "Downtime", - "DOWNTIME MODE IS ON!", + rawHTML("DOWNTIME MODE IS ON!"), "left", 0 )); @@ -30,45 +32,40 @@ public function display_message(string $message): void $login_link = make_link("user_admin/login"); $form = make_form($login_link); + $head = emptyHTML( + TITLE("Downtime"), + LINK(["rel" => "stylesheet", "href" => "$data_href/themes/$theme_name/style.css", "type" => "text/css"]) + ); + $body = rawHTML(<< +
        +

        Down for Maintenance

        +
        + $message +
        +
        +
        +

        Admin Login

        +
        + $form + + + + + + + + + + +
        + +
        +
        + +EOD); $page->set_mode(PageMode::DATA); $page->set_code(503); - $page->set_data( - << - - Downtime - - - -
        -
        -

        Down for Maintenance

        -
        - $message -
        -
        -
        -

        Admin Login

        -
        - $form - - - - - - - - - - -
        - -
        -
        -
        - - -EOD - ); + $page->set_data((string)$page->html_html($head, $body)); } } diff --git a/ext/emoticons/info.php b/ext/emoticons/info.php index 2dd054f8f..5814c1176 100644 --- a/ext/emoticons/info.php +++ b/ext/emoticons/info.php @@ -16,7 +16,7 @@ class EmoticonsInfo extends ExtensionInfo public array $dependencies = [EmoticonListInfo::KEY]; public string $description = "Lets users use graphical smilies"; public ?string $documentation = -"This extension will turn colon-something-colon into a link + "This extension will turn colon-something-colon into a link to an image with that something as the name, eg :smile: becomes a link to smile.gif

        Images are stored in /ext/emoticons/default/, and you can diff --git a/ext/emoticons/main.php b/ext/emoticons/main.php index 2f8d47028..5c8a8acab 100644 --- a/ext/emoticons/main.php +++ b/ext/emoticons/main.php @@ -12,7 +12,7 @@ class Emoticons extends FormatterExtension public function format(string $text): string { $data_href = get_base_href(); - $text = preg_replace("/:([a-z]*?):/s", "\1", $text); + $text = preg_replace_ex("/:([a-z]*?):/s", "\1", $text); return $text; } diff --git a/ext/emoticons_list/theme.php b/ext/emoticons_list/theme.php index 5d770d704..bbc7fdd50 100644 --- a/ext/emoticons_list/theme.php +++ b/ext/emoticons_list/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\{TITLE, rawHTML}; + class EmoticonListTheme extends Themelet { /** @@ -13,19 +15,21 @@ public function display_emotes(array $list): void { global $page; $data_href = get_base_href(); - $html = "Emoticon list"; - $html .= ""; + $body = "
        "; $n = 1; foreach ($list as $item) { $name = pathinfo($item, PATHINFO_FILENAME); - $html .= ""; + $body .= ""; if ($n++ % 3 == 0) { - $html .= ""; + $body .= ""; } } - $html .= "
        $name :$name:$name :$name:
        "; - $html .= ""; + $body .= ""; + $page->set_mode(PageMode::DATA); - $page->set_data($html); + $page->set_data((string)$page->html_html( + TITLE("Emoticon list"), + rawHTML($body) + )); } } diff --git a/ext/et/info.php b/ext/et/info.php index e641eee8d..9347dea06 100644 --- a/ext/et/info.php +++ b/ext/et/info.php @@ -16,7 +16,7 @@ class ETInfo extends ExtensionInfo public bool $core = true; public string $description = "Show various bits of system information"; public ?string $documentation = -"Knowing the information that this extension shows can be very useful for debugging. There's also an option to send + "Knowing the information that this extension shows can be very useful for debugging. There's also an option to send your stats to my database, so I can get some idea of how shimmie is used, which servers I need to support, which versions of PHP I should test with, etc."; } diff --git a/ext/et/main.php b/ext/et/main.php index 7dac4f15e..94e75266a 100644 --- a/ext/et/main.php +++ b/ext/et/main.php @@ -67,13 +67,13 @@ private function get_info(): array } $ver = VERSION; - if(defined("BUILD_TIME")) { + if (defined("BUILD_TIME")) { $ver .= "-" . substr(str_replace("-", "", constant("BUILD_TIME")), 0, 8); } - if(defined("BUILD_HASH")) { + if (defined("BUILD_HASH")) { $ver .= "-" . substr(constant("BUILD_HASH"), 0, 7); } - if(file_exists(".git")) { + if (file_exists(".git")) { $ver .= "+"; } @@ -123,7 +123,7 @@ private function get_info(): array $commitHash = trim(\Safe\exec('git log --pretty="%h" -n1 HEAD')); $commitBranch = trim(\Safe\exec('git rev-parse --abbrev-ref HEAD')); $commitOrigin = trim(\Safe\exec('git config --get remote.origin.url')); - $commitOrigin = preg_replace("#//.*@#", "//xxx@", $commitOrigin); + $commitOrigin = preg_replace_ex("#//.*@#", "//xxx@", $commitOrigin); $info['versions']['shimmie'] .= $commitHash; $info['versions']['origin'] = "$commitOrigin ($commitBranch)"; $info['git'] = [ diff --git a/ext/et/theme.php b/ext/et/theme.php index f877ecb53..f39c96dbd 100644 --- a/ext/et/theme.php +++ b/ext/et/theme.php @@ -19,7 +19,6 @@ public function display_info_page(string $yaml): void global $page; $page->set_title("System Info"); - $page->set_heading("System Info"); $page->add_block(new NavBlock()); $page->add_block(new Block("Information:", $this->build_data_form($yaml))); } diff --git a/ext/et_server/main.php b/ext/et_server/main.php index 5543fdf40..b212e7660 100644 --- a/ext/et_server/main.php +++ b/ext/et_server/main.php @@ -4,7 +4,7 @@ namespace Shimmie2; -use function MicroHTML\{PRE}; +use function MicroHTML\{CODE,rawHTML}; class ETServer extends Extension { @@ -19,16 +19,14 @@ public function onPageRequest(PageRequestEvent $event): void ["data" => $data] ); $page->set_title("Thanks!"); - $page->set_heading("Thanks!"); - $page->add_block(new Block("Thanks!", "Your data has been recorded~")); + $page->add_block(new Block("Thanks!", rawHTML("Your data has been recorded~"))); } elseif ($user->can(Permissions::VIEW_REGISTRATIONS)) { $page->set_title("Registrations"); - $page->set_heading("Registrations"); $n = 0; 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"]), + CODE(["style" => "text-align: left; overflow: scroll;"], $row["data"]), "main", $n++ )); diff --git a/ext/ext_manager/main.php b/ext/ext_manager/main.php index 92fef2bc2..560ffcd37 100644 --- a/ext/ext_manager/main.php +++ b/ext/ext_manager/main.php @@ -35,11 +35,7 @@ public function onPageRequest(PageRequestEvent $event): void $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("ext_manager")); } else { - $this->theme->display_error( - 500, - "File Operation Failed", - "The config file (data/config/extensions.conf.php) isn't writable by the web server :(" - ); + throw new ServerError("The config file (data/config/extensions.conf.php) isn't writable by the web server :("); } } elseif ($event->page_matches("ext_manager", method: "GET")) { $is_admin = $user->can(Permissions::MANAGE_EXTENSION_LIST); @@ -49,7 +45,7 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("ext_doc/{ext}")) { $ext = $event->get_arg('ext'); $info = ExtensionInfo::get_by_key($ext); - if($info) { + if ($info) { $this->theme->display_doc($page, $info); } } elseif ($event->page_matches("ext_doc")) { @@ -97,7 +93,7 @@ private function get_extensions(bool $all): array $extensions = array_filter($extensions, fn ($x) => Extension::is_enabled($x->key)); } usort($extensions, function ($a, $b) { - if($a->category->name !== $b->category->name) { + if ($a->category->name !== $b->category->name) { return $a->category->name <=> $b->category->name; } if ($a->beta !== $b->beta) { diff --git a/ext/ext_manager/theme.php b/ext/ext_manager/theme.php index 5a035004a..fff770f7c 100644 --- a/ext/ext_manager/theme.php +++ b/ext/ext_manager/theme.php @@ -56,7 +56,7 @@ public function display_table(Page $page, array $extensions, bool $editable): vo continue; } - if($extension->category !== $last_cat) { + if ($extension->category !== $last_cat) { $last_cat = $extension->category; $categories[] = $last_cat; $tbody->appendChild( @@ -101,7 +101,7 @@ public function display_table(Page $page, array $extensions, bool $editable): vo )); } - if($editable) { + if ($editable) { foreach ($extensions as $extension) { if ($extension->visibility === ExtensionVisibility::HIDDEN && !$extension->core) { $form->appendChild(INPUT([ @@ -122,7 +122,6 @@ public function display_table(Page $page, array $extensions, bool $editable): vo } $page->set_title("Extensions"); - $page->set_heading("Extensions"); $page->add_block(new Block("Navigation", \MicroHTML\joinHTML(BR(), $cat_html), "left", 0)); $page->add_block(new Block("Extension Manager", $form)); } diff --git a/ext/favorites/info.php b/ext/favorites/info.php index e13358df7..d043d44df 100644 --- a/ext/favorites/info.php +++ b/ext/favorites/info.php @@ -14,7 +14,7 @@ class FavoritesInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "Allow users to favorite images"; public ?string $documentation = -"Gives users a \"favorite this image\" button that they can press + "Gives users a \"favorite this image\" button that they can press

        Favorites for a user can then be retrieved by searching for \"favorited_by=UserName\"

        Popular images can be searched for by eg. \"favorites>5\"

        Favorite info can be added to a post's filename or tooltip using the \$favorites placeholder"; diff --git a/ext/favorites/main.php b/ext/favorites/main.php index 3a493279c..d6ad1f4c4 100644 --- a/ext/favorites/main.php +++ b/ext/favorites/main.php @@ -44,7 +44,7 @@ public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): ["user_id" => $user_id, "image_id" => $image_id] ) > 0; - if($is_favorited) { + if ($is_favorited) { $event->add_button("Un-Favorite", "favourite/remove/{$event->image->id}"); } else { $event->add_button("Favorite", "favourite/add/{$event->image->id}"); @@ -157,7 +157,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $event->add_block(new Block("Favorites", $this->theme->get_help_html())); + $event->add_section("Favorites", $this->theme->get_help_html()); } } diff --git a/ext/favorites/theme.php b/ext/favorites/theme.php index 95a1bb9e2..b3e2cf502 100644 --- a/ext/favorites/theme.php +++ b/ext/favorites/theme.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\INPUT; +use function MicroHTML\{INPUT,rawHTML}; class FavoritesTheme extends Themelet { @@ -27,27 +27,27 @@ public function display_people(array $username_array): void $html .= "
        $username"; } - $page->add_block(new Block("Favorited By", $html, "left", 25)); + $page->add_block(new Block("Favorited By", rawHTML($html), "left", 25)); } public function get_help_html(): string { return '

        Search for posts that have been favorited a certain number of times, or favorited by a particular individual.

        -
        favorites=1
        + favorites=1

        Returns posts that have been favorited once.

        -
        favorites>0
        + favorites>0

        Returns posts that have been favorited 1 or more times

        Can use <, <=, >, >=, or =.

        -
        favorited_by:username
        + favorited_by:username

        Returns posts that have been favorited by "username".

        -
        favorited_by_userno:123
        + favorited_by_userno:123

        Returns posts that have been favorited by user 123.

        '; diff --git a/ext/featured/info.php b/ext/featured/info.php index 15b46d8b7..c38139c8b 100644 --- a/ext/featured/info.php +++ b/ext/featured/info.php @@ -15,7 +15,7 @@ class FeaturedInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "Bring a specific image to the users' attentions"; public ?string $documentation = -"Once enabled, a new \"feature this\" button will appear next + "Once enabled, a new \"feature this\" button will appear next to the other post control buttons (delete, rotate, etc). Clicking it will set the image as the site's current feature, which will be shown in the side bar of the post list. diff --git a/ext/featured/main.php b/ext/featured/main.php index a4afa1171..ba9addcc0 100644 --- a/ext/featured/main.php +++ b/ext/featured/main.php @@ -72,8 +72,7 @@ public function onImageDeletion(ImageDeletionEvent $event): void { global $config; if ($event->image->id == $config->get_int("featured_id")) { - $config->set_int("featured_id", 0); - $config->save(); + $config->delete("featured_id"); } } diff --git a/ext/filter/main.php b/ext/filter/main.php index 1735b13c5..6a86b8c1e 100644 --- a/ext/filter/main.php +++ b/ext/filter/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\SCRIPT; + class Filter extends Extension { /** @var FilterTheme */ @@ -19,10 +21,6 @@ public function onPageRequest(PageRequestEvent $event): void { global $page; $this->theme->addFilterBox(); - $page->add_html_header(""); } public function onSetupBuilding(SetupBuildingEvent $event): void diff --git a/ext/filter/style.css b/ext/filter/style.css index ead4ca372..dc8887c8e 100644 --- a/ext/filter/style.css +++ b/ext/filter/style.css @@ -1,4 +1,3 @@ #filter-list{padding:revert; text-align:left;} -.thumb.filtered{display:unset} .thumb.filtered.filtered-active{display:none;} .filter-inactive,.filter-inactive:hover{text-decoration: line-through;} \ No newline at end of file diff --git a/ext/filter/theme.php b/ext/filter/theme.php index 53206d14c..b362a6e13 100644 --- a/ext/filter/theme.php +++ b/ext/filter/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\{META,rawHTML}; + class FilterTheme extends Themelet { public function addFilterBox(): void @@ -21,7 +23,7 @@ public function addFilterBox(): void "; - $page->add_html_header(""); - $page->add_block(new Block("Filters", $html, "left", 10)); + $page->add_html_header(META(['id' => 'filter-tags', 'tags' => $tags])); + $page->add_block(new Block("Filters", rawHTML($html), "left", 10)); } } diff --git a/ext/forum/main.php b/ext/forum/main.php index f2f4c31f2..fb07560c0 100644 --- a/ext/forum/main.php +++ b/ext/forum/main.php @@ -17,6 +17,15 @@ class Forum extends Extension /** @var ForumTheme */ protected Themelet $theme; + public function onInitExt(InitExtEvent $event): void + { + global $config; + $config->set_default_int("forumTitleSubString", 25); + $config->set_default_int("forumThreadsPerPage", 15); + $config->set_default_int("forumPostsPerPage", 15); + $config->set_default_int("forumMaxCharsPerPost", 512); + } + public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void { global $config, $database; @@ -46,12 +55,6 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void "); $database->execute("CREATE INDEX forum_posts_date_idx ON forum_posts(date)", []); - $config->set_int("forumTitleSubString", 25); - $config->set_int("forumThreadsPerPage", 15); - $config->set_int("forumPostsPerPage", 15); - - $config->set_int("forumMaxCharsPerPost", 512); - $this->set_version("forum_version", 3); } if ($this->get_version("forum_version") < 2) { diff --git a/ext/forum/theme.php b/ext/forum/theme.php index c530cfec4..3fc363075 100644 --- a/ext/forum/theme.php +++ b/ext/forum/theme.php @@ -20,13 +20,12 @@ class ForumTheme extends Themelet public function display_thread_list(Page $page, array $threads, bool $showAdminOptions, int $pageNumber, int $totalPages): void { if (count($threads) == 0) { - $html = "There are no threads to show."; + $html = rawHTML("There are no threads to show."); } else { $html = $this->make_thread_list($threads, $showAdminOptions); } - $page->set_title(html_escape("Forum")); - $page->set_heading(html_escape("Forum")); + $page->set_title("Forum"); $page->add_block(new Block("Forum", $html, "main", 10)); $this->display_paginator($page, "forum/index", null, $pageNumber, $totalPages); @@ -34,7 +33,7 @@ public function display_thread_list(Page $page, array $threads, bool $showAdminO - public function display_new_thread_composer(Page $page, string $threadText = null, string $threadTitle = null): void + public function display_new_thread_composer(Page $page, ?string $threadText = null, ?string $threadTitle = null): void { global $config, $user; $max_characters = $config->get_int('forumMaxCharsPerPost'); @@ -76,7 +75,6 @@ public function display_new_thread_composer(Page $page, string $threadText = nul $blockTitle = "Write a new thread"; $page->set_title(html_escape($blockTitle)); - $page->set_heading(html_escape($blockTitle)); $page->add_block(new Block($blockTitle, $html, "main", 120)); } @@ -192,7 +190,6 @@ public function display_thread(array $posts, bool $showAdminOptions, string $thr $this->display_paginator($page, "forum/view/".$threadID, null, $pageNumber, $totalPages); $page->set_title(html_escape($threadTitle)); - $page->set_heading(html_escape($threadTitle)); $page->add_block(new Block($threadTitle, $html, "main", 20)); } diff --git a/ext/four_oh_four/main.php b/ext/four_oh_four/main.php index 621e23fdd..300d10f78 100644 --- a/ext/four_oh_four/main.php +++ b/ext/four_oh_four/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class FourOhFour extends Extension { public function onPageRequest(PageRequestEvent $event): void @@ -13,10 +15,9 @@ public function onPageRequest(PageRequestEvent $event): void if ($page->mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) { log_debug("four_oh_four", "Hit 404: {$event->path}"); $page->set_code(404); - $page->set_title("404"); - $page->set_heading("404 - No Handler Found"); + $page->set_title("404 - No Handler Found"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Explanation", "No handler could be found for the page '{$event->path}'")); + $page->add_block(new Block("Explanation", rawHTML("No handler could be found for the page '{$event->path}'"))); } } diff --git a/ext/google_analytics/info.php b/ext/google_analytics/info.php index b9d2eaa0c..dc88aa54f 100644 --- a/ext/google_analytics/info.php +++ b/ext/google_analytics/info.php @@ -15,6 +15,6 @@ class GoogleAnalyticsInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "Integrates Google Analytics tracking"; public ?string $documentation = -"User has to enter their Google Analytics ID in the Board Config to use this extension."; + "User has to enter their Google Analytics ID in the Board Config to use this extension."; public ExtensionCategory $category = ExtensionCategory::OBSERVABILITY; } diff --git a/ext/google_analytics/main.php b/ext/google_analytics/main.php index ee7cd2783..c39c9875f 100644 --- a/ext/google_analytics/main.php +++ b/ext/google_analytics/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\SCRIPT; + class GoogleAnalytics extends Extension { # Add analytics to config @@ -21,15 +23,16 @@ public function onPageRequest(PageRequestEvent $event): void $google_analytics_id = $config->get_string('google_analytics_id', ''); if (stristr($google_analytics_id, "UA-")) { - $page->add_html_header(""); + $page->add_html_header(SCRIPT(["type" => 'text/javascript'], " + var _gaq = _gaq || []; + _gaq.push(['_setAccount', '$google_analytics_id']); + _gaq.push(['_trackPageview']); + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' === document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + ")); } } } diff --git a/ext/graphql/main.php b/ext/graphql/main.php index cdc75a688..b14127946 100644 --- a/ext/graphql/main.php +++ b/ext/graphql/main.php @@ -7,7 +7,6 @@ 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; @@ -174,7 +173,7 @@ private static function handle_uploads(): array } try { $results[] = ["image_ids" => self::handle_upload($n, $metadata)]; - } catch(\Exception $e) { + } catch (\Exception $e) { $results[] = ["error" => $e->getMessage()]; } } @@ -192,7 +191,7 @@ private static function handle_upload(int $n, array $metadata): array throw new UploadException("URLs not handled yet"); } else { $ec = $_FILES["data$n"]["error"]; - switch($ec) { + switch ($ec) { case UPLOAD_ERR_OK: $tmpname = $_FILES["data$n"]["tmp_name"]; $filename = $_FILES["data$n"]["name"]; diff --git a/ext/graphql/test.php b/ext/graphql/test.php index f8a79ca07..b5b4c2e26 100644 --- a/ext/graphql/test.php +++ b/ext/graphql/test.php @@ -30,7 +30,7 @@ public function testQuery(): void { $this->log_in_as_user(); $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); - $image = Image::by_id($image_id); + $image = Image::by_id_ex($image_id); $result = $this->graphql('{ posts(limit: 3, offset: 0) { diff --git a/ext/handle_archive/info.php b/ext/handle_archive/info.php index 5fa960ff6..19490c786 100644 --- a/ext/handle_archive/info.php +++ b/ext/handle_archive/info.php @@ -15,7 +15,7 @@ class ArchiveFileHandlerInfo extends ExtensionInfo public string $description = "Allow users to upload archives (zip, etc)"; public ExtensionCategory $category = ExtensionCategory::FILE_HANDLING; public ?string $documentation = -"Note: requires exec() access and an external unzip command + "Note: requires exec() access and an external unzip command

        Any command line unzipper should work, some examples:

        unzip: unzip -d \"%d\" \"%f\"
        7-zip: 7zr x -o\"%d\" \"%f\""; diff --git a/ext/handle_archive/main.php b/ext/handle_archive/main.php index 85bd910ef..fab6f52af 100644 --- a/ext/handle_archive/main.php +++ b/ext/handle_archive/main.php @@ -38,10 +38,10 @@ public function onDataUpload(DataUploadEvent $event): void try { $results = add_dir($tmpdir, Tag::explode($event->metadata['tags'])); foreach ($results as $r) { - if(is_a($r, UploadError::class)) { + if (is_a($r, UploadError::class)) { $page->flash($r->name." failed: ".$r->error); } - if(is_a($r, UploadSuccess::class)) { + if (is_a($r, UploadSuccess::class)) { $event->images[] = Image::by_id_ex($r->image_id); } } diff --git a/ext/handle_archive/test.php b/ext/handle_archive/test.php index a4582ffab..39afb5f58 100644 --- a/ext/handle_archive/test.php +++ b/ext/handle_archive/test.php @@ -20,7 +20,7 @@ public function testArchiveHander(): void public function tearDown(): void { - if(file_exists("tests/test.zip")) { + if (file_exists("tests/test.zip")) { unlink("tests/test.zip"); } diff --git a/ext/handle_cbz/theme.php b/ext/handle_cbz/theme.php index 5ff89f3b0..78068f6e6 100644 --- a/ext/handle_cbz/theme.php +++ b/ext/handle_cbz/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class CBZFileHandlerTheme extends Themelet { public function display_image(Image $image): void @@ -27,6 +29,6 @@ public function display_image(Image $image): void "; - $page->add_block(new Block("Comic", $html, "main", 10, "comicBlock")); + $page->add_block(new Block("Comic", rawHTML($html), "main", 10, "comicBlock")); } } diff --git a/ext/handle_ico/theme.php b/ext/handle_ico/theme.php index 39405e2c0..877e767b6 100644 --- a/ext/handle_ico/theme.php +++ b/ext/handle_ico/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class IcoFileHandlerTheme extends Themelet { public function display_image(Image $image): void @@ -14,6 +16,6 @@ public function display_image(Image $image): void main image "; - $page->add_block(new Block("Image", $html, "main", 10)); + $page->add_block(new Block("Image", rawHTML($html), "main", 10)); } } diff --git a/ext/handle_mp3/theme.php b/ext/handle_mp3/theme.php index ea94094cc..9708423cd 100644 --- a/ext/handle_mp3/theme.php +++ b/ext/handle_mp3/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\{SCRIPT, rawHTML}; + class MP3FileHandlerTheme extends Themelet { public function display_image(Image $image): void @@ -20,7 +22,10 @@ public function display_image(Image $image): void

        Download"; - $page->add_html_header(""); - $page->add_block(new Block("Music", $html, "main", 10)); + $page->add_html_header(SCRIPT([ + 'src' => "$data_href/ext/handle_mp3/lib/jsmediatags.min.js", + 'type' => 'text/javascript' + ])); + $page->add_block(new Block("Music", rawHTML($html), "main", 10)); } } diff --git a/ext/handle_pixel/script.js b/ext/handle_pixel/script.js index dd738cf58..980e0bd0a 100644 --- a/ext/handle_pixel/script.js +++ b/ext/handle_pixel/script.js @@ -21,6 +21,17 @@ document.addEventListener('DOMContentLoaded', () => { img.css('max-height', (window.innerHeight * 0.95) + 'px'); } + const zoomed_height_diff = Math.round(window.innerHeight * 0.95 - img.height()); + const zoomed_width_diff = Math.round(img.parent().width() * 0.95 - img.width()); + + if (zoomed_height_diff > 0 && zoomed_width_diff > 0) { + img.css('cursor', ''); + } else if (zoom_type == "full") { + img.css('cursor', 'zoom-out'); + } else { + img.css('cursor', 'zoom-in'); + } + $(".shm-zoomer").val(zoom_type); if (save_cookie) { diff --git a/ext/handle_pixel/theme.php b/ext/handle_pixel/theme.php index da3e7f4b5..89ab633f2 100644 --- a/ext/handle_pixel/theme.php +++ b/ext/handle_pixel/theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -use function MicroHTML\IMG; +use function MicroHTML\{IMG,rawHTML}; class PixelFileHandlerTheme extends Themelet { @@ -46,7 +46,7 @@ public function display_metadata(Image $image): void } } if ($head) { - $page->add_block(new Block("EXIF Info", $head, "left")); + $page->add_block(new Block("EXIF Info", rawHTML($head), "left")); } } } diff --git a/ext/handle_svg/theme.php b/ext/handle_svg/theme.php index a37d0051e..707ea03ac 100644 --- a/ext/handle_svg/theme.php +++ b/ext/handle_svg/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class SVGFileHandlerTheme extends Themelet { public function display_image(Image $image): void @@ -21,6 +23,6 @@ class='shm-main-image' data-height='{$image->height}' /> "; - $page->add_block(new Block("Image", $html, "main", 10)); + $page->add_block(new Block("Image", rawHTML($html), "main", 10)); } } diff --git a/ext/handle_video/info.php b/ext/handle_video/info.php index ee618fd44..d9b0a5dc1 100644 --- a/ext/handle_video/info.php +++ b/ext/handle_video/info.php @@ -15,7 +15,7 @@ class VideoFileHandlerInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::FILE_HANDLING; public string $description = "Handle FLV, MP4, OGV and WEBM video files."; public ?string $documentation = -"Based heavily on \"Handle MP3\" by Shish.

        + "Based heavily on \"Handle MP3\" by Shish.

        FLV: Flash player
        MP4: HTML5 with Flash fallback
        OGV, WEBM: HTML5
        diff --git a/ext/help_pages/main.php b/ext/help_pages/main.php index d06f84544..a329e0f4e 100644 --- a/ext/help_pages/main.php +++ b/ext/help_pages/main.php @@ -4,6 +4,10 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + +use function MicroHTML\rawHTML; + class HelpPageListBuildingEvent extends Event { /** @var array */ @@ -32,6 +36,14 @@ public function add_block(Block $block, int $position = 50): void { $this->add_part($block, $position); } + + public function add_section(string $title, string|HTMLElement $html): void + { + if (is_string($html)) { + $html = rawHTML($html); + } + $this->add_block(new Block($title, $html)); + } } class HelpPages extends Extension @@ -96,12 +108,14 @@ public function onUserBlockBuilding(UserBlockBuildingEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { 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); - - $block = new Block(ExtensionInfo::LICENSE_GPLV2); - $block->body = "

                            GNU GENERAL PUBLIC LICENSE
        +            $event->add_section(
        +                "Software Licenses",
        +                "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_section(
        +                ExtensionInfo::LICENSE_GPLV2,
        +                "
                            GNU GENERAL PUBLIC LICENSE
                                Version 2, June 1991
         
          Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
        @@ -439,11 +453,12 @@ public function onHelpPageBuilding(HelpPageBuildingEvent $event): void
         proprietary programs.  If your program is a subroutine library, you may
         consider it more useful to permit linking proprietary applications with the
         library.  If this is what you want to do, use the GNU Lesser General
        -Public License instead of this License.
        "; - $event->add_block($block); +Public License instead of this License.
        " + ); - $block = new Block(ExtensionInfo::LICENSE_MIT); - $block->body = "
        Permission is hereby granted, free of charge, to any person obtaining a copy
        +            $event->add_section(
        +                ExtensionInfo::LICENSE_MIT,
        +                "
        Permission is hereby granted, free of charge, to any person obtaining a copy
         of this software and associated documentation files (the \"Software\"), to deal
         in the Software without restriction, including without limitation the rights
         to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        @@ -459,12 +474,12 @@ public function onHelpPageBuilding(HelpPageBuildingEvent $event): void
         AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        -SOFTWARE.
        "; - $event->add_block($block); - +SOFTWARE.
        " + ); - $block = new Block(ExtensionInfo::LICENSE_WTFPL); - $block->body = "
                    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
        +            $event->add_section(
        +                ExtensionInfo::LICENSE_WTFPL,
        +                "
                    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
                             Version 2, December 2004
         
          Copyright (C) 2004 Sam Hocevar 
        @@ -478,8 +493,8 @@ public function onHelpPageBuilding(HelpPageBuildingEvent $event): void
         
           0. You just DO WHAT THE FUCK YOU WANT TO.
         
        -
        "; - $event->add_block($block); +
        " + ); } } } diff --git a/ext/help_pages/style.css b/ext/help_pages/style.css index d1530ee50..f44444164 100644 --- a/ext/help_pages/style.css +++ b/ext/help_pages/style.css @@ -3,9 +3,10 @@ padding-left: 16pt; } -.command_example pre { +.command_example code { padding:4pt; border: dashed 2px black; + background: inherit; } .command_example p { @@ -13,7 +14,7 @@ } @media (min-width: 750px) { - .command_example pre { + .command_example code { display: table-cell; width: 256px; } diff --git a/ext/help_pages/theme.php b/ext/help_pages/theme.php index 0087a4bb0..147e6949e 100644 --- a/ext/help_pages/theme.php +++ b/ext/help_pages/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\{A, BR, emptyHTML, rawHTML}; + class HelpPagesTheme extends Themelet { /** @@ -14,16 +16,17 @@ public function display_list_page(array $pages): void global $page; $page->set_title("Help Pages"); - $page->set_heading("Help Pages"); - $nav_block = new Block("Help", "", "left", 0); + $items = emptyHTML(); foreach ($pages as $link => $desc) { - $link = make_link("help/{$link}"); - $nav_block->body .= "".html_escape($desc)."
        "; + $items->appendChild( + A(["href" => make_link("help/{$link}")], $desc), + BR(), + ); } - $page->add_block($nav_block); - $page->add_block(new Block("Help Pages", "See list of pages to left")); + $page->add_block(new Block("Help", $items, "left", 0)); + $page->add_block(new Block("Help Pages", rawHTML("See list of pages to left"))); } public function display_help_page(string $title): void @@ -31,7 +34,6 @@ public function display_help_page(string $title): void global $page; $page->set_title("Help - $title"); - $page->set_heading("Help - $title"); $page->add_block(new NavBlock()); } } diff --git a/ext/holiday/main.php b/ext/holiday/main.php index b1c8fe453..594f03045 100644 --- a/ext/holiday/main.php +++ b/ext/holiday/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\LINK; + class Holiday extends Extension { public function onInitExt(InitExtEvent $event): void @@ -22,9 +24,11 @@ public function onPageRequest(PageRequestEvent $event): void { global $config, $page; if (date('d/m') == '01/04' && $config->get_bool("holiday_aprilfools")) { - $page->add_html_header( - "" - ); + $page->add_html_header(LINK([ + 'rel' => 'stylesheet', + 'href' => get_base_href() . '/ext/holiday/stylesheets/aprilfools.css', + 'type' => 'text/css' + ])); } } } diff --git a/ext/home/info.php b/ext/home/info.php index c7fb1518d..67ab6725c 100644 --- a/ext/home/info.php +++ b/ext/home/info.php @@ -15,7 +15,7 @@ class HomeInfo extends ExtensionInfo public ExtensionVisibility $visibility = ExtensionVisibility::ADMIN; public string $description = "Displays a front page with logo, search box and post count"; public ?string $documentation = -"Once enabled, the page will show up at the URL \"home\", so if you want + "Once enabled, the page will show up at the URL \"home\", so if you want this to be the front page of your site, you should go to \"Board Config\" and set \"Front Page\" to \"home\".

        The images used for the numbers can be changed from the board config diff --git a/ext/home/main.php b/ext/home/main.php index 12121099d..0208eee50 100644 --- a/ext/home/main.php +++ b/ext/home/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + class Home extends Extension { /** @var HomeTheme */ @@ -40,7 +42,7 @@ public function onSetupBuilding(SetupBuildingEvent $event): void } - private function get_body(): string + private function get_body(): HTMLElement { // returns just the contents of the body global $config; diff --git a/ext/home/theme.php b/ext/home/theme.php index 574e7eb47..3772c6bfd 100644 --- a/ext/home/theme.php +++ b/ext/home/theme.php @@ -4,32 +4,29 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + +use function MicroHTML\{emptyHTML, TITLE, META, rawHTML}; + class HomeTheme extends Themelet { - public function display_page(Page $page, string $sitename, string $base_href, string $theme_name, string $body): void + public function display_page(Page $page, string $sitename, string $base_href, string $theme_name, HTMLElement $body): void { $page->set_mode(PageMode::DATA); $page->add_auto_html_headers(); - $hh = $page->get_all_html_headers(); - $page->set_data( - << - - - $sitename - - - $hh - - - $body - - -EOD - ); + + $page->set_data((string)$page->html_html( + emptyHTML( + TITLE($sitename), + META(["http-equiv" => "Content-Type", "content" => "text/html;charset=utf-8"]), + META(["name" => "viewport", "content" => "width=device-width, initial-scale=1"]), + $page->get_all_html_headers(), + ), + $body + )); } - 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, string $num_comma, string $counter_text): HTMLElement { $main_links_html = empty($main_links) ? "" : "

        "; $message_html = empty($main_text) ? "" : "
        $main_text
        "; @@ -44,7 +41,7 @@ public function build_body(string $sitename, string $main_links, string $main_te "; - return " + return rawHTML("

        $sitename

        $main_links_html @@ -57,6 +54,6 @@ public function build_body(string $sitename, string $main_links, string $main_te Running Shimmie2
        - "; + "); } } diff --git a/ext/image/main.php b/ext/image/main.php index 089dd217b..996bba8a3 100644 --- a/ext/image/main.php +++ b/ext/image/main.php @@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\{InputInterface,InputArgument}; use Symfony\Component\Console\Output\OutputInterface; -use function MicroHTML\{INPUT, emptyHTML}; +use function MicroHTML\{INPUT, emptyHTML, STYLE}; require_once "config.php"; @@ -74,7 +74,7 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void $config->set_string(ImageConfig::THUMB_MIME, MimeType::JPEG); break; } - $config->set_string("thumb_type", null); + $config->delete("thumb_type"); $this->set_version(ImageConfig::VERSION, 1); } @@ -86,7 +86,7 @@ public function onPageRequest(PageRequestEvent $event): void $thumb_width = $config->get_int(ImageConfig::THUMB_WIDTH, 192); $thumb_height = $config->get_int(ImageConfig::THUMB_HEIGHT, 192); - $page->add_html_header(""); + $page->add_html_header(STYLE(":root {--thumb-width: {$thumb_width}px; --thumb-height: {$thumb_height}px;}")); if ($event->page_matches("image/delete", method: "POST", permission: Permissions::DELETE_IMAGE)) { global $page, $user; @@ -213,7 +213,7 @@ public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void $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)) { + if (isset($event->image->posted)) { $event->replace('$date', autodate($event->image->posted, false)); } $event->replace("\\n", "\n"); @@ -244,7 +244,7 @@ private function send_file(int $image_id, string $type, array $params): void if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) { - $if_modified_since = preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]); + $if_modified_since = preg_replace_ex('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]); } else { $if_modified_since = ""; } diff --git a/ext/image_hash_ban/info.php b/ext/image_hash_ban/info.php index 384d409f7..65ac705c9 100644 --- a/ext/image_hash_ban/info.php +++ b/ext/image_hash_ban/info.php @@ -16,5 +16,5 @@ class ImageBanInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::MODERATION; public string $description = "Ban images based on their hash"; public ?string $documentation = -"Based on the ResolutionLimit and IPban extensions by Shish"; + "Based on the ResolutionLimit and IPban extensions by Shish"; } diff --git a/ext/image_hash_ban/main.php b/ext/image_hash_ban/main.php index 08c66a823..1b3ce314d 100644 --- a/ext/image_hash_ban/main.php +++ b/ext/image_hash_ban/main.php @@ -91,8 +91,8 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("image_hash_ban/add", method: "POST", permission: Permissions::BAN_IMAGE)) { $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; + $image = isset($input['c_image_id']) ? Image::by_id_ex($input['c_image_id']) : null; + $hash = isset($input["c_hash"]) ? $input["c_hash"] : ($image ? $image->hash : null); $reason = isset($input['c_reason']) ? $input['c_reason'] : "DNP"; if ($hash) { @@ -119,7 +119,9 @@ public function onPageRequest(PageRequestEvent $event): void $t = new HashBanTable($database->raw_db()); $t->token = $user->get_auth_token(); $t->inputs = $event->GET; - $this->theme->display_crud("Post Bans", $t->table($t->query()), $t->paginator()); + $page->set_title("Post Bans"); + $page->add_block(new NavBlock()); + $page->add_block(new Block(null, emptyHTML($t->table($t->query()), $t->paginator()))); } } diff --git a/ext/image_hash_ban/test.php b/ext/image_hash_ban/test.php index e791b4a61..967dc96b6 100644 --- a/ext/image_hash_ban/test.php +++ b/ext/image_hash_ban/test.php @@ -29,7 +29,7 @@ public function testBan(): void send_event(new ImageDeletionEvent(Image::by_id_ex($image_id), true)); // Check deleted - $this->assertException(ImageNotFound::class, function () use ($image_id) { + $this->assertException(PostNotFound::class, function () use ($image_id) { $this->get_page("post/view/$image_id"); }); diff --git a/ext/image_view_counter/config.php b/ext/image_view_counter/config.php new file mode 100644 index 000000000..2103e6e91 --- /dev/null +++ b/ext/image_view_counter/config.php @@ -0,0 +1,10 @@ +get_bool("image_viewcounter_installed") == false) { //todo + if ($config->get_bool("image_viewcounter_installed")) { + $this->set_version(ImageViewCounterConfig::VERSION, 1); + $config->delete("image_viewcounter_installed"); + } + if ($this->get_version(ImageViewCounterConfig::VERSION) < 1) { $database->create_table("image_views", " id SCORE_AIPK, image_id INTEGER NOT NULL, user_id INTEGER NOT NULL, timestamp INTEGER NOT NULL, ipaddress SCORE_INET NOT NULL"); - $config->set_bool("image_viewcounter_installed", true); + $this->set_version(ImageViewCounterConfig::VERSION, 1); } } diff --git a/ext/image_view_counter/theme.php b/ext/image_view_counter/theme.php index 485b82b72..74c10cc8d 100644 --- a/ext/image_view_counter/theme.php +++ b/ext/image_view_counter/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class ImageViewCounterTheme extends Themelet { /** @@ -17,11 +19,9 @@ public function view_popular(array $images): void $pop_images .= $this->build_thumb_html($image) . "\n"; } - $nav_html = "Index"; - - $page->set_heading($config->get_string(SetupConfig::TITLE)); - $page->add_block(new Block("Navigation", $nav_html, "left", 10)); - $page->add_block(new Block(null, $pop_images, "main", 30)); + $page->set_title($config->get_string(SetupConfig::TITLE)); + $page->add_block(new NavBlock()); + $page->add_block(new Block(null, rawHTML($pop_images), "main", 30)); } public function get_help_html(): string diff --git a/ext/index/events.php b/ext/index/events.php index 02e9ec836..121239fb2 100644 --- a/ext/index/events.php +++ b/ext/index/events.php @@ -24,7 +24,7 @@ class SearchTermParseEvent extends Event /** * @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(); diff --git a/ext/index/main.php b/ext/index/main.php index d1bbfdf39..c82570227 100644 --- a/ext/index/main.php +++ b/ext/index/main.php @@ -9,6 +9,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function MicroHTML\rawHTML; + require_once "config.php"; require_once "events.php"; @@ -43,17 +45,12 @@ public function onPageRequest(PageRequestEvent $event): void $page_number = $event->get_iarg('page_num', 1); $page_size = $config->get_int(IndexConfig::IMAGES); + $speed_hax = (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::FAST_PAGE_LIMIT)); $fast_page_limit = 500; - $ua = $_SERVER["HTTP_USER_AGENT"] ?? "No UA"; if ( - SPEED_HAX - && ( - str_contains($ua, "Googlebot") - || str_contains($ua, "YandexBot") - || str_contains($ua, "bingbot") - || str_contains($ua, "msnbot") - ) + $speed_hax + && is_bot() && ( $count_search_terms > 1 || ($count_search_terms == 1 && $search_terms[0][0] == "-") @@ -63,7 +60,7 @@ public function onPageRequest(PageRequestEvent $event): void $fast_page_limit = 10; } - if (SPEED_HAX && $page_number > $fast_page_limit && !$user->can("big_search")) { + if ($speed_hax && $page_number > $fast_page_limit && !$user->can("big_search")) { throw new PermissionDenied( "Only $fast_page_limit pages of results are searchable - " . "if you want to find older results, use more specific search terms" @@ -71,12 +68,12 @@ public function onPageRequest(PageRequestEvent $event): void } $total_pages = (int)ceil(Search::count_images($search_terms) / $config->get_int(IndexConfig::IMAGES)); - if (SPEED_HAX && $total_pages > $fast_page_limit && !$user->can("big_search")) { + if ($speed_hax && $total_pages > $fast_page_limit && !$user->can("big_search")) { $total_pages = $fast_page_limit; } $images = null; - if (SPEED_HAX) { + if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::CACHE_FIRST_FEW)) { if ($count_search_terms === 0 && ($page_number < 10)) { // extra caching for the first few post/list pages $images = cache_get_or_set( @@ -165,7 +162,7 @@ public function onCliGen(CliGenEvent $event): void $count = $input->getOption('count'); [$tag_conditions, $img_conditions, $order] = Search::terms_to_conditions($search); - if($count) { + if ($count) { $order = null; $page = null; $limit = null; @@ -180,9 +177,9 @@ public function onCliGen(CliGenEvent $event): void ); $sql_str = $q->sql; - $sql_str = preg_replace("/\s+/", " ", $sql_str); - foreach($q->variables as $key => $val) { - if(is_string($val)) { + $sql_str = preg_replace_ex("/\s+/", " ", $sql_str); + foreach ($q->variables as $key => $val) { + if (is_string($val)) { $sql_str = str_replace(":$key", "'$val'", $sql_str); } else { $sql_str = str_replace(":$key", (string)$val, $sql_str); @@ -249,6 +246,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void // If we've reached this far, and nobody else has done anything with this term, then treat it as a tag if ($event->order === null && $event->img_conditions == [] && $event->tag_conditions == []) { + assert(is_string($event->term)); $event->add_tag_condition(new TagCondition($event->term, $event->positive)); } } diff --git a/ext/index/test.php b/ext/index/test.php index ddf2ba304..93de02e08 100644 --- a/ext/index/test.php +++ b/ext/index/test.php @@ -31,12 +31,14 @@ public function testIndexPage(): void $this->get_page('post/list/1'); $this->assert_title("Shimmie"); - $this->get_page('post/list/99999'); - $this->assert_response(404); + $this->assertException(PostNotFound::class, function () { + $this->get_page('post/view/99999'); + }); # No results: 404 - $this->get_page('post/list/maumaumau/1'); - $this->assert_response(404); + $this->assertException(PostNotFound::class, function () { + $this->get_page('post/list/maumaumau/1'); + }); # One result: 302 $this->get_page("post/list/pbx/1"); diff --git a/ext/index/theme.php b/ext/index/theme.php index 4e10960b7..172837d23 100644 --- a/ext/index/theme.php +++ b/ext/index/theme.php @@ -6,8 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\emptyHTML; -use function MicroHTML\{BR,H3,HR,P}; +use function MicroHTML\{BR,H3,HR,P,META,rawHTML,emptyHTML}; class IndexTheme extends Themelet { @@ -42,7 +41,7 @@ public function display_intro(Page $page): void "; $page->set_title("Welcome to Shimmie ".VERSION); $page->set_heading("Welcome to Shimmie"); - $page->add_block(new Block("Nothing here yet!", $text, "main", 0)); + $page->add_block(new Block("Nothing here yet!", rawHTML($text), "main", 0)); } /** @@ -60,7 +59,7 @@ public function display_page(Page $page, array $images): void if (count($images) > 0) { $this->display_page_images($page, $images); } else { - $this->display_error(404, "No posts Found", "No posts were found to match the search criteria"); + throw new PostNotFound("No posts were found to match the search criteria"); } } @@ -70,14 +69,14 @@ public function display_page(Page $page, array $images): void public function display_admin_block(array $parts): void { global $page; - $page->add_block(new Block("List Controls", join("
        ", $parts), "left", 50)); + $page->add_block(new Block("List Controls", rawHTML(join("
        ", $parts)), "left", 50)); } /** * @param string[] $search_terms */ - protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string + protected function build_navigation(int $page_number, int $total_pages, array $search_terms): HTMLElement { $prev = $page_number - 1; $next = $page_number + 1; @@ -96,13 +95,13 @@ protected function build_navigation(int $page_number, int $total_pages, array $s "; - return $h_prev.' | '.$h_index.' | '.$h_next.'
        '.$h_search; + return rawHTML($h_prev.' | '.$h_index.' | '.$h_next.'
        '.$h_search); } /** * @param Image[] $images */ - protected function build_table(array $images, ?string $query): string + protected function build_table(array $images, ?string $query): HTMLElement { $h_query = html_escape($query); $table = "
        "; @@ -110,7 +109,7 @@ protected function build_table(array $images, ?string $query): string $table .= $this->build_thumb_html($image); } $table .= "
        "; - return $table; + return rawHTML($table); } protected function display_shortwiki(Page $page): void @@ -137,7 +136,7 @@ protected function display_shortwiki(Page $page): void $st = $tagcategories->getTagHtml(html_escape($st), $tag_category_dict); } $short_wiki_description = '

        '.$st.' 

        '.$short_wiki_description; - $page->add_block(new Block(null, $short_wiki_description, "main", 0, "short-wiki-description")); + $page->add_block(new Block(null, rawHTML($short_wiki_description), "main", 0, "short-wiki-description")); } } } @@ -165,7 +164,6 @@ protected function display_page_header(Page $page, array $images): void */ $page->set_title($page_title); - $page->set_heading($page_title); } /** @@ -176,7 +174,7 @@ protected function display_page_images(Page $page, array $images): void if (count($this->search_terms) > 0) { if ($this->page_number > 3) { // only index the first pages of each term - $page->add_html_header(''); + $page->add_html_header(META(["name" => "robots", "content" => "noindex, nofollow"])); } $query = url_escape(Tag::implode($this->search_terms)); $page->add_block(new Block("Posts", $this->build_table($images, "#search=$query"), "main", 10, "image-list")); diff --git a/ext/ipban/info.php b/ext/ipban/info.php index 1384d1cd8..98e036229 100644 --- a/ext/ipban/info.php +++ b/ext/ipban/info.php @@ -16,7 +16,7 @@ class IPBanInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::MODERATION; public string $description = "Ban IP addresses"; public ?string $documentation = -"Adding a Ban + "Adding a Ban
        IP: Can be a single IP (eg. 123.234.210.21), or a CIDR block (eg. 152.23.43.0/24)
        Reason: Any text, for the admin to remember why the ban was put in place
        Until: Either a date in YYYY-MM-DD format, or an offset like \"3 days\""; diff --git a/ext/ipban/main.php b/ext/ipban/main.php index df83de444..db87b4656 100644 --- a/ext/ipban/main.php +++ b/ext/ipban/main.php @@ -12,6 +12,8 @@ use MicroCRUD\EnumColumn; use MicroCRUD\Table; +use function MicroHTML\rawHTML; + class IPBanTable extends Table { public function __construct(\FFSPHP\PDO $db) @@ -157,14 +159,14 @@ public function onUserLogin(UserLoginEvent $event): void $msg .= ""; if ($row["mode"] == "ghost") { - $b = new Block(null, $msg, "main", 0); + $b = new Block(null, rawHTML($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"]; } elseif ($row["mode"] == "anon-ghost") { if ($event->user->is_anonymous()) { - $b = new Block(null, $msg, "main", 0); + $b = new Block(null, rawHTML($msg), "main", 0); $b->is_content = false; $page->add_block($b); $page->add_cookie("nocache", "Ghost Banned", time() + 60 * 60 * 2, "/"); diff --git a/ext/ipban/test.php b/ext/ipban/test.php index 3cbe5e131..e503ffffd 100644 --- a/ext/ipban/test.php +++ b/ext/ipban/test.php @@ -37,7 +37,7 @@ public function testIPBan(): void $page = $this->get_page('ip_ban/list'); $this->assertStringContainsString( "42.42.42.42", - $page->find_block("Edit IP Bans")->body + (string)$page->find_block("Edit IP Bans")->body ); // Delete ban @@ -48,7 +48,7 @@ public function testIPBan(): void $page = $this->get_page('ip_ban/list'); $this->assertStringNotContainsString( "42.42.42.42", - $page->find_block("Edit IP Bans")->body + (string)$page->find_block("Edit IP Bans")->body ); } diff --git a/ext/ipban/theme.php b/ext/ipban/theme.php index 16f7dd9a4..c5c4fa098 100644 --- a/ext/ipban/theme.php +++ b/ext/ipban/theme.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\emptyHTML; +use function MicroHTML\rawHTML; class IPBanTheme extends Themelet { @@ -21,8 +21,7 @@ public function display_bans(Page $page, HTMLElement $table, HTMLElement $pagina $paginator "; $page->set_title("IP Bans"); - $page->set_heading("IP Bans"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Edit IP Bans", $html)); + $page->add_block(new Block("Edit IP Bans", rawHTML($html))); } } diff --git a/ext/link_image/theme.php b/ext/link_image/theme.php index 1305df4ed..fa0ebfe03 100644 --- a/ext/link_image/theme.php +++ b/ext/link_image/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class LinkImageTheme extends Themelet { /** @@ -18,7 +20,7 @@ public function links_block(Page $page, array $data): void $page->add_block(new Block( "Link to Post", - " + rawHTML("
        @@ -55,13 +57,13 @@ public function links_block(Page $page, array $data): void
        - ", + "), "main", 50 )); } - protected function url(string $url, string $content, string $type): string + protected function url(string $url, ?string $content, string $type): string { if (empty($content)) { $content = $url; diff --git a/ext/link_scan/main.php b/ext/link_scan/main.php index 8c7b01c30..bbc1ec772 100644 --- a/ext/link_scan/main.php +++ b/ext/link_scan/main.php @@ -35,14 +35,14 @@ private function scan(string $text): array $ids = []; $matches = []; preg_match_all("/post\/view\/(\d+)/", $text, $matches); - foreach($matches[1] as $match) { + 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) { + foreach ($matches[1] as $match) { $img = Image::by_hash($match); if ($img) { $ids[] = $img->id; diff --git a/ext/log_db/main.php b/ext/log_db/main.php index 1d1a1efd6..67bbc4cdd 100644 --- a/ext/log_db/main.php +++ b/ext/log_db/main.php @@ -200,8 +200,11 @@ public function display(array $row): HTMLElement protected function scan_entities(string $line): string { $line = preg_replace_callback("/Image #(\d+)/s", [$this, "link_image"], $line); + assert(is_string($line)); $line = preg_replace_callback("/Post #(\d+)/s", [$this, "link_image"], $line); + assert(is_string($line)); $line = preg_replace_callback("/>>(\d+)/s", [$this, "link_image"], $line); + assert(is_string($line)); return $line; } @@ -277,11 +280,13 @@ public function onSetupBuilding(SetupBuildingEvent $event): void public function onPageRequest(PageRequestEvent $event): void { - global $database, $user; + global $database, $page, $user; if ($event->page_matches("log/view", permission: Permissions::VIEW_EVENTLOG)) { $t = new LogTable($database->raw_db()); $t->inputs = $event->GET; - $this->theme->display_crud("Event Log", $t->table($t->query()), $t->paginator()); + $page->set_title("Event Log"); + $page->add_block(new NavBlock()); + $page->add_block(new Block(null, emptyHTML($t->table($t->query()), $t->paginator()))); } } diff --git a/ext/media/events.php b/ext/media/events.php index a7bce3f48..abf13a108 100644 --- a/ext/media/events.php +++ b/ext/media/events.php @@ -27,7 +27,7 @@ public function __construct( int $target_width, int $target_height, string $resize_type = Media::RESIZE_TYPE_FIT, - string $target_mime = null, + ?string $target_mime = null, string $alpha_color = Media::DEFAULT_ALPHA_CONVERSION_COLOR, int $target_quality = 80, bool $minimize = false, diff --git a/ext/media/main.php b/ext/media/main.php index 06ef0719a..f2cd2fc2f 100644 --- a/ext/media/main.php +++ b/ext/media/main.php @@ -252,7 +252,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void $event->add_querylet(new Querylet("$field = :true", ["true" => true])); } } elseif (preg_match("/^ratio([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+):(\d+)$/i", $event->term, $matches)) { - $cmp = preg_replace('/^:/', '=', $matches[1]); + $cmp = preg_replace_ex('/^:/', '=', $matches[1]); $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("/^size([:]?<|[:]?>|[:]?<=|[:]?>=|[:|=])(\d+)x(\d+)$/i", $event->term, $matches)) { @@ -275,10 +275,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Media"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Media", $this->theme->get_help_html()); } } @@ -548,7 +545,7 @@ public static function image_resize_convert( int $new_width, int $new_height, string $output_filename, - string $output_mime = null, + ?string $output_mime = null, string $alpha_color = Media::DEFAULT_ALPHA_CONVERSION_COLOR, string $resize_type = self::RESIZE_TYPE_FIT, int $output_quality = 80, @@ -862,7 +859,7 @@ public static function video_size(string $filename): array // \Safe\shell_exec is a little broken // https://github.com/thecodingmachine/safe/issues/281 $output = shell_exec($cmd . " 2>&1"); - if(is_null($output) || $output === false) { + if (is_null($output) || $output === false) { throw new MediaException("Failed to execute command: $cmd"); } // error_log("Getting size with `$cmd`"); diff --git a/ext/media/theme.php b/ext/media/theme.php index 793206503..d77aa498a 100644 --- a/ext/media/theme.php +++ b/ext/media/theme.php @@ -10,11 +10,11 @@ public function get_help_html(): string { return '

        Search for posts based on the type of media.

        -
        content:audio
        + content:audio

        Returns posts that contain audio, including videos and audio files.

        -
        content:video
        + content:video

        Returns posts that contain video, including animated GIFs.

        These search terms depend on the posts being scanned for media content. Automatic scanning was implemented in mid-2019, so posts uploaded before, or posts uploaded on a system without ffmpeg, will require additional scanning before this will work.

        diff --git a/ext/mime/main.php b/ext/mime/main.php index d205def6b..bb6d1d8f0 100644 --- a/ext/mime/main.php +++ b/ext/mime/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + require_once "mime_map.php"; require_once "file_extension.php"; require_once "mime_type.php"; @@ -62,10 +64,7 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "File Types"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("File Types", $this->theme->get_help_html()); } } diff --git a/ext/mime/theme.php b/ext/mime/theme.php index a90f39147..1d17327f0 100644 --- a/ext/mime/theme.php +++ b/ext/mime/theme.php @@ -21,7 +21,7 @@ public function get_help_html(): string return '

        Search for posts by extension

        -
        ext=jpg
        + ext=jpg

        Returns posts with the extension "jpg".

        @@ -33,7 +33,7 @@ public function get_help_html(): string

        Search for posts by MIME type

        -
        mime=image/jpeg
        + mime=image/jpeg

        Returns posts that have the MIME type "image/jpeg".

        diff --git a/ext/not_a_tag/main.php b/ext/not_a_tag/main.php index d180da5c4..d26fc3e40 100644 --- a/ext/not_a_tag/main.php +++ b/ext/not_a_tag/main.php @@ -8,6 +8,8 @@ use MicroCRUD\TextColumn; use MicroCRUD\Table; +use function MicroHTML\{emptyHTML}; + class NotATagTable extends Table { public function __construct(\FFSPHP\PDO $db) @@ -149,7 +151,9 @@ public function onPageRequest(PageRequestEvent $event): void $t = new NotATagTable($database->raw_db()); $t->token = $user->get_auth_token(); $t->inputs = $event->GET; - $this->theme->display_crud("UnTags", $t->table($t->query()), $t->paginator()); + $page->set_title("UnTags"); + $page->add_block(new NavBlock()); + $page->add_block(new Block(null, emptyHTML($t->table($t->query()), $t->paginator()))); } } } diff --git a/ext/notes/main.php b/ext/notes/main.php index e19ce22c5..9f0dec627 100644 --- a/ext/notes/main.php +++ b/ext/notes/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class Notes extends Extension { /** @var NotesTheme */ @@ -11,7 +13,11 @@ class Notes extends Extension public function onInitExt(InitExtEvent $event): void { + global $config; Image::$prop_types["notes"] = ImagePropType::INT; + $config->set_default_int("notesNotesPerPage", 20); + $config->set_default_int("notesRequestsPerPage", 20); + $config->set_default_int("notesHistoriesPerPage", 20); } public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void @@ -67,14 +73,25 @@ public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void "); $database->execute("CREATE INDEX note_histories_image_id_idx ON note_histories(image_id)", []); - $config->set_int("notesNotesPerPage", 20); - $config->set_int("notesRequestsPerPage", 20); - $config->set_int("notesHistoriesPerPage", 20); - $this->set_version("ext_notes_version", 1); } } + public function onPageNavBuilding(PageNavBuildingEvent $event): void + { + $event->add_nav_link("note", new Link('note/requests'), "Notes"); + } + + public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void + { + if ($event->parent == "note") { + $event->add_nav_link("note_requests", new Link('note/requests'), "Requests"); + $event->add_nav_link("note_list", new Link('note/list'), "List"); + $event->add_nav_link("note_updated", new Link('note/updated'), "Updates"); + $event->add_nav_link("note_help", new Link('ext_doc/notes'), "Help"); + } + } + public function onPageRequest(PageRequestEvent $event): void { global $page, $user; @@ -90,6 +107,9 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("note/history/{note_id}", paged: true)) { $this->get_history($event->get_iarg('note_id'), $event->get_iarg('page_num', 1) - 1); } + if ($event->page_matches("note_history/{image_id}", paged: true)) { + $this->get_image_history($event->get_iarg('image_id'), $event->get_iarg('page_num', 1) - 1); + } if ($event->page_matches("note/revert/{noteID}/{reviewID}", permission: Permissions::NOTES_EDIT)) { $noteID = $event->get_iarg('noteID'); $reviewID = $event->get_iarg('reviewID'); @@ -135,6 +155,11 @@ public function onPageRequest(PageRequestEvent $event): void } } + public function onRobotsBuilding(RobotsBuildingEvent $event): void + { + $event->add_disallow("note_history"); + } + /* * HERE WE LOAD THE NOTES IN THE IMAGE @@ -166,6 +191,8 @@ public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): if ($user->can(Permissions::NOTES_REQUEST)) { $event->add_part($this->theme->request_button($event->image->id)); } + + $event->add_button("View Note History", "note_history/{$event->image->id}", 20); } @@ -198,10 +225,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Notes"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Notes", $this->theme->get_help_html()); } } @@ -219,7 +243,7 @@ private function get_notes(int $imageID): array SELECT * FROM notes WHERE enable = :enable AND image_id = :image_id - ORDER BY date ASC + ORDER BY date ASC, id ASC ", ['enable' => '1', 'image_id' => $imageID]); } @@ -349,7 +373,7 @@ private function get_notes_list(int $pageNumber): void SELECT DISTINCT image_id FROM notes WHERE enable = :enable - ORDER BY date DESC LIMIT :limit OFFSET :offset", + ORDER BY date DESC, id DESC LIMIT :limit OFFSET :offset", ['enable' => 1, 'offset' => $pageNumber * $notesPerPage, 'limit' => $notesPerPage] ); @@ -373,7 +397,7 @@ private function get_notes_requests(int $pageNumber): void " SELECT DISTINCT image_id FROM note_request - ORDER BY date DESC LIMIT :limit OFFSET :offset", + ORDER BY date DESC, id DESC LIMIT :limit OFFSET :offset", ["offset" => $pageNumber * $requestsPerPage, "limit" => $requestsPerPage] ); @@ -427,7 +451,7 @@ private function get_histories(int $pageNumber): void "FROM note_histories AS h " . "INNER JOIN users AS u " . "ON u.id = h.user_id " . - "ORDER BY date DESC LIMIT :limit OFFSET :offset", + "ORDER BY date DESC, note_id DESC LIMIT :limit OFFSET :offset", ['offset' => $pageNumber * $historiesPerPage, 'limit' => $historiesPerPage] ); @@ -448,7 +472,7 @@ private function get_history(int $noteID, int $pageNumber): void "INNER JOIN users AS u " . "ON u.id = h.user_id " . "WHERE note_id = :note_id " . - "ORDER BY date DESC LIMIT :limit OFFSET :offset", + "ORDER BY date DESC, note_id DESC LIMIT :limit OFFSET :offset", ['note_id' => $noteID, 'offset' => $pageNumber * $historiesPerPage, 'limit' => $historiesPerPage] ); @@ -457,6 +481,27 @@ private function get_history(int $noteID, int $pageNumber): void $this->theme->display_history($histories, $pageNumber + 1, $totalPages); } + private function get_image_history(int $imageID, int $pageNumber): void + { + global $config, $database; + + $historiesPerPage = $config->get_int('notesHistoriesPerPage'); + + $histories = $database->get_all( + "SELECT h.note_id, h.review_id, h.image_id, h.date, h.note, u.name AS user_name " . + "FROM note_histories AS h " . + "INNER JOIN users AS u " . + "ON u.id = h.user_id " . + "WHERE image_id = :image_id " . + "ORDER BY date DESC, note_id DESC LIMIT :limit OFFSET :offset", + ['image_id' => $imageID, 'offset' => $pageNumber * $historiesPerPage, 'limit' => $historiesPerPage] + ); + + $totalPages = (int) ceil($database->get_one("SELECT COUNT(*) FROM note_histories WHERE image_id = :image_id", ['image_id' => $imageID]) / $historiesPerPage); + + $this->theme->display_image_history($histories, $imageID, $pageNumber + 1, $totalPages); + } + /** * HERE GO BACK IN HISTORY AND SET THE OLD NOTE. IF WAS REMOVED WE RE-ADD IT. */ diff --git a/ext/notes/script.js b/ext/notes/script.js index 80ea8995d..9b4e79bf8 100644 --- a/ext/notes/script.js +++ b/ext/notes/script.js @@ -88,7 +88,7 @@ function renderEditor(noteDiv, note) { dragStart = { x: e.pageX, y: e.pageY, - mode: getArea(e.offsetX, e.offsetY, noteDiv.offsetWidth, noteDiv.offsetHeight), + mode: getArea(e.offsetX, e.offsetY, noteDiv.clientWidth, noteDiv.clientHeight), }; noteDiv.classList.add("dragging"); }); @@ -113,7 +113,7 @@ function renderEditor(noteDiv, note) { noteDiv.style.width = (note.width * scale) + (e.pageX - dragStart.x) + 'px'; } } else { - let area = getArea(e.offsetX, e.offsetY, noteDiv.offsetWidth, noteDiv.offsetHeight); + let area = getArea(e.offsetX, e.offsetY, noteDiv.clientWidth, noteDiv.clientHeight); if(area == "c") { noteDiv.style.cursor = 'move'; } else { @@ -126,8 +126,8 @@ function renderEditor(noteDiv, note) { dragStart = null; note.x1 = Math.round(noteDiv.offsetLeft / scale); note.y1 = Math.round(noteDiv.offsetTop / scale); - note.width = Math.round(noteDiv.offsetWidth / scale); - note.height = Math.round(noteDiv.offsetHeight / scale); + note.width = Math.round(noteDiv.clientWidth / scale); + note.height = Math.round(noteDiv.clientHeight / scale); renderNotes(); } noteDiv.addEventListener('mouseup', _commit); @@ -150,7 +150,7 @@ function renderEditor(noteDiv, note) { save.innerText = 'Save'; save.addEventListener('click', () => { if(note.note_id == null) { - fetch('/note/create_note', { + fetch(shm_make_link('note/create_note'), { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -169,7 +169,7 @@ function renderEditor(noteDiv, note) { alert(error); }); } else { - fetch('/note/update_note', { + fetch(shm_make_link('note/update_note'), { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -205,7 +205,7 @@ function renderEditor(noteDiv, note) { deleteNote.innerText = 'Delete'; deleteNote.addEventListener('click', () => { // TODO: delete note from server - fetch('/note/delete_note', { + fetch(shm_make_link('note/delete_note'), { method: 'POST', headers: { 'Content-Type': 'application/json' diff --git a/ext/notes/theme.php b/ext/notes/theme.php index a14996d80..0ca1b476e 100644 --- a/ext/notes/theme.php +++ b/ext/notes/theme.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\INPUT; +use function MicroHTML\{INPUT,SCRIPT,rawHTML}; /** * @phpstan-type NoteHistory array{image_id:int,note_id:int,review_id:int,user_name:string,note:string,date:string} @@ -61,12 +61,15 @@ public function display_note_system(Page $page, int $image_id, array $recovered_ 'note_id' => $note["id"], ]; } - $page->add_html_header(""); + $page->add_html_header(SCRIPT( + ["type" => "text/javascript"], + " + window.notes = ".\Safe\json_encode($to_json)."; + window.notes_image_id = $image_id; + window.notes_admin = ".($adminOptions ? "true" : "false")."; + window.notes_edit = ".($editOptions ? "true" : "false")."; + " + )); } /** @@ -86,8 +89,7 @@ public function display_note_list(array $images, int $pageNumber, int $totalPage $this->display_paginator($page, "note/list", null, $pageNumber, $totalPages); $page->set_title("Notes"); - $page->set_heading("Notes"); - $page->add_block(new Block("Notes", $pool_images, "main", 20)); + $page->add_block(new Block("Notes", rawHTML($pool_images), "main", 20)); } /** @@ -107,8 +109,7 @@ public function display_note_requests(array $images, int $pageNumber, int $total $this->display_paginator($page, "requests/list", null, $pageNumber, $totalPages); $page->set_title("Note Requests"); - $page->set_heading("Note Requests"); - $page->add_block(new Block("Note Requests", $pool_images, "main", 20)); + $page->add_block(new Block("Note Requests", rawHTML($pool_images), "main", 20)); } /** @@ -166,8 +167,7 @@ public function display_histories(array $histories, int $pageNumber, int $totalP $html = $this->get_history($histories); $page->set_title("Note Updates"); - $page->set_heading("Note Updates"); - $page->add_block(new Block("Note Updates", $html, "main", 10)); + $page->add_block(new Block("Note Updates", rawHTML($html), "main", 10)); $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages); } @@ -182,30 +182,45 @@ public function display_history(array $histories, int $pageNumber, int $totalPag $html = $this->get_history($histories); $page->set_title("Note History"); - $page->set_heading("Note History"); - $page->add_block(new Block("Note History", $html, "main", 10)); + $page->add_block(new Block("Note History", rawHTML($html), "main", 10)); $this->display_paginator($page, "note/updated", null, $pageNumber, $totalPages); } + /** + * @param NoteHistory[] $histories + */ + public function display_image_history(array $histories, int $imageID, int $pageNumber, int $totalPages): void + { + global $page; + + $html = $this->get_history($histories); + + $page->set_title("Note History #$imageID"); + $page->set_heading("Note History #$imageID"); + $page->add_block(new Block("Note History #$imageID", $html, "main", 10)); + + $this->display_paginator($page, "note_history/$imageID", null, $pageNumber, $totalPages); + } + public function get_help_html(): string { return '

        Search for posts with notes.

        -
        note=noted
        + note=noted

        Returns posts with a note matching "noted".

        -
        notes>0
        + notes>0

        Returns posts with 1 or more notes.

        Can use <, <=, >, >=, or =.

        -
        notes_by=username
        + notes_by=username

        Returns posts with note(s) by "username".

        -
        notes_by_user_id=123
        + notes_by_user_id=123

        Returns posts with note(s) by user 123.

        '; diff --git a/ext/numeric_score/main.php b/ext/numeric_score/main.php index 52b442262..18f185d63 100644 --- a/ext/numeric_score/main.php +++ b/ext/numeric_score/main.php @@ -8,6 +8,8 @@ use GQLA\Field; use GQLA\Mutation; +use function MicroHTML\rawHTML; + #[Type(name: "NumericScoreVote")] class NumericScoreVote { @@ -199,18 +201,36 @@ public function onPageRequest(PageRequestEvent $event): void $totaldate = $year."/".$month."/".$day; - $sql = "SELECT id FROM images WHERE EXTRACT(YEAR FROM posted) = :year"; + if ($database->get_driver_id() === DatabaseDriverID::SQLITE) { + $sql = "SELECT id FROM images WHERE strftime('%Y', posted) = cast(:year as text)"; + $month = str_pad(strval($month), 2, "0", STR_PAD_LEFT); + $day = str_pad(strval($day), 2, "0", STR_PAD_LEFT); + } else { + $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"; + if ($database->get_driver_id() === DatabaseDriverID::SQLITE) { + $sql .= " AND strftime('%m', posted) = cast(:month as text) AND strftime('%d', posted) = cast(:day as text)"; + } else { + $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", \Safe\strtotime($totaldate)). + $current = date("F jS, Y", \Safe\strtotime($totaldate)); $name = "day"; $fmt = "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m\\&\\d\\a\\y\\=d"; } elseif ($event->page_matches("popular_by_month")) { - $sql .= " AND EXTRACT(MONTH FROM posted) = :month"; + if ($database->get_driver_id() === DatabaseDriverID::SQLITE) { + $sql .= " AND strftime('%m', posted) = cast(:month as text)"; + } else { + $sql .= " AND EXTRACT(MONTH FROM posted) = :month"; + } $args = array_merge($args, ["month" => $month]); + // PHP's -1 month and +1 month functionality break when modifying dates that are on the 31st of the month. + // See Example #3 on https://www.php.net/manual/en/datetime.modify.php + // To get around this, set the day to 1 when doing month work. + $totaldate = $year."/".$month."/01"; $current = date("F Y", \Safe\strtotime($totaldate)); $name = "month"; $fmt = "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m"; @@ -225,7 +245,6 @@ public function onPageRequest(PageRequestEvent $event): void $sql .= " AND NOT numeric_score=0 ORDER BY numeric_score DESC LIMIT :limit OFFSET 0"; //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); @@ -290,10 +309,7 @@ public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Numeric Score"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Numeric Score", $this->theme->get_help_html()); } } @@ -310,22 +326,12 @@ public function onSearchTermParse(SearchTermParseEvent $event): void $event->add_querylet(new Querylet("numeric_score $cmp $score")); } elseif (preg_match("/^upvoted_by[=|:](.*)$/i", $event->term, $matches)) { $duser = User::by_name($matches[1]); - if (is_null($duser)) { - throw new SearchTermParseException( - "Can't find the user named ".html_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" => $duser->id] )); } elseif (preg_match("/^downvoted_by[=|:](.*)$/i", $event->term, $matches)) { $duser = User::by_name($matches[1]); - if (is_null($duser)) { - throw new SearchTermParseException( - "Can't find the user named ".html_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" => $duser->id] diff --git a/ext/numeric_score/test.php b/ext/numeric_score/test.php index 77eba21d6..dfb53765d 100644 --- a/ext/numeric_score/test.php +++ b/ext/numeric_score/test.php @@ -46,20 +46,22 @@ public function testNumericScore(): void $this->assertEquals(PageMode::REDIRECT, $page->mode); # and downvote - $page = $this->get_page("post/list/downvoted_by=test/1"); - $this->assertEquals(404, $page->code); + $this->assertException(PostNotFound::class, function () { + $this->get_page("post/list/downvoted_by=test/1"); + }); # test errors - $this->assertException(SearchTermParseException::class, function () { + $this->assertException(UserNotFound::class, function () { $this->get_page("post/list/upvoted_by=asdfasdf/1"); }); - $this->assertException(SearchTermParseException::class, function () { + $this->assertException(UserNotFound::class, function () { $this->get_page("post/list/downvoted_by=asdfasdf/1"); }); - - $page = $this->get_page("post/list/upvoted_by_id=0/1"); - $this->assertEquals(404, $page->code); - $page = $this->get_page("post/list/downvoted_by_id=0/1"); - $this->assertEquals(404, $page->code); + $this->assertException(PostNotFound::class, function () { + $this->get_page("post/list/upvoted_by_id=0/1"); + }); + $this->assertException(PostNotFound::class, function () { + $this->get_page("post/list/downvoted_by_id=0/1"); + }); } } diff --git a/ext/numeric_score/theme.php b/ext/numeric_score/theme.php index a8a04d6f9..c5200ed43 100644 --- a/ext/numeric_score/theme.php +++ b/ext/numeric_score/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class NumericScoreTheme extends Themelet { public function get_voter(Image $image): void @@ -47,7 +49,7 @@ public function get_voter(Image $image): void "; } - $page->add_block(new Block("Post Score", $html, "left", 20)); + $page->add_block(new Block("Post Score", rawHTML($html), "left", 20)); } public function get_nuller(User $duser): void @@ -58,7 +60,7 @@ public function get_nuller(User $duser): void "; - $page->add_block(new Block("Votes", $html, "main", 80)); + $page->add_block(new Block("Votes", rawHTML($html), "main", 80)); } /** @@ -85,9 +87,9 @@ public function view_popular(array $images, string $totaldate, string $current, $nav_html = "Index"; - $page->set_heading($config->get_string(SetupConfig::TITLE)); - $page->add_block(new Block("Navigation", $nav_html, "left", 10)); - $page->add_block(new Block(null, $html, "main", 30)); + $page->set_title($config->get_string(SetupConfig::TITLE)); + $page->add_block(new Block("Navigation", rawHTML($nav_html), "left", 10)); + $page->add_block(new Block(null, rawHTML($html), "main", 30)); } @@ -95,38 +97,38 @@ public function get_help_html(): string { return '

        Search for posts that have received numeric scores by the score or by the scorer.

        -
        score=1
        + score=1

        Returns posts with a score of 1.

        -
        score>0
        + score>0

        Returns posts with a score of 1 or more.

        Can use <, <=, >, >=, or =.

        -
        upvoted_by=username
        + upvoted_by=username

        Returns posts upvoted by "username".

        -
        upvoted_by_id=123
        + upvoted_by_id=123

        Returns posts upvoted by user 123.

        -
        downvoted_by=username
        + downvoted_by=username

        Returns posts downvoted by "username".

        -
        downvoted_by_id=123
        + downvoted_by_id=123

        Returns posts downvoted by user 123.

        -
        order:score_desc
        + order:score_desc

        Sorts the search results by score, descending.

        -
        order:score_asc
        + order:score_asc

        Sorts the search results by score, ascending.

        '; diff --git a/ext/ouroboros_api/info.php b/ext/ouroboros_api/info.php index 3998ac109..590dcdeb5 100644 --- a/ext/ouroboros_api/info.php +++ b/ext/ouroboros_api/info.php @@ -14,7 +14,7 @@ class OuroborosAPIInfo extends ExtensionInfo public string $description = "Ouroboros-like API for Shimmie"; public ExtensionCategory $category = ExtensionCategory::INTEGRATION; public ?string $documentation = -"Currently working features + "Currently working features
        • Post:
            diff --git a/ext/ouroboros_api/main.php b/ext/ouroboros_api/main.php index 6aae49fec..98c7fab43 100644 --- a/ext/ouroboros_api/main.php +++ b/ext/ouroboros_api/main.php @@ -201,7 +201,6 @@ public function __construct(array $tag) class OuroborosAPI extends Extension { - private ?PageRequestEvent $event; private ?string $type; public const HEADER_HTTP_200 = 'OK'; @@ -246,7 +245,6 @@ public function onPageRequest(PageRequestEvent $event): void global $page, $user; if (preg_match("%\.(xml|json)$%", implode('/', $event->args), $matches) === 1) { - $this->event = $event; $this->type = $matches[1]; if ($this->type == 'json') { $page->set_mime('application/json; charset=utf-8'); @@ -257,7 +255,7 @@ public function onPageRequest(PageRequestEvent $event): void $this->tryAuth(); if ($event->page_matches('post')) { - if ($this->match('create')) { + if ($this->match($event, 'create')) { // Create if ($user->can(Permissions::CREATE_IMAGE)) { $md5 = !empty($_REQUEST['md5']) ? filter_var_ex($_REQUEST['md5'], FILTER_SANITIZE_STRING) : null; @@ -265,13 +263,13 @@ public function onPageRequest(PageRequestEvent $event): void } else { $this->sendResponse(403, 'You cannot create new posts'); } - } elseif ($this->match('update')) { + } elseif ($this->match($event, 'update')) { throw new ServerError("update not implemented"); - } elseif ($this->match('show')) { + } elseif ($this->match($event, 'show')) { // Show $id = !empty($_REQUEST['id']) ? (int)filter_var_ex($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) : null; $this->postShow($id); - } elseif ($this->match('index') || $this->match('list')) { + } elseif ($this->match($event, 'index') || $this->match($event, 'list')) { // List $limit = !empty($_REQUEST['limit']) ? intval( filter_var_ex($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT) @@ -286,7 +284,7 @@ public function onPageRequest(PageRequestEvent $event): void $this->postIndex($limit, $p, $tags); } } elseif ($event->page_matches('tag')) { - if ($this->match('index') || $this->match('list')) { + if ($this->match($event, 'index') || $this->match($event, 'list')) { $limit = !empty($_REQUEST['limit']) ? intval( filter_var_ex($_REQUEST['limit'], FILTER_SANITIZE_NUMBER_INT) ) : 50; @@ -297,18 +295,12 @@ public function onPageRequest(PageRequestEvent $event): void $_REQUEST['order'], FILTER_SANITIZE_STRING ) : 'date'; - $id = !empty($_REQUEST['id']) ? intval( - filter_var_ex($_REQUEST['id'], FILTER_SANITIZE_NUMBER_INT) - ) : null; - $after_id = !empty($_REQUEST['after_id']) ? intval( - filter_var_ex($_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( $_REQUEST['name_pattern'], FILTER_SANITIZE_STRING ) : ''; - $this->tagIndex($limit, $p, $order, $id, $after_id, $name, $name_pattern); + $this->tagIndex($limit, $p, $order, $name, $name_pattern); } } } elseif ($event->page_matches('post/show')) { @@ -337,17 +329,18 @@ protected function postCreate(OuroborosPost $post, ?string $md5 = ''): void return; } } + /** @var array $meta */ $meta = []; $meta['tags'] = $post->tags; - $meta['source'] = $post->source; + $meta['source'] = $post->source ?? ''; 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( - $post->file_url, - FILTER_VALIDATE_URL - ) !== false + if ( + empty($post->file) && + !empty($post->file_url) && + filter_var_ex($post->file_url, FILTER_VALIDATE_URL) !== false ) { // Transload from source $meta['file'] = shm_tempnam('transload_' . $config->get_string(UploadConfig::TRANSLOAD_ENGINE)); @@ -361,6 +354,7 @@ protected function postCreate(OuroborosPost $post, ?string $md5 = ''): void $meta['hash'] = \Safe\md5_file($meta['file']); } else { // Use file + assert(!is_null($post->file)); $meta['file'] = $post->file['tmp_name']; $meta['filename'] = $post->file['name']; $meta['hash'] = \Safe\md5_file($meta['file']); @@ -379,7 +373,7 @@ protected function postCreate(OuroborosPost $post, ?string $md5 = ''): void send_event(new TagSetEvent($img, $merged)); // This is really the only thing besides tags we should care - if (isset($meta['source'])) { + if (!empty($meta['source'])) { send_event(new SourceSetEvent($img, $meta['source'])); } $this->sendResponse(200, self::OK_POST_CREATE_UPDATE . ' ID: ' . $img->id); @@ -405,7 +399,7 @@ protected function postCreate(OuroborosPost $post, ?string $md5 = ''): void /** * Wrapper for getting a single post */ - protected function postShow(int $id = null): void + protected function postShow(?int $id = null): void { if (!is_null($id)) { $post = new _SafeOuroborosImage(Image::by_id_ex($id)); @@ -437,7 +431,7 @@ protected function postIndex(int $limit, int $page, array $tags): void * 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, string $name, string $name_pattern): void { global $database, $config; $start = ($page - 1) * $limit; @@ -624,8 +618,8 @@ private function tryAuth(): void /** * Helper for matching API methods from event */ - private function match(string $page): bool + private function match(PageRequestEvent $event, string $page): bool { - return (preg_match("%{$page}\.(xml|json)$%", implode('/', $this->event->args), $matches) === 1); + return (preg_match("%{$page}\.(xml|json)$%", implode('/', $event->args), $matches) === 1); } } diff --git a/ext/pm/info.php b/ext/pm/info.php index b8ef42c65..20856c726 100644 --- a/ext/pm/info.php +++ b/ext/pm/info.php @@ -16,7 +16,7 @@ class PrivMsgInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::FEATURE; public string $description = "Allow users to send messages to eachother"; public ?string $documentation = -"PMs show up on a user's profile page, readable by that user + "PMs show up on a user's profile page, readable by that user as well as board admins. To send a PM, visit another user's profile page and a box will be shown."; } diff --git a/ext/pm/main.php b/ext/pm/main.php index 42ac6a6f5..7e4157e9d 100644 --- a/ext/pm/main.php +++ b/ext/pm/main.php @@ -231,7 +231,7 @@ public function onPageRequest(PageRequestEvent $event): void $pm_id = $event->get_iarg('pm_id'); $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]); if (is_null($pm)) { - $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id"); + throw new ObjectNotFound("No such PM"); } elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) { $from_user = User::by_id((int)$pm["from_id"]); if ($pm["to_id"] == $user->id) { @@ -240,7 +240,7 @@ public function onPageRequest(PageRequestEvent $event): void } $pmo = PM::from_row($pm); $this->theme->display_message($page, $from_user, $user, $pmo); - if($user->can(Permissions::SEND_PM)) { + if ($user->can(Permissions::SEND_PM)) { $this->theme->display_composer($page, $user, $from_user, "Re: ".$pmo->subject); } } else { @@ -251,7 +251,7 @@ public function onPageRequest(PageRequestEvent $event): void $pm_id = int_escape($event->req_POST("pm_id")); $pm = $database->get_row("SELECT * FROM private_message WHERE id = :id", ["id" => $pm_id]); if (is_null($pm)) { - $this->theme->display_error(404, "No such PM", "There is no PM #$pm_id"); + throw new ObjectNotFound("No such PM"); } elseif (($pm["to_id"] == $user->id) || $user->can(Permissions::VIEW_OTHER_PMS)) { $database->execute("DELETE FROM private_message WHERE id = :id", ["id" => $pm_id]); $cache->delete("pm-count-{$user->id}"); @@ -293,7 +293,7 @@ private function count_pms(User $user): int global $database; return cache_get_or_set( - "pm-count:{$user->id}", + "pm-count-{$user->id}", fn () => $database->get_one(" SELECT count(*) FROM private_message diff --git a/ext/pm/theme.php b/ext/pm/theme.php index 24f45ca4a..03525195c 100644 --- a/ext/pm/theme.php +++ b/ext/pm/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class PrivMsgTheme extends Themelet { /** @@ -57,7 +59,7 @@ public function display_pms(Page $page, array $pms): void "; - $page->add_block(new Block("Private Messages", $html, "main", 40, "private-messages")); + $page->add_block(new Block("Private Messages", rawHTML($html), "main", 40, "private-messages")); } public function display_composer(Page $page, User $from, User $to, string $subject = ""): void @@ -77,7 +79,7 @@ public function display_composer(Page $page, User $from, User $to, string $subje EOD; - $page->add_block(new Block("Write a PM", $html, "main", 50)); + $page->add_block(new Block("Write a PM", rawHTML($html), "main", 50)); } public function display_message(Page $page, User $from, User $to, PM $pm): void @@ -85,6 +87,6 @@ public function display_message(Page $page, User $from, User $to, PM $pm): void $page->set_title("Private Message"); $page->set_heading(html_escape($pm->subject)); $page->add_block(new NavBlock()); - $page->add_block(new Block("Message from {$from->name}", format_text($pm->message), "main", 10)); + $page->add_block(new Block("Message from {$from->name}", rawHTML(format_text($pm->message)), "main", 10)); } } diff --git a/ext/pools/info.php b/ext/pools/info.php index 78b2065b7..d39ce9ae9 100644 --- a/ext/pools/info.php +++ b/ext/pools/info.php @@ -15,5 +15,5 @@ class PoolsInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::FEATURE; public string $description = "Allow users to create groups of images and order them."; public ?string $documentation = -"This extension allows users to created named groups of images, and order the images within the group. Useful for related images like in a comic, etc."; + "This extension allows users to created named groups of images, and order the images within the group. Useful for related images like in a comic, etc."; } diff --git a/ext/pools/main.php b/ext/pools/main.php index 6ff101357..2c7983797 100644 --- a/ext/pools/main.php +++ b/ext/pools/main.php @@ -45,7 +45,7 @@ class PoolCreationEvent extends Event public function __construct( string $title, - User $pool_user = null, + ?User $pool_user = null, bool $public = false, string $description = "" ) { @@ -304,7 +304,9 @@ public function onPageRequest(PageRequestEvent $event): void WHERE pool_id=:pid AND i.id=:iid", ["pid" => $pool_id, "iid" => (int) $row['image_id']] ); - $images[] = ($image ? new Image($image) : null); + if ($image) { + $images[] = new Image($image); + } } $this->theme->edit_order($page, $pool, $images); @@ -483,7 +485,7 @@ public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $event->add_block(new Block("Pools", $this->theme->get_help_html())); + $event->add_section("Pools", $this->theme->get_help_html()); } } @@ -551,13 +553,15 @@ public function onTagTermParse(TagTermParseEvent $event): void public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): void { - global $database; + global $database, $user; - $options = $database->get_pairs("SELECT id,title FROM pools ORDER BY title"); + if (!$user->can(Permissions::POOLS_UPDATE)) { + $options = $database->get_pairs("SELECT id,title FROM pools ORDER BY title"); - // TODO: Don't cast into strings, make BABBE accept HTMLElement instead. - $event->add_action("bulk_pool_add_existing", "Add To (P)ool", "p", "", (string) $this->theme->get_bulk_pool_selector($options)); - $event->add_action("bulk_pool_add_new", "Create Pool", "", "", (string) $this->theme->get_bulk_pool_input($event->search_terms)); + // TODO: Don't cast into strings, make BABBE accept HTMLElement instead. + $event->add_action("bulk_pool_add_existing", "Add To (P)ool", "p", "", (string) $this->theme->get_bulk_pool_selector($options)); + $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 diff --git a/ext/pools/test.php b/ext/pools/test.php index 9f181e753..b8380a13f 100644 --- a/ext/pools/test.php +++ b/ext/pools/test.php @@ -78,7 +78,7 @@ public function testSearch(): void $this->get_page("post/list/pool=$pool_id/1"); $this->assert_text("Pool"); - $this->get_page("post/list/pool_by_name=demo_pool/1"); + $this->get_page("post/list/pool_by_name=foo/1"); $this->assert_text("Pool"); } diff --git a/ext/pools/theme.php b/ext/pools/theme.php index d508e8e2c..737cd238e 100644 --- a/ext/pools/theme.php +++ b/ext/pools/theme.php @@ -30,7 +30,7 @@ public function pool_info(array $navIDs): void 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")); } @@ -58,7 +58,7 @@ public function list_pools(Page $page, array $pools, string $search, int $pageNu $pool_rows = []; foreach ($pools as $pool) { $pool_link = SHM_A("pool/view/" . $pool->id, $pool->title); - $user_link = SHM_A("user/" . url_escape($pool->user_name), $pool->user_name); + $user_link = SHM_A("user/" . url_escape($pool->user_name), $pool->user_name ?? "No Name"); $pool_rows[] = TR( TD(["class" => "left"], $pool_link), @@ -75,7 +75,7 @@ public function list_pools(Page $page, array $pools, string $search, int $pageNu ); $order_arr = ['created' => 'Recently created', 'updated' => 'Last updated', 'name' => 'Name', 'count' => 'Post Count']; - $order_selected = $page->get_cookie('ui-order-pool'); + $order_selected = $page->get_cookie('ui-order-pool') ?? ""; $order_sel = SHM_SELECT("order_pool", $order_arr, selected_options: [$order_selected], attrs: ["id" => "order_pool"]); $this->display_top(null, "Pools"); @@ -110,7 +110,6 @@ private function display_top(?Pool $pool, string $heading, bool $check_all = fal global $page, $user; $page->set_title($heading); - $page->set_heading($heading); $poolnav = emptyHTML( SHM_A("pool/list", "Pool Index"), @@ -127,7 +126,7 @@ private function display_top(?Pool $pool, string $heading, bool $check_all = fal $page->add_block(new NavBlock()); $page->add_block(new Block("Pool Navigation", $poolnav, "left", 10)); - $page->add_block(new Block("Search", $search, "left", 10)); + $page->add_block(new Block("Search", rawHTML($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 @@ -136,7 +135,7 @@ private function display_top(?Pool $pool, string $heading, bool $check_all = fal } } $tfe = send_event(new TextFormattingEvent($pool->description)); - $page->add_block(new Block(html_escape($pool->title), $tfe->formatted, "main", 10)); + $page->add_block(new Block(html_escape($pool->title), rawHTML($tfe->formatted), "main", 10)); } } diff --git a/ext/post_owner/main.php b/ext/post_owner/main.php index 3c8712922..1ecdaf170 100644 --- a/ext/post_owner/main.php +++ b/ext/post_owner/main.php @@ -28,11 +28,7 @@ public function onImageInfoSet(ImageInfoSetEvent $event): void $owner = $event->get_param('owner'); if ($user->can(Permissions::EDIT_IMAGE_OWNER) && !is_null($owner)) { $owner_ob = User::by_name($owner); - if (!is_null($owner_ob)) { - send_event(new OwnerSetEvent($event->image, $owner_ob)); - } else { - throw new UserNotFound("Error: No user with that name was found."); - } + send_event(new OwnerSetEvent($event->image, $owner_ob)); } } diff --git a/ext/post_source/main.php b/ext/post_source/main.php index f25fbcd4a..6cfe2c374 100644 --- a/ext/post_source/main.php +++ b/ext/post_source/main.php @@ -7,9 +7,9 @@ class SourceSetEvent extends Event { public Image $image; - public ?string $source; + public string $source; - public function __construct(Image $image, string $source = null) + public function __construct(Image $image, string $source) { parent::__construct(); $this->image = $image; @@ -36,7 +36,7 @@ public function onImageInfoSet(ImageInfoSetEvent $event): void { global $config, $page, $user; $source = $event->get_param('source'); - if(is_null($source) && $config->get_bool(UploadConfig::TLSOURCE)) { + if (is_null($source) && $config->get_bool(UploadConfig::TLSOURCE)) { $source = $event->get_param('url'); } if ($user->can(Permissions::EDIT_IMAGE_SOURCE) && !is_null($source)) { @@ -93,7 +93,7 @@ public function onTagTermCheck(TagTermCheckEvent $event): void public function onTagTermParse(TagTermParseEvent $event): void { if (preg_match("/^source[=|:](.*)$/i", $event->term, $matches)) { - $source = ($matches[1] !== "none" ? $matches[1] : null); + $source = ($matches[1] !== "none" ? $matches[1] : ""); send_event(new SourceSetEvent(Image::by_id_ex($event->image_id), $source)); } } diff --git a/ext/post_source/theme.php b/ext/post_source/theme.php index a31994b6f..626d3b13b 100644 --- a/ext/post_source/theme.php +++ b/ext/post_source/theme.php @@ -36,7 +36,7 @@ public function get_source_editor_html(Image $image): HTMLElement ); } - protected function format_source(string $source = null): HTMLElement + protected function format_source(?string $source = null): HTMLElement { if (!empty($source)) { if (!str_contains($source, "://")) { diff --git a/ext/post_tags/info.php b/ext/post_tags/info.php index 6845b06aa..73c0942ce 100644 --- a/ext/post_tags/info.php +++ b/ext/post_tags/info.php @@ -16,7 +16,7 @@ class PostTagsInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::METADATA; public string $description = "Allow images to have tags assigned to them"; public ?string $documentation = -" Here is a list of the tagging metatags available out of the box; + " Here is a list of the tagging metatags available out of the box; Shimmie extensions may provide other metatags:
            • source=(*, none) eg -- using this metatag will ignore anything set in the \"Source\" box diff --git a/ext/post_tags/theme.php b/ext/post_tags/theme.php index 786654765..4dab4dcb4 100644 --- a/ext/post_tags/theme.php +++ b/ext/post_tags/theme.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\{joinHTML, A, TEXTAREA, TR, TH, TD, INPUT}; +use function MicroHTML\{joinHTML, A, TEXTAREA, TR, TH, TD, INPUT, rawHTML}; class PostTagsTheme extends Themelet { @@ -22,7 +22,7 @@ public function display_mass_editor(): void "; - $page->add_block(new Block("Mass Tag Edit", $html)); + $page->add_block(new Block("Mass Tag Edit", rawHTML($html))); } public function get_tag_editor_html(Image $image): HTMLElement diff --git a/ext/private_image/main.php b/ext/private_image/main.php index c4533fe7c..7d220a064 100644 --- a/ext/private_image/main.php +++ b/ext/private_image/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + abstract class PrivateImageConfig { public const VERSION = "ext_private_image_version"; @@ -152,10 +154,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Private Posts"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Private Posts", $this->theme->get_help_html()); } } diff --git a/ext/private_image/theme.php b/ext/private_image/theme.php index a2aa45a75..d8d490c98 100644 --- a/ext/private_image/theme.php +++ b/ext/private_image/theme.php @@ -12,11 +12,11 @@ public function get_help_html(): string { return '

              Search for posts that are private/public.

              -
              private:yes
              + private:yes

              Returns posts that are private, restricted to yourself if you are not an admin.

              -
              private:no
              + private:no

              Returns posts that are public.

              '; diff --git a/ext/qr_code/info.php b/ext/qr_code/info.php index dac6bf0d3..12a1cb377 100644 --- a/ext/qr_code/info.php +++ b/ext/qr_code/info.php @@ -15,7 +15,7 @@ class QRImageInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "Shows a QR Code for downloading a post to cell phones"; public ?string $documentation = -"Shows a QR Code for downloading a post to cell phones. + "Shows a QR Code for downloading a post to cell phones.
              Based on Artanis's Link to Post Extension.
              Further modified by Shish to remove the 7MB local QR generator and replace it with a link to Google Chart APIs"; } diff --git a/ext/qr_code/theme.php b/ext/qr_code/theme.php index 4579880cb..ded3a1bc9 100644 --- a/ext/qr_code/theme.php +++ b/ext/qr_code/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class QRImageTheme extends Themelet { public function links_block(string $link): void @@ -12,7 +14,7 @@ public function links_block(string $link): void $page->add_block(new Block( "QR Code", - "QR Code", + rawHTML("QR Code"), "left", 50 )); diff --git a/ext/random_image/info.php b/ext/random_image/info.php index 6d103042b..150c38182 100644 --- a/ext/random_image/info.php +++ b/ext/random_image/info.php @@ -15,7 +15,7 @@ class RandomImageInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "Do things with a random post"; public ?string $documentation = -"Viewing a random post + "Viewing a random post
              Visit /random_image/view

              Downloading a random post
              Link to /random_image/download. This will give diff --git a/ext/random_image/main.php b/ext/random_image/main.php index dd81eb577..f3e663520 100644 --- a/ext/random_image/main.php +++ b/ext/random_image/main.php @@ -21,7 +21,7 @@ public function onPageRequest(PageRequestEvent $event): void $search_terms = Tag::explode($event->get_arg('search', ""), false); $image = Image::by_random($search_terms); if (!$image) { - throw new ImageNotFound("Couldn't find any posts randomly"); + throw new PostNotFound("Couldn't find any posts randomly"); } if ($action === "download") { diff --git a/ext/random_image/test.php b/ext/random_image/test.php index 44a00077a..6d45b79ce 100644 --- a/ext/random_image/test.php +++ b/ext/random_image/test.php @@ -32,7 +32,7 @@ public function testPostListBlock(): void # enabled, no image = no text $config->set_bool("show_random_block", true); $page = $this->get_page("post/list"); - $this->assertNull($page->find_block("Random Post")); + $this->assertException(\Exception::class, function () use ($page) {$page->find_block("Random Post");}); # enabled, image = text $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test"); @@ -42,11 +42,11 @@ public function testPostListBlock(): void # disabled, image = no text $config->set_bool("show_random_block", false); $page = $this->get_page("post/list"); - $this->assertNull($page->find_block("Random Post")); + $this->assertException(\Exception::class, function () use ($page) {$page->find_block("Random Post");}); # disabled, no image = no image $this->delete_image($image_id); $page = $this->get_page("post/list"); - $this->assertNull($page->find_block("Random Post")); + $this->assertException(\Exception::class, function () use ($page) {$page->find_block("Random Post");}); } } diff --git a/ext/random_image/theme.php b/ext/random_image/theme.php index 198598da0..f405fec19 100644 --- a/ext/random_image/theme.php +++ b/ext/random_image/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + use function MicroHTML\DIV; use function MicroHTML\A; use function MicroHTML\IMG; @@ -15,11 +17,11 @@ public function display_random(Page $page, Image $image): void $page->add_block(new Block("Random Post", $this->build_random_html($image), "left", 8)); } - public function build_random_html(Image $image, ?string $query = null): string + public function build_random_html(Image $image, ?string $query = null): HTMLElement { $tsize = get_thumbnail_size($image->width, $image->height); - return (string)DIV( + return DIV( ["style" => "text-align: center;"], A( ["href" => make_link("post/view/{$image->id}", $query)], diff --git a/ext/random_list/info.php b/ext/random_list/info.php index 8a52e22bc..5310084d7 100644 --- a/ext/random_list/info.php +++ b/ext/random_list/info.php @@ -15,6 +15,6 @@ class RandomListInfo extends ExtensionInfo public string $license = self::LICENSE_GPLV2; public string $description = "Allows displaying a page with random posts"; public ?string $documentation = -"Random post list can be accessed through www.yoursite.com/random + "Random post list can be accessed through www.yoursite.com/random It is recommended that you create a link to this page so users know it exists."; } diff --git a/ext/random_list/theme.php b/ext/random_list/theme.php index a16eeae49..afdd0e8dd 100644 --- a/ext/random_list/theme.php +++ b/ext/random_list/theme.php @@ -4,6 +4,10 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + +use function MicroHTML\rawHTML; + class RandomListTheme extends Themelet { /** @var string[] */ @@ -37,7 +41,7 @@ public function display_page(Page $page, array $images): void $html .= "

              No posts were found to match the search criteria"; } - $page->add_block(new Block("Random Posts", $html)); + $page->add_block(new Block("Random Posts", rawHTML($html))); $nav = $this->build_navigation($this->search_terms); $page->add_block(new Block("Navigation", $nav, "left", 0)); @@ -46,7 +50,7 @@ public function display_page(Page $page, array $images): void /** * @param string[] $search_terms */ - protected function build_navigation(array $search_terms): string + protected function build_navigation(array $search_terms): HTMLElement { $h_search_string = html_escape(Tag::implode($search_terms)); $h_search_link = make_link("random"); @@ -58,6 +62,6 @@ protected function build_navigation(array $search_terms): string "; - return $h_search; + return rawHTML($h_search); } } diff --git a/ext/rating/info.php b/ext/rating/info.php index 167e6c6e8..39b4e6e83 100644 --- a/ext/rating/info.php +++ b/ext/rating/info.php @@ -16,7 +16,7 @@ class RatingsInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::METADATA; public string $description = "Allow users to rate images \"safe\", \"questionable\" or \"explicit\""; public ?string $documentation = -"This shimmie extension provides filter: + "This shimmie extension provides filter:

              • rating = (safe|questionable|explicit|unknown)
                  diff --git a/ext/rating/main.php b/ext/rating/main.php index 81ca9ca3a..02e5c86b5 100644 --- a/ext/rating/main.php +++ b/ext/rating/main.php @@ -246,7 +246,7 @@ public function onImageInfoSet(ImageInfoSetEvent $event): void public function onParseLinkTemplate(ParseLinkTemplateEvent $event): void { - if(!is_null($event->image['rating'])) { + if (!is_null($event->image['rating'])) { $event->replace('$rating', $this->rating_to_human($event->image['rating'])); } } @@ -255,7 +255,7 @@ public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { $ratings = self::get_sorted_ratings(); - $event->add_block(new Block("Ratings", $this->theme->get_help_html($ratings))); + $event->add_section("Ratings", $this->theme->get_help_html($ratings)); } } @@ -440,7 +440,7 @@ public static function get_sorted_ratings(): array * @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(); diff --git a/ext/rating/test.php b/ext/rating/test.php index eff467d3f..79061e4d2 100644 --- a/ext/rating/test.php +++ b/ext/rating/test.php @@ -68,7 +68,9 @@ public function testUserConfig(): void // 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); + $next = $image_s->get_next(); + $this->assertNotNull($next); + $this->assertEquals($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 diff --git a/ext/ratings_blur/info.php b/ext/ratings_blur/info.php new file mode 100644 index 000000000..6b79d3518 --- /dev/null +++ b/ext/ratings_blur/info.php @@ -0,0 +1,17 @@ + ""]; + public string $license = self::LICENSE_WTFPL; + public string $description = "Blurs thumbs based on rating, users can override. Requires 'Post Ratings'."; + public array $dependencies = [RatingsInfo::KEY]; +} diff --git a/ext/ratings_blur/main.php b/ext/ratings_blur/main.php new file mode 100644 index 000000000..5f7fd52ba --- /dev/null +++ b/ext/ratings_blur/main.php @@ -0,0 +1,79 @@ +set_default_array(RatingsBlurConfig::GLOBAL_DEFAULTS, RatingsBlurConfig::DEFAULT_OPTIONS); + } + + public function onInitUserConfig(InitUserConfigEvent $event): void + { + global $config; + + $event->user_config->set_default_array(RatingsBlurConfig::USER_DEFAULTS, $config->get_array(RatingsBlurConfig::GLOBAL_DEFAULTS)); + } + + public function onUserOptionsBuilding(UserOptionsBuildingEvent $event): void + { + global $user; + + $levels = Ratings::get_user_class_privs($user); + $options = []; + foreach ($levels as $level) { + $options[ImageRating::$known_ratings[$level]->name] = $level; + } + $null_option = RatingsBlurConfig::NULL_OPTION; + $options[$null_option] = $null_option; + + $sb = $event->panel->create_new_block("Rating Blur Filter"); + $sb->start_table(); + $sb->add_multichoice_option(RatingsBlurConfig::USER_DEFAULTS, $options, "Blurred Ratings: ", true); + $sb->end_table(); + $sb->add_label("This controls which posts will be blurred. Unselecting all will revert to default settings, so select '$null_option' to blur no images."); + } + + public function onSetupBuilding(SetupBuildingEvent $event): void + { + $ratings = Ratings::get_sorted_ratings(); + + $options = []; + foreach ($ratings as $key => $rating) { + $options[$rating->name] = $rating->code; + } + $null_option = RatingsBlurConfig::NULL_OPTION; + $options[$null_option] = $null_option; + + $sb = $event->panel->create_new_block("Post Rating Blur Defaults"); + $sb->start_table(); + $sb->add_multichoice_option(RatingsBlurConfig::GLOBAL_DEFAULTS, $options, "Default blurred ratings", true); + $sb->end_table(); + $sb->add_label("Unselecting all will revert to default settings, so select '$null_option' to blur no images."); + } + + public function blur(string $rating): bool + { + global $user_config; + + $blur_ratings = $user_config->get_array(RatingsBlurConfig::USER_DEFAULTS); + if (in_array(RatingsBlurConfig::NULL_OPTION, $blur_ratings)) { + return false; + } + return in_array($rating, $blur_ratings); + } +} diff --git a/ext/ratings_blur/style.css b/ext/ratings_blur/style.css new file mode 100644 index 000000000..adb42ef61 --- /dev/null +++ b/ext/ratings_blur/style.css @@ -0,0 +1,7 @@ +.blur IMG { + filter: blur(5px); + transition: all .3s ease-in; +} +.blur IMG:hover { + filter: blur(0px); +} \ No newline at end of file diff --git a/ext/ratings_blur/test.php b/ext/ratings_blur/test.php new file mode 100644 index 000000000..46db667dc --- /dev/null +++ b/ext/ratings_blur/test.php @@ -0,0 +1,133 @@ +log_in_as_user(); + $image_id_s = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $image_s = Image::by_id_ex($image_id_s); + send_event(new RatingSetEvent($image_s, "s")); + + // the safe image should not insert a blur class + $this->get_page("post/list"); + $this->assert_no_text("blur"); + + $image_id_e = $this->post_image("tests/bedroom_workshop.jpg", "bedroom"); + $image_e = Image::by_id_ex($image_id_e); + send_event(new RatingSetEvent($image_e, "e")); + + // the explicit image should insert a blur class + $this->get_page("post/list"); + $this->assert_text("blur"); + } + + public function testRatingBlurGlobalConfig(): void + { + global $config, $user_config; + + // change global setting: don't blur explict, only blur safe + $config->set_array(RatingsBlurConfig::GLOBAL_DEFAULTS, ["s"]); + // create a new user to simulate inheriting the global default without manually setting the user default + $this->create_test_user($this->username); + + $image_id_e = $this->post_image("tests/bedroom_workshop.jpg", "bedroom"); + $image_e = Image::by_id_ex($image_id_e); + send_event(new RatingSetEvent($image_e, "e")); + + // the explicit image should not insert a blur class + $this->get_page("post/list"); + $this->assert_no_text("blur"); + + $image_id_s = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $image_s = Image::by_id_ex($image_id_s); + send_event(new RatingSetEvent($image_s, "s")); + + // the safe image should insert a blur class + $this->get_page("post/list"); + $this->assert_text("blur"); + + // change global setting: don't blur any + $config->set_array(RatingsBlurConfig::GLOBAL_DEFAULTS, [RatingsBlurConfig::NULL_OPTION]); + // create a new user to simulate inheriting the global default without manually setting the user default + $this->delete_test_user($this->username); + $this->create_test_user($this->username); + + $this->get_page("post/list"); + $this->assert_no_text("blur"); + + $this->delete_test_user($this->username); + } + + public function testRatingBlurUserConfig(): void + { + global $config, $user_config; + // set global default to blur all, so we can test it is overriden + $config->set_array(RatingsBlurConfig::GLOBAL_DEFAULTS, array_keys(ImageRating::$known_ratings)); + + $this->log_in_as_user(); + + // don't blur explict, blur safe + $user_config->set_array(RatingsBlurConfig::USER_DEFAULTS, ["s"]); + + $image_id_e = $this->post_image("tests/bedroom_workshop.jpg", "bedroom"); + $image_e = Image::by_id_ex($image_id_e); + send_event(new RatingSetEvent($image_e, "e")); + + // the explicit image should not insert a blur class + $this->get_page("post/list"); + $this->assert_no_text("blur"); + + $image_id_s = $this->post_image("tests/pbx_screenshot.jpg", "pbx"); + $image_s = Image::by_id_ex($image_id_s); + send_event(new RatingSetEvent($image_s, "s")); + + // the safe image should insert a blur class + $this->get_page("post/list"); + $this->assert_text("blur"); + + // don't blur any + $user_config->set_array(RatingsBlurConfig::USER_DEFAULTS, [RatingsBlurConfig::NULL_OPTION]); + + $this->get_page("post/list"); + $this->assert_no_text("blur"); + } + + private function create_test_user(string $username): void + { + $uce = send_event(new UserCreationEvent($username, $username, $username, "$username@test.com", false)); + send_event(new UserLoginEvent($uce->get_user())); + } + + private function delete_test_user(string $username): void + { + $this->log_out(); + $this->log_in_as_admin(); + send_event(new PageRequestEvent( + "POST", + "user_admin/delete_user", + [], + ['id' => (string)User::by_name($username)->id, 'with_images' => '', 'with_comments' => ''] + )); + $this->log_out(); + } + + // 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(RatingsBlurConfig::GLOBAL_DEFAULTS, RatingsBlurConfig::DEFAULT_OPTIONS); + + $this->log_in_as_user(); + $user_config->set_array(RatingsBlurConfig::USER_DEFAULTS, RatingsBlurConfig::DEFAULT_OPTIONS); + + parent::tearDown(); + } +} diff --git a/ext/regen_thumb/info.php b/ext/regen_thumb/info.php index 080115a4f..b47ffeb37 100644 --- a/ext/regen_thumb/info.php +++ b/ext/regen_thumb/info.php @@ -16,7 +16,7 @@ class RegenThumbInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::FILE_HANDLING; public string $description = "Regenerate a thumbnail image"; public ?string $documentation = -"This adds a button in the post control section on a post's view page, which allows an admin to regenerate + "This adds a button in the post control section on a post's view page, which allows an admin to regenerate a post's thumbnail; useful for instance if the first attempt failed due to lack of memory, and memory has since been increased."; } diff --git a/ext/regen_thumb/theme.php b/ext/regen_thumb/theme.php index 89b847507..07cfd9b9c 100644 --- a/ext/regen_thumb/theme.php +++ b/ext/regen_thumb/theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -use function MicroHTML\INPUT; +use function MicroHTML\{META,rawHTML}; class RegenThumbTheme extends Themelet { @@ -14,8 +14,7 @@ class RegenThumbTheme extends Themelet public function display_results(Page $page, Image $image): void { $page->set_title("Thumbnail Regenerated"); - $page->set_heading("Thumbnail Regenerated"); - $page->add_html_header(""); + $page->add_html_header(META(['http-equiv' => 'cache-control', 'content' => 'no-cache'])); $page->add_block(new NavBlock()); $page->add_block(new Block("Thumbnail", $this->build_thumb_html($image))); } @@ -62,6 +61,6 @@ public function display_admin_block(): void

                  "; - $page->add_block(new Block("Regen Thumbnails", $html)); + $page->add_block(new Block("Regen Thumbnails", rawHTML($html))); } } diff --git a/ext/relationships/main.php b/ext/relationships/main.php index a12cdb46c..6fb9930ce 100644 --- a/ext/relationships/main.php +++ b/ext/relationships/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class ImageRelationshipSetEvent extends Event { public int $child_id; @@ -96,10 +98,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Relationships"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Relationships", $this->theme->get_help_html()); } } diff --git a/ext/relationships/theme.php b/ext/relationships/theme.php index 6cf7c39b8..3028a1386 100644 --- a/ext/relationships/theme.php +++ b/ext/relationships/theme.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\{TR, TH, TD, emptyHTML, DIV, INPUT}; +use function MicroHTML\{TR, TH, TD, emptyHTML, DIV, INPUT, rawHTML}; class RelationshipsTheme extends Themelet { @@ -37,7 +37,7 @@ public function relationship_info(Image $image): void $parent_summary_html .= "« hide"; $parent_thumb_html .= ""; $html = $parent_summary_html . $parent_thumb_html; - $page->add_block(new Block(null, $html, "main", 5, "PostRelationshipsParent")); + $page->add_block(new Block(null, rawHTML($html), "main", 5, "PostRelationshipsParent")); } if (bool_escape($image['has_children'])) { @@ -54,7 +54,7 @@ public function relationship_info(Image $image): void $child_summary_html .= "« hide"; $child_thumb_html .= ""; $html = $child_summary_html . $child_thumb_html; - $page->add_block(new Block(null, $html, "main", 5, "PostRelationshipsChildren")); + $page->add_block(new Block(null, rawHTML($html), "main", 5, "PostRelationshipsChildren")); } } } @@ -66,7 +66,7 @@ public function get_parent_editor_html(Image $image): HTMLElement return SHM_POST_INFO( "Parent", strval($image['parent_id']) ?: "None", - !$user->is_anonymous() ? INPUT(["type" => "number", "name" => "parent", "value" => $image['parent_id']]) : null + $user->can(Permissions::EDIT_IMAGE_RELATIONSHIPS) ? INPUT(["type" => "number", "name" => "parent", "value" => $image['parent_id']]) : null ); } @@ -75,23 +75,23 @@ public function get_help_html(): string { return '

                  Search for posts that have parent/child relationships.

                  -
                  parent=any
                  + parent=any

                  Returns posts that have a parent.

                  -
                  parent=none
                  + parent=none

                  Returns posts that have no parent.

                  -
                  parent=123
                  + parent=123

                  Returns posts that have image 123 set as parent.

                  -
                  child=any
                  + child=any

                  Returns posts that have at least 1 child.

                  -
                  child=none
                  + child=none

                  Returns posts that have no children.

                  '; diff --git a/ext/replace_file/main.php b/ext/replace_file/main.php index d139bad2e..7a37ddfb2 100644 --- a/ext/replace_file/main.php +++ b/ext/replace_file/main.php @@ -23,7 +23,7 @@ public function onPageRequest(PageRequestEvent $event): void $image_id = $event->get_iarg('image_id'); $image = Image::by_id_ex($image_id); - if(empty($event->get_POST("url")) && count($_FILES) == 0) { + if (empty($event->get_POST("url")) && count($_FILES) == 0) { $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("replace/$image_id")); return; @@ -36,7 +36,7 @@ public function onPageRequest(PageRequestEvent $event): void } elseif (count($_FILES) > 0) { send_event(new ImageReplaceEvent($image, $_FILES["data"]['tmp_name'])); } - if($event->get_POST("source")) { + if ($event->get_POST("source")) { send_event(new SourceSetEvent($image, $event->req_POST("source"))); } $cache->delete("thumb-block:{$image_id}"); @@ -67,12 +67,10 @@ public function onImageReplace(ImageReplaceEvent $event): void $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']}" - ); + try { + \Safe\copy($event->tmp_filename, $target); + } catch (\Exception $e) { + throw new ImageReplaceException("Failed to copy file from uploads ({$event->tmp_filename}) to archive ($target): {$e->getMessage()}"); } unlink($event->tmp_filename); diff --git a/ext/replace_file/theme.php b/ext/replace_file/theme.php index f0df2d8db..d82254187 100644 --- a/ext/replace_file/theme.php +++ b/ext/replace_file/theme.php @@ -58,7 +58,6 @@ public function display_replace_page(Page $page, int $image_id): void ); $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)); } diff --git a/ext/report_image/theme.php b/ext/report_image/theme.php index 57117f89a..d70b58f46 100644 --- a/ext/report_image/theme.php +++ b/ext/report_image/theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -use function MicroHTML\INPUT; +use function MicroHTML\{INPUT,rawHTML}; /** * @phpstan-type Report array{id: int, image: Image, reason: string, reporter_name: string} @@ -55,9 +55,8 @@ public function display_reported_images(Page $page, array $reports): void "; $page->set_title("Reported Posts"); - $page->set_heading("Reported Posts"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Reported Posts", $html)); + $page->add_block(new Block("Reported Posts", rawHTML($html))); } /** @@ -93,7 +92,7 @@ public function display_image_banner(Image $image, array $reports): void "; - $page->add_block(new Block("Report Post", $html, "left")); + $page->add_block(new Block("Report Post", rawHTML($html), "left")); } public function get_nuller(User $duser): void @@ -104,6 +103,6 @@ public function get_nuller(User $duser): void 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)); + $page->add_block(new Block("Reports", rawHTML($html), "main", 80)); } } diff --git a/ext/res_limit/test.php b/ext/res_limit/test.php index 618a15ff8..71e87b509 100644 --- a/ext/res_limit/test.php +++ b/ext/res_limit/test.php @@ -28,8 +28,8 @@ public function testResLimitSmall(): void global $config; $config->set_int("upload_min_height", 900); $config->set_int("upload_min_width", 900); - $config->set_int("upload_max_height", -1); - $config->set_int("upload_max_width", -1); + $config->delete("upload_max_height"); + $config->delete("upload_max_width"); $config->set_string("upload_ratios", "4:3 16:9"); $this->log_in_as_user(); @@ -57,10 +57,10 @@ public function testResLimitLarge(): void public function testResLimitRatio(): void { global $config; - $config->set_int("upload_min_height", -1); - $config->set_int("upload_min_width", -1); - $config->set_int("upload_max_height", -1); - $config->set_int("upload_max_width", -1); + $config->delete("upload_min_height"); + $config->delete("upload_min_width"); + $config->delete("upload_max_height"); + $config->delete("upload_max_width"); $config->set_string("upload_ratios", "16:9"); $e = $this->assertException(UploadException::class, function () { @@ -74,11 +74,11 @@ public function testResLimitRatio(): void public function tearDown(): void { global $config; - $config->set_int("upload_min_height", -1); - $config->set_int("upload_min_width", -1); - $config->set_int("upload_max_height", -1); - $config->set_int("upload_max_width", -1); - $config->set_string("upload_ratios", ""); + $config->delete("upload_min_height"); + $config->delete("upload_min_width"); + $config->delete("upload_max_height"); + $config->delete("upload_max_width"); + $config->delete("upload_ratios"); parent::tearDown(); } diff --git a/ext/reverse_search_links/main.php b/ext/reverse_search_links/main.php index 92fcfe8d9..392d4f07d 100644 --- a/ext/reverse_search_links/main.php +++ b/ext/reverse_search_links/main.php @@ -23,7 +23,7 @@ public function onDisplayingImage(DisplayingImageEvent $event): void // only support image types $supported_types = [MimeType::JPEG, MimeType::GIF, MimeType::PNG, MimeType::WEBP]; - if(in_array($event->image->get_mime(), $supported_types)) { + if (in_array($event->image->get_mime(), $supported_types)) { $this->theme->reverse_search_block($page, $event->image); } } diff --git a/ext/reverse_search_links/theme.php b/ext/reverse_search_links/theme.php index dfff6ea75..d54a1e8a3 100644 --- a/ext/reverse_search_links/theme.php +++ b/ext/reverse_search_links/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class ReverseSearchLinksTheme extends Themelet { public function reverse_search_block(Page $page, Image $image): void @@ -22,13 +24,13 @@ public function reverse_search_block(Page $page, Image $image): void $enabled_services = $config->get_array(ReverseSearchLinksConfig::ENABLED_SERVICES); $html = ""; - foreach($links as $name => $link) { + 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)); + $page->add_block(new Block("Reverse Image Search", rawHTML($html), "main", 20)); } } diff --git a/ext/rss_comments/main.php b/ext/rss_comments/main.php index e274a15ca..14a616c64 100644 --- a/ext/rss_comments/main.php +++ b/ext/rss_comments/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\LINK; + class RSSComments extends Extension { public function onPostListBuilding(PostListBuildingEvent $event): void @@ -11,8 +13,12 @@ public function onPostListBuilding(PostListBuildingEvent $event): void global $config, $page; $title = $config->get_string(SetupConfig::TITLE); - $page->add_html_header(""); + $page->add_html_header(LINK([ + 'rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => "$title - Comments", + 'href' => make_link("rss/comments") + ])); } public function onPageRequest(PageRequestEvent $event): void diff --git a/ext/rss_images/main.php b/ext/rss_images/main.php index 85d8ce8d8..956a595fb 100644 --- a/ext/rss_images/main.php +++ b/ext/rss_images/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\{LINK}; + class RSSImages extends Extension { public function onPostListBuilding(PostListBuildingEvent $event): void @@ -13,11 +15,19 @@ public function onPostListBuilding(PostListBuildingEvent $event): void if (count($event->search_terms) > 0) { $search = url_escape(Tag::implode($event->search_terms)); - $page->add_html_header(""); + $page->add_html_header(LINK([ + 'rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => "$title - Posts with tags: $search", + 'href' => make_link("rss/images/$search/1") + ])); } else { - $page->add_html_header(""); + $page->add_html_header(LINK([ + 'rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => "$title - Posts", + 'href' => make_link("rss/images/1") + ])); } } @@ -31,7 +41,7 @@ public function onPageRequest(PageRequestEvent $event): void $search_terms = Tag::explode($event->get_arg('search', "")); $page_number = $event->get_iarg('page_num', 1); $page_size = $config->get_int(IndexConfig::IMAGES); - if (SPEED_HAX && $page_number > 9) { + if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::RSS_LIMIT) && $page_number > 9) { return; } $images = Search::find_images(($page_number - 1) * $page_size, $page_size, $search_terms); diff --git a/ext/s3/main.php b/ext/s3/main.php index 72a237c43..80c6d4548 100644 --- a/ext/s3/main.php +++ b/ext/s3/main.php @@ -56,15 +56,17 @@ public function onAdminBuilding(AdminBuildingEvent $event): void public function onAdminAction(AdminActionEvent $event): void { global $database; - if($event->action == "s3_process") { - foreach($database->get_all( + if ($event->action == "s3_process") { + foreach ($database->get_all( "SELECT * FROM s3_sync_queue ORDER BY time ASC LIMIT :count", ["count" => isset($event->params['count']) ? int_escape($event->params["count"]) : 10] ) as $row) { - if($row['action'] == "S") { + if ($row['action'] == "S") { $image = Image::by_hash($row['hash']); - $this->sync_post($image); - } elseif($row['action'] == "D") { + if ($image) { + $this->sync_post($image); + } + } elseif ($row['action'] == "D") { $this->remove_file($row['hash']); } } @@ -81,15 +83,17 @@ public function onCliGen(CliGenEvent $event): void global $database; $count = $database->get_one("SELECT COUNT(*) FROM s3_sync_queue"); $output->writeln("{$count} items in queue"); - foreach($database->get_all( + 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") { + 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") { + if ($image) { + $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 { @@ -103,8 +107,8 @@ public function onCliGen(CliGenEvent $event): void ->setDescription('Search for some images, and sync them to s3') ->setCode(function (InputInterface $input, OutputInterface $output): int { $query = Tag::explode($input->getArgument('query')); - foreach(Search::find_images_iterable(tags: $query) as $image) { - if($this->sync_post($image)) { + foreach (Search::find_images_iterable(tags: $query) as $image) { + if ($this->sync_post($image)) { print("{$image->id}: {$image->hash}\n"); } else { print("{$image->id}: {$image->hash} (skipped)\n"); @@ -171,7 +175,7 @@ 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)) { + if (is_null($access_key_id) || is_null($access_key_secret)) { return null; } $endpoint = $config->get_string(S3Config::ENDPOINT); @@ -195,7 +199,7 @@ private function is_busy(): bool { global $config; $this->synced++; - if(PHP_SAPI == "cli") { + if (PHP_SAPI == "cli") { return false; // CLI can go on for as long as it wants } return $this->synced > $config->get_int(UploadConfig::COUNT); @@ -210,20 +214,20 @@ private function sync_post(Image $image, ?array $new_tags = null, bool $overwrit global $config; $client = $this->get_client(); - if(is_null($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)) { + if (!$overwrite && $client->doesObjectExist($image_bucket, $key)) { return false; } - if($this->is_busy()) { + if ($this->is_busy()) { $this->enqueue($image->hash, "S"); } else { - if(is_null($new_tags)) { + if (is_null($new_tags)) { $friendly = $image->parse_link_template('$id - $tags.$ext'); } else { $_orig_tags = $image->get_tag_array(); @@ -248,10 +252,10 @@ private function remove_file(string $hash): void { global $config; $client = $this->get_client(); - if(is_null($client)) { + if (is_null($client)) { return; } - if($this->is_busy()) { + if ($this->is_busy()) { $this->enqueue($hash, "D"); } else { $client->deleteObject([ diff --git a/ext/setup/main.php b/ext/setup/main.php index 3d0011285..f453f641a 100644 --- a/ext/setup/main.php +++ b/ext/setup/main.php @@ -8,6 +8,8 @@ use Symfony\Component\Console\Input\{InputInterface,InputArgument}; use Symfony\Component\Console\Output\OutputInterface; +use function MicroHTML\rawHTML; + require_once "config.php"; /* @@ -16,11 +18,11 @@ class ConfigSaveEvent extends Event { public Config $config; - /** @var array $values */ + /** @var array> $values */ public array $values; /** - * @param array $values + * @param array> $values */ public function __construct(Config $config, array $values) { @@ -28,6 +30,55 @@ public function __construct(Config $config, array $values) $this->config = $config; $this->values = $values; } + + /** + * Convert POST data to settings data, eg + * + * $_POST = [ + * "_type_mynull" => "string", + * "_type_mystring" => "string", + * "_config_mystring" => "hello world!", + * "_type_myint" => "int", + * "_config_myint" => "42KB", + * ] + * + * becomes + * + * $config = [ + * "mynull" => null, + * "mystring" => "hello world!", + * "myint" => 43008, + * ] + * + * @param array $post + * @return array> + */ + public static function postToSettings(array $post): array + { + $settings = []; + foreach ($post as $key => $type) { + if (str_starts_with($key, "_type_")) { + $key = str_replace("_type_", "", $key); + $value = $post["_config_$key"] ?? null; + if ($type === "string") { + $settings[$key] = $value; + } elseif ($type === "int") { + assert(is_string($value)); + $settings[$key] = $value ? parse_shorthand_int($value) : null; + } elseif ($type === "bool") { + $settings[$key] = $value === "on"; + } elseif ($type === "array") { + $settings[$key] = $value; + } else { + if (is_array($value)) { + $value = implode(", ", $value); + } + throw new InvalidInput("Invalid type '$value' for key '$key'"); + } + } + } + return $settings; + } } /* @@ -49,9 +100,9 @@ class SetupPanel { /** @var SetupBlock[] */ public array $blocks = []; - public BaseConfig $config; + public Config $config; - public function __construct(BaseConfig $config) + public function __construct(Config $config) { $this->config = $config; } @@ -66,44 +117,44 @@ public function create_new_block(string $title): SetupBlock class SetupBlock extends Block { - public ?string $header; - public ?string $body; - public BaseConfig $config; + public ?string $str_body; + public Config $config; - public function __construct(string $title, BaseConfig $config) + public function __construct(string $title, Config $config) { - parent::__construct($title, "", "main", 50); + parent::__construct($title, rawHTML(""), "main", 50); $this->config = $config; + $this->str_body = ""; } public function add_label(string $text): void { - $this->body .= $text; + $this->str_body .= $text; } public function start_table(): void { - $this->body .= ""; + $this->str_body .= "
                  "; } public function end_table(): void { - $this->body .= "
                  "; + $this->str_body .= ""; } public function start_table_row(): void { - $this->body .= ""; + $this->str_body .= ""; } public function end_table_row(): void { - $this->body .= ""; + $this->str_body .= ""; } public function start_table_head(): void { - $this->body .= ""; + $this->str_body .= ""; } public function end_table_head(): void { - $this->body .= ""; + $this->str_body .= ""; } public function add_table_header(string $content, int $colspan = 2): void { @@ -116,30 +167,30 @@ public function add_table_header(string $content, int $colspan = 2): void public function start_table_cell(int $colspan = 1): void { - $this->body .= ""; + $this->str_body .= ""; } public function end_table_cell(): void { - $this->body .= ""; + $this->str_body .= ""; } public function add_table_cell(string $content, int $colspan = 1): void { $this->start_table_cell($colspan); - $this->body .= $content; + $this->str_body .= $content; $this->end_table_cell(); } public function start_table_header_cell(int $colspan = 1, string $align = 'right'): void { - $this->body .= ""; + $this->str_body .= ""; } public function end_table_header_cell(): void { - $this->body .= ""; + $this->str_body .= ""; } public function add_table_header_cell(string $content, int $colspan = 1): void { $this->start_table_header_cell($colspan); - $this->body .= $content; + $this->str_body .= $content; $this->end_table_header_cell(); } @@ -157,7 +208,7 @@ private function format_option( $this->start_table_header_cell($label_row ? 2 : 1, $label_row ? 'center' : 'right'); } if (!is_null($label)) { - $this->body .= ""; + $this->str_body .= ""; } if ($table_row) { @@ -172,7 +223,7 @@ private function format_option( if ($table_row) { $this->start_table_cell($label_row ? 2 : 1); } - $this->body .= $html; + $this->str_body .= $html; if ($table_row) { $this->end_table_cell(); } @@ -181,7 +232,7 @@ private function format_option( } } - 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): void { $val = html_escape($this->config->get_string($name)); @@ -191,7 +242,7 @@ public function add_text_option(string $name, string $label = null, bool $table_ $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): void { $val = html_escape($this->config->get_string($name)); @@ -202,7 +253,7 @@ public function add_longtext_option(string $name, string $label = null, bool $ta $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): void { $checked = $this->config->get_bool($name) ? " checked" : ""; @@ -221,13 +272,13 @@ public function add_bool_option(string $name, string $label = null, bool $table_ $this->format_option($name, $html, null, $table_row); } - // public function add_hidden_option($name, $label=null) { + // public function add_hidden_option($name) { // global $config; // $val = $config->get_string($name); - // $this->body .= ""; + // $this->str_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): void { $val = $this->config->get_int($name); @@ -237,9 +288,9 @@ public function add_int_option(string $name, string $label = null, bool $table_r $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): void { - $val = to_shorthand_int($this->config->get_int($name)); + $val = to_shorthand_int($this->config->get_int($name, 0)); $html = "\n"; $html .= "\n"; @@ -249,7 +300,7 @@ public function add_shorthand_int_option(string $name, string $label = null, boo /** * @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): void { if (is_int(array_values($options)[0])) { $current = $this->config->get_int($name); @@ -275,7 +326,7 @@ public function add_choice_option(string $name, array $options, string $label = /** * @param array $options */ - public function add_multichoice_option(string $name, array $options, string $label = null, bool $table_row = false): void + public function add_multichoice_option(string $name, array $options, ?string $label = null, bool $table_row = false): void { $current = $this->config->get_array($name, []); @@ -294,7 +345,7 @@ public function add_multichoice_option(string $name, array $options, string $lab $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): void { $val = html_escape($this->config->get_string($name)); @@ -343,8 +394,7 @@ public function onPageRequest(PageRequestEvent $event): void send_event(new SetupBuildingEvent($panel)); $this->theme->display_page($page, $panel); } elseif ($event->page_matches("setup/save", method: "POST", permission: Permissions::CHANGE_SETTING)) { - send_event(new ConfigSaveEvent($config, $event->POST)); - $config->save(); + send_event(new ConfigSaveEvent($config, ConfigSaveEvent::postToSettings($event->POST))); $page->flash("Config saved"); $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("setup")); @@ -386,32 +436,16 @@ public function onSetupBuilding(SetupBuildingEvent $event): void public function onConfigSave(ConfigSaveEvent $event): void { $config = $event->config; - foreach ($event->values as $_name => $junk) { - if (substr($_name, 0, 6) == "_type_") { - $name = substr($_name, 6); - $type = $event->values["_type_$name"]; - $value = isset($event->values["_config_$name"]) ? $event->values["_config_$name"] : null; - switch ($type) { - case "string": - $config->set_string($name, $value); - break; - case "int": - $config->set_int($name, parse_shorthand_int((string)$value)); - break; - case "bool": - $config->set_bool($name, bool_escape($value)); - break; - case "array": - $config->set_array($name, $value); - break; - } - } + foreach ($event->values as $key => $value) { + match(true) { + is_null($value) => $config->delete($key), + is_string($value) => $config->set_string($key, $value), + is_int($value) => $config->set_int($key, $value), + is_bool($value) => $config->set_bool($key, $value), + is_array($value) => $config->set_array($key, $value), + }; } log_warning("setup", "Configuration updated"); - foreach (\Safe\glob("data/cache/*.css") as $css_cache) { - unlink($css_cache); - } - log_warning("setup", "Cache cleared"); } public function onCliGen(CliGenEvent $event): void diff --git a/ext/setup/script.js b/ext/setup/script.js index ee6d067ef..baea92ebc 100644 --- a/ext/setup/script.js +++ b/ext/setup/script.js @@ -5,25 +5,31 @@ document.addEventListener('DOMContentLoaded', () => { if(checkbox !== null && out_span !== null) { checkbox.disabled = true; out_span.innerHTML = '(testing...)'; + const test_url = document.body.getAttribute('data-base-href') + "/nicetest"; + console.log("NiceURL testing with", test_url); - fetch(document.body.getAttribute('data-base-href') + "/nicetest").then(response => { + fetch(test_url).then(response => { if(!response.ok) { checkbox.disabled = true; out_span.innerHTML = '(http error)'; + console.log("NiceURL test got HTTP error:", response.status, response.statusText); } else { response.text().then(text => { if(text === 'ok') { checkbox.disabled = false; out_span.innerHTML = '(test passed)'; + console.log("NiceURL test passed"); } else { checkbox.disabled = true; out_span.innerHTML = '(test failed)'; + console.log("NiceURL test got wrong content:", text); } }); } - }).catch(() => { + }).catch((e) => { checkbox.disabled = true; out_span.innerHTML = '(request failed)'; + console.log("NiceURL test hit an exception:", e); }); } }); diff --git a/ext/setup/test.php b/ext/setup/test.php index de4f36680..907d0c1b3 100644 --- a/ext/setup/test.php +++ b/ext/setup/test.php @@ -6,6 +6,45 @@ class SetupTest extends ShimmiePHPUnitTestCase { + public function testParseSettings(): void + { + $this->assertEquals( + [ + "mynull" => null, + "mystring" => "hello world!", + "myint" => 42 * 1024, + "mybool_true" => true, + "mybool_false" => false, + "myarray" => ["hello", "world"], + ], + ConfigSaveEvent::postToSettings([ + // keys in POST that don't start with _type or _config are ignored + "some_post" => "value", + // _type with no _config means the value is null + "_type_mynull" => "string", + // strings left as-is + "_type_mystring" => "string", + "_config_mystring" => "hello world!", + // ints parsed from human-readable form + "_type_myint" => "int", + "_config_myint" => "42KB", + // HTML booleans (HTML checkboxes are "on" or undefined, there is no "off") + "_type_mybool_true" => "bool", + "_config_mybool_true" => "on", + "_type_mybool_false" => "bool", + // Arrays are... passed as arrays? Does this work? + "_type_myarray" => "array", + "_config_myarray" => ["hello", "world"], + ]) + ); + + $this->assertException(InvalidInput::class, function () { + ConfigSaveEvent::postToSettings([ + "_type_myint" => "cake", + "_config_myint" => "tasty", + ]); + }); + } public function testNiceUrlsTest(): void { # XXX: this only checks that the text is "ok", to check diff --git a/ext/setup/theme.php b/ext/setup/theme.php index d2a5be1d0..a0915c40d 100644 --- a/ext/setup/theme.php +++ b/ext/setup/theme.php @@ -4,6 +4,10 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + +use function MicroHTML\rawHTML; + class SetupTheme extends Themelet { /* @@ -38,9 +42,8 @@ public function display_page(Page $page, SetupPanel $panel): void "; $page->set_title("Shimmie Setup"); - $page->set_heading("Shimmie Setup"); $page->add_block(new Block("Navigation", $this->build_navigation(), "left", 0)); - $page->add_block(new Block("Setup", $table)); + $page->add_block(new Block("Setup", rawHTML($table))); } /** @@ -79,30 +82,26 @@ public function display_advanced(Page $page, array $options): void "; $page->set_title("Shimmie Setup"); - $page->set_heading("Shimmie Setup"); $page->add_block(new Block("Navigation", $this->build_navigation(), "left", 0)); - $page->add_block(new Block("Setup", $table)); + $page->add_block(new Block("Setup", rawHTML($table))); } - protected function build_navigation(): string + protected function build_navigation(): HTMLElement { - return " + return rawHTML(" Index
                  Help
                  Advanced - "; + "); } protected function sb_to_html(SetupBlock $block): string { - $h = $block->header; - $b = $block->body; - $html = " + return "
                  - $h -
                  $b + {$block->header} +
                  {$block->str_body}
                  "; - return $html; } } diff --git a/ext/site_description/info.php b/ext/site_description/info.php index 5ffab210b..f83c09139 100644 --- a/ext/site_description/info.php +++ b/ext/site_description/info.php @@ -17,5 +17,5 @@ class SiteDescriptionInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::INTEGRATION; public string $description = "A description for search engines"; public ?string $documentation = -"This extension sets the \"description\" meta tag in the header of pages so that search engines can pick it up"; + "This extension sets the \"description\" meta tag in the header of pages so that search engines can pick it up"; } diff --git a/ext/site_description/main.php b/ext/site_description/main.php index fe2961cc8..55b14a9ac 100644 --- a/ext/site_description/main.php +++ b/ext/site_description/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\{META}; + class SiteDescription extends Extension { public function onPageRequest(PageRequestEvent $event): void @@ -11,11 +13,17 @@ public function onPageRequest(PageRequestEvent $event): void global $config, $page; if (!empty($config->get_string("site_description"))) { $description = $config->get_string("site_description"); - $page->add_html_header(""); + $page->add_html_header(META([ + 'name' => 'description', + 'content' => $description + ])); } if (!empty($config->get_string("site_keywords"))) { $keywords = $config->get_string("site_keywords"); - $page->add_html_header(""); + $page->add_html_header(META([ + 'name' => 'keywords', + 'content' => $keywords + ])); } } diff --git a/ext/site_description/test.php b/ext/site_description/test.php index b2af0dbfa..c28359aae 100644 --- a/ext/site_description/test.php +++ b/ext/site_description/test.php @@ -12,8 +12,8 @@ public function testSiteDescription(): void $config->set_string("site_description", "A Shimmie testbed"); $this->get_page("post/list"); $this->assertStringContainsString( - '', - $page->get_all_html_headers() + "", + (string)$page->get_all_html_headers() ); } @@ -23,8 +23,8 @@ public function testSiteKeywords(): void $config->set_string("site_keywords", "foo,bar,baz"); $this->get_page("post/list"); $this->assertStringContainsString( - '', - $page->get_all_html_headers() + "", + (string)$page->get_all_html_headers() ); } } diff --git a/ext/sitemap/main.php b/ext/sitemap/main.php index 5310d43a7..4e3a7ecef 100644 --- a/ext/sitemap/main.php +++ b/ext/sitemap/main.php @@ -62,7 +62,7 @@ private function handle_full_sitemap(): string } /* --- Add latest images to sitemap with higher priority --- */ - foreach(Search::find_images(limit: 50) as $image) { + foreach (Search::find_images(limit: 50) as $image) { $urls[] = new XMLSitemapURL( "post/view/$image->id", "weekly", @@ -82,7 +82,7 @@ private function handle_full_sitemap(): string } /* --- Add all other images to sitemap with lower priority --- */ - foreach(Search::find_images(offset: 51, limit: 10000) as $image) { + foreach (Search::find_images(offset: 51, limit: 10000) as $image) { $urls[] = new XMLSitemapURL( "post/view/$image->id", "monthly", @@ -102,7 +102,7 @@ private function generate_sitemap(array $urls): string { $xml = "<" . "?xml version=\"1.0\" encoding=\"utf-8\"?" . ">\n" . "\n"; - foreach($urls as $url) { + foreach ($urls as $url) { $link = make_http(make_link($url->url)); $xml .= " diff --git a/ext/source_history/main.php b/ext/source_history/main.php index 065b67582..921ee95ee 100644 --- a/ext/source_history/main.php +++ b/ext/source_history/main.php @@ -267,13 +267,8 @@ public function process_revert_all_changes(?string $name, ?string $ip, ?string $ if (!is_null($name)) { $duser = User::by_name($name); - if (is_null($duser)) { - $this->theme->add_status($name, "user not found"); - return; - } else { - $select_code[] = 'user_id = :user_id'; - $select_args['user_id'] = $duser->id; - } + $select_code[] = 'user_id = :user_id'; + $select_args['user_id'] = $duser->id; } if (!is_null($ip)) { @@ -310,7 +305,7 @@ public function process_revert_all_changes(?string $name, ?string $ip, ?string $ FROM source_histories WHERE image_id='.$image_id.' AND NOT ('.implode(" AND ", $select_code).') - ORDER BY date_set DESC LIMIT 1 + ORDER BY date_set DESC, id DESC LIMIT 1 ', $select_args); if (!empty($row)) { diff --git a/ext/source_history/theme.php b/ext/source_history/theme.php index 5523c9f74..87ad6a0af 100644 --- a/ext/source_history/theme.php +++ b/ext/source_history/theme.php @@ -28,7 +28,7 @@ public function display_history_page(Page $page, int $image_id, array $history): $page->set_title('Post '.$image_id.' Source History'); $page->set_heading('Source History: '.$image_id); $page->add_block(new NavBlock()); - $page->add_block(new Block("Source History", $history_html, "main", 10)); + $page->add_block(new Block("Source History", rawHTML($history_html), "main", 10)); } /** @@ -39,8 +39,7 @@ public function display_global_page(Page $page, array $history, int $page_number $history_html = $this->history_list($history, false); $page->set_title("Global Source History"); - $page->set_heading("Global Source History"); - $page->add_block(new Block("Source History", $history_html, "main", 10)); + $page->add_block(new Block("Source History", rawHTML($history_html), "main", 10)); $h_prev = ($page_number <= 1) ? "Prev" : 'Prev'; @@ -48,7 +47,7 @@ public function display_global_page(Page $page, array $history, int $page_number $h_next = 'Next'; $nav = $h_prev.' | '.$h_index.' | '.$h_next; - $page->add_block(new Block("Navigation", $nav, "left")); + $page->add_block(new Block("Navigation", rawHTML($nav), "left")); } /** @@ -75,7 +74,7 @@ public function display_admin_block(string $validation_msg = ''): void "; - $page->add_block(new Block("Mass Source Revert", $html)); + $page->add_block(new Block("Mass Source Revert", rawHTML($html))); } /* @@ -85,7 +84,7 @@ public function display_revert_ip_results(): void { global $page; $html = implode("\n", $this->messages); - $page->add_block(new Block("Bulk Revert Results", $html)); + $page->add_block(new Block("Bulk Revert Results", rawHTML($html))); } public function add_status(string $title, string $body): void diff --git a/ext/speed_hax/info.php b/ext/speed_hax/info.php new file mode 100644 index 000000000..1297a88a4 --- /dev/null +++ b/ext/speed_hax/info.php @@ -0,0 +1,33 @@ + self::SHISH_EMAIL, "jgen" => "jgen.tech@gmail.com", "Matthew Barbour" => "matthew@darkholme.net", "Discomrade" => ""]; + public string $license = self::LICENSE_GPLV2; + public ExtensionCategory $category = ExtensionCategory::ADMIN; + public string $description = "Show performance tweak options. Read the documentation."; + public ?string $documentation = + "Many of these changes reduce the correctness of the software and increase admin workload for the sake of speed. You almost certainly don't want to set some of them, but if you do (e.g. you're trying to run a site with 10,000 concurrent users on a single server), it can be a huge help. +

                  +
                    +
                  • Don't auto-upgrade database: Database schema upgrades are no longer automatic; you'll need to run php index.php db-upgrade from the CLI each time you update the code.
                  • +
                  • Cache event listeners: Mapping from Events to Extensions is cached - you'll need to delete data/cache/shm_event_listeners.php after each code change, and after enabling or disabling any extensions.
                  • +
                  • Purge cookie on logout: Clears the user cookie when a user logs out, to keep as few versions of content as possible.
                  • +
                  • List only recent comments: Only comments from the past 24 hours show up in /comment/list.
                  • +
                  • Fast page limit: We only show the first 500 pages of results for any query, except for the most simple (no tags, or one positive tag). Web crawlers are blocked from creating too many nonsense searches by limiting to 10 pages.
                  • +
                  • Anonymous search tag limit: Anonymous users can only search for this many tags at once. To disable, set to 0.
                  • +
                  • Limit complex searches: Only ever show the first 5,000 results for complex queries.
                  • +
                  • Fast page limit: We only show the first 500 pages of results for any query, except for the most simple (no tags, or one positive tag) or users with the BIG_SEARCH permission. Web crawlers are blocked from creating too many nonsense searches by limiting to 10 pages. Consider enabling Extra caching on first pages as well.
                  • +
                  • Extra caching on first pages: The first 10 pages in the post/list index get extra caching.
                  • +
                  • Limit images RSS: RSS is limited to 10 pages for the image list.
                  • +
                  +"; +} diff --git a/ext/speed_hax/main.php b/ext/speed_hax/main.php new file mode 100644 index 000000000..9f81a37a1 --- /dev/null +++ b/ext/speed_hax/main.php @@ -0,0 +1,55 @@ +set_default_bool(SpeedHaxConfig::NO_AUTO_DB_UPGRADE, false); + $config->set_default_bool(SpeedHaxConfig::CACHE_EVENT_LISTENERS, false); + $config->set_default_bool(SpeedHaxConfig::CACHE_TAG_LISTS, false); + $config->set_default_bool(SpeedHaxConfig::PURGE_COOKIE, false); + $config->set_default_bool(SpeedHaxConfig::RECENT_COMMENTS, false); + $config->set_default_int(SpeedHaxConfig::BIG_SEARCH, 0); + $config->set_default_bool(SpeedHaxConfig::LIMIT_COMPLEX, false); + $config->set_default_bool(SpeedHaxConfig::FAST_PAGE_LIMIT, false); + $config->set_default_bool(SpeedHaxConfig::CACHE_FIRST_FEW, false); + $config->set_default_bool(SpeedHaxConfig::RSS_LIMIT, false); + } + + public function onSetupBuilding(SetupBuildingEvent $event): void + { + $sb = $event->panel->create_new_block("Speed Hax"); + $sb->start_table(); + $sb->add_bool_option(SpeedHaxConfig::NO_AUTO_DB_UPGRADE, "Don't auto-upgrade database: ", false); + $sb->add_bool_option(SpeedHaxConfig::CACHE_EVENT_LISTENERS, "
                  Cache event listeners: ", false); + $sb->add_bool_option(SpeedHaxConfig::CACHE_TAG_LISTS, "
                  Cache tag lists: ", false); + $sb->add_bool_option(SpeedHaxConfig::PURGE_COOKIE, "
                  Purge cookie on logout: ", false); + $sb->add_bool_option(SpeedHaxConfig::RECENT_COMMENTS, "
                  List only recent comments: ", false); + $sb->add_int_option(SpeedHaxConfig::BIG_SEARCH, "
                  Anonymous search tag limit: ", false); + $sb->add_bool_option(SpeedHaxConfig::LIMIT_COMPLEX, "
                  Limit complex searches: ", false); + $sb->add_bool_option(SpeedHaxConfig::FAST_PAGE_LIMIT, "
                  Fast page limit: ", false); + $sb->add_bool_option(SpeedHaxConfig::CACHE_FIRST_FEW, "
                  Extra caching on first pages: ", false); + $sb->add_bool_option(SpeedHaxConfig::RSS_LIMIT, "
                  Limit images RSS: ", false); + $sb->end_table(); + } +} diff --git a/ext/static_files/init.js b/ext/static_files/init.js index fe6cabf29..0a93530cd 100644 --- a/ext/static_files/init.js +++ b/ext/static_files/init.js @@ -4,6 +4,13 @@ function shm_cookie_set(name, value) { function shm_cookie_get(name) { return Cookies.get(name); } +function shm_make_link(page, query) { + let base = (document.body.getAttribute("data-base-link") ?? ""); + let joiner = base.indexOf("?") === -1 ? "?" : "&"; + let url = base + page; + if(query) url += joiner + new URLSearchParams(query).toString(); + return url; +} function shm_log(section, ...message) { window.dispatchEvent(new CustomEvent("shm_log", {detail: {section, message}})); diff --git a/ext/static_files/main.php b/ext/static_files/main.php index 0f05d73f0..a13004ba5 100644 --- a/ext/static_files/main.php +++ b/ext/static_files/main.php @@ -36,7 +36,7 @@ public function onPageRequest(PageRequestEvent $event): void // hax. if ($page->mode == PageMode::PAGE && (!isset($page->blocks) || $this->count_main($page->blocks) == 0)) { $h_pagename = html_escape(implode('/', $event->args)); - $f_pagename = preg_replace("/[^a-z_\-\.]+/", "_", $h_pagename); + $f_pagename = preg_replace_ex("/[^a-z_\-\.]+/", "_", $h_pagename); $theme_name = $config->get_string(SetupConfig::THEME, "default"); $theme_file = "themes/$theme_name/static/$f_pagename"; diff --git a/ext/statistics/info.php b/ext/statistics/info.php index 98d74ae8d..dfa999704 100644 --- a/ext/statistics/info.php +++ b/ext/statistics/info.php @@ -15,7 +15,7 @@ class StatisticsInfo extends ExtensionInfo public ExtensionVisibility $visibility = ExtensionVisibility::ADMIN; public string $description = "Displays a user statistics page, similar to booru.org. Read the documentation before enabling."; public ?string $documentation = -"This will display certain user statistics, depending on which extensions are enabled. The taggers statistic relies on the Tag History extension, so it will only count from when that extension was enabled.\n + "This will display certain user statistics, depending on which extensions are enabled. The taggers statistic relies on the Tag History extension, so it will only count from when that extension was enabled.\n Assuming the extension is enabled, statistics are shown for user uploads, comments, tags (requires Tag History), notes, sources (requires Source History), favorites and forum posts.\n Tags statistics count both removing and adding tags, so changing 'tag_me' to 'tagme' counts as both a deletion and an addition, 2 tag edits. This is different to how booru.org calculates their tag statistics (which seems to only count the number of changes submitted)."; } diff --git a/ext/statistics/main.php b/ext/statistics/main.php index 901a354b5..b0977cf00 100644 --- a/ext/statistics/main.php +++ b/ext/statistics/main.php @@ -8,6 +8,8 @@ class Statistics extends Extension { /** @var StatisticsTheme */ protected Themelet $theme; + /** @var String[] */ + private array $unlisted = ['anonymous', 'ghost', 'hellbanned']; public function onPageRequest(PageRequestEvent $event): void { @@ -16,7 +18,7 @@ public function onPageRequest(PageRequestEvent $event): void $base_href = get_base_href(); $sitename = $config->get_string(SetupConfig::TITLE); $theme_name = $config->get_string(SetupConfig::THEME); - $anon_id = $config->get_int("anon_id"); + $unlisted = "'".implode("','", $this->unlisted)."'"; $limit = 10; if ($event->page_matches("stats/100")) { @@ -24,28 +26,36 @@ public function onPageRequest(PageRequestEvent $event): void } if (Extension::is_enabled(TagHistoryInfo::KEY)) { - $tag_tally = $this->get_tag_stats($anon_id); - arsort($tag_tally, $flags = SORT_NUMERIC); - $tag_table = $this->theme->build_table($tag_tally, "Taggers", "Top $limit taggers", $limit); + $tallies = $this->get_tag_stats($this->unlisted); + arsort($tallies[0], SORT_NUMERIC); + $stats = []; + foreach ($tallies[0] as $name => $tag_diff) { + $entries = ""; + if (isset($tallies[1][$name])) { + $entries = " " . $tallies[1][$name] . ""; + } + $stats[$name] = "$tag_diff$entries"; + } + $tag_table = $this->theme->build_table($stats, "Taggers", "Top $limit taggers", $limit); } else { $tag_table = null; } $upload_tally = []; - foreach ($this->get_upload_stats($anon_id) as $name) { + foreach ($this->get_upload_stats($unlisted) as $name) { array_key_exists($name, $upload_tally) ? $upload_tally[$name] += 1 : $upload_tally[$name] = 1; } - arsort($upload_tally, $flags = SORT_NUMERIC); + arsort($upload_tally, SORT_NUMERIC); $upload_table = $this->theme->build_table($upload_tally, "Uploaders", "Top $limit uploaders", $limit); if (Extension::is_enabled(CommentListInfo::KEY)) { $comment_tally = []; - foreach ($this->get_comment_stats($anon_id) as $name) { + foreach ($this->get_comment_stats($unlisted) as $name) { array_key_exists($name, $comment_tally) ? $comment_tally[$name] += 1 : $comment_tally[$name] = 1; } - arsort($comment_tally, $flags = SORT_NUMERIC); + arsort($comment_tally, SORT_NUMERIC); $comment_table = $this->theme->build_table($comment_tally, "Commenters", "Top $limit commenters", $limit); } else { $comment_table = null; @@ -53,11 +63,11 @@ public function onPageRequest(PageRequestEvent $event): void if (Extension::is_enabled(FavoritesInfo::KEY)) { $favorite_tally = []; - foreach ($this->get_favorite_stats($anon_id) as $name) { + foreach ($this->get_favorite_stats($unlisted) as $name) { array_key_exists($name, $favorite_tally) ? $favorite_tally[$name] += 1 : $favorite_tally[$name] = 1; } - arsort($favorite_tally, $flags = SORT_NUMERIC); + arsort($favorite_tally, SORT_NUMERIC); $favorite_table = $this->theme->build_table($favorite_tally, "Favoriters", "Top $limit favoriters", $limit); } else { $favorite_table = null; @@ -86,63 +96,91 @@ public function onPageSubNavBuilding(PageSubNavBuildingEvent $event): void } /** - * @return array + * @param String[] $unlisted + * @return array> */ - private function get_tag_stats(int $anon_id): array + private function get_tag_stats(array $unlisted): array { global $database; - // Returns the username and tags from each tag history entry. Excludes Anonymous - $tag_stats = $database->get_all("SELECT users.name,tag_histories.tags,tag_histories.image_id FROM tag_histories INNER JOIN users ON users.id = tag_histories.user_id WHERE tag_histories.user_id <> $anon_id;"); + // Returns the username and tags from each tag history entry. This includes Anonymous tag histories to prevent their tagging being ignored and credited to the next user to edit. + $tag_stats = $database->get_all("SELECT users.class,users.name,tag_histories.tags,tag_histories.image_id FROM tag_histories INNER JOIN users ON users.id = tag_histories.user_id WHERE 1=1 ORDER BY tag_histories.id;"); // Group tag history entries by image id $tag_histories = []; foreach ($tag_stats as $ts) { - $tag_history = ['name' => $ts['name'], 'tags' => $ts['tags']]; + $tag_history = ['class' => $ts['class'], 'name' => $ts['name'], 'tags' => $ts['tags']]; $id = $ts['image_id']; array_key_exists($id, $tag_histories) ? array_push($tag_histories[$id], $tag_history) : $tag_histories[$id] = [$tag_history]; } + + // Grab alias list so we can ignore those changes + // While this strategy may discount some change made before those aliases were implemented, it is preferable over crediting the changes made by an alias to whoever edits the tags next. + $alias_db = $database->get_all( + " + SELECT * + FROM aliases + WHERE 1=1 + " + ); + $aliases = []; + foreach ($alias_db as $alias) { + $aliases[$alias['oldtag']] = $alias['newtag']; + } + // Count changes made in each tag history and tally tags for users $tag_tally = []; + $change_tally = []; foreach ($tag_histories as $i => $image) { - $first = true; $prev = []; foreach ($image as $change) { $curr = explode(' ', $change['tags']); - $name = (string)$change['name']; - $tag_tally[$name] += count(array_diff($curr, $prev)); + foreach ($curr as $i => $tag) { + if (array_key_exists($tag, $aliases)) { + $curr[$i] = $aliases[$tag]; + } + } + if (!in_array($change['class'], $unlisted)) { + $name = (string)$change['name']; + if (!isset($tag_tally[$name])) { + $tag_tally[$name] = 0; + $change_tally[$name] = 0; + } + $tag_tally[$name] += count(array_diff($curr, $prev)); + $change_tally[$name] += 1; + } $prev = $curr; } } - return $tag_tally; + return [$tag_tally, $change_tally]; } /** * @return array */ - private function get_upload_stats(int $anon_id): array + private function get_upload_stats(string $unlisted): array { global $database; - // Returns the username of each post, as an array. Excludes Anonymous - return $database->get_col("SELECT users.name FROM images INNER JOIN users ON users.id = images.owner_id WHERE images.owner_id <> $anon_id;"); + // Returns the username of each post, as an array. + return $database->get_col("SELECT users.name FROM images INNER JOIN users ON users.id = images.owner_id WHERE users.class NOT IN ($unlisted) ORDER BY users.id;"); } /** * @return array */ - private function get_comment_stats(int $anon_id): array + private function get_comment_stats(string $unlisted): array { global $database; - // Returns the username of each comment, as an array. Excludes Anonymous - return $database->get_col("SELECT users.name FROM comments INNER JOIN users ON users.id = comments.owner_id WHERE comments.owner_id <> $anon_id;"); + // Returns the username of each comment, as an array. + return $database->get_col("SELECT users.name FROM comments INNER JOIN users ON users.id = comments.owner_id WHERE users.class NOT IN ($unlisted) ORDER BY users.id;"); } /** * @return array */ - private function get_favorite_stats(int $anon_id): array + private function get_favorite_stats(string $unlisted): array { global $database; - // Returns the username of each favorite, as an array. Excludes Anonymous - return $database->get_col("SELECT users.name FROM user_favorites INNER JOIN users ON users.id = user_favorites.user_id WHERE user_favorites.user_id <> $anon_id;"); + // Returns the username of each favorite, as an array. + return $database->get_col("SELECT users.name FROM user_favorites INNER JOIN users ON users.id = user_favorites.user_id WHERE users.class NOT IN ($unlisted) ORDER BY users.id;"); } } diff --git a/ext/statistics/theme.php b/ext/statistics/theme.php index 90e697a6b..f5ae97c0a 100644 --- a/ext/statistics/theme.php +++ b/ext/statistics/theme.php @@ -25,14 +25,13 @@ public function display_page(Page $page, int $limit, ?HTMLElement $tag_table, ?H $favorite_table, ); - $page->set_title(html_escape("Stats")); - $page->set_heading(html_escape("Stats - Top $limit")); + $page->set_title("Stats - Top $limit"); $page->add_block(new NavBlock()); $page->add_block(new Block("Stats", $html, "main", 20)); } /** - * @param array $data + * @param array $data */ public function build_table(array $data, string $id, string $title, ?int $limit = 10): HTMLElement { @@ -42,8 +41,8 @@ public function build_table(array $data, string $id, string $title, ?int $limit $rows->appendChild( TR( TD([], $n), - TD([], $value), - TD([], $user) + TD([], rawHTML((string)$value)), + TD([], rawHTML(''.$user.'')) ) ); $n++; diff --git a/ext/tag_categories/main.php b/ext/tag_categories/main.php index c5011b261..f718aeeb8 100644 --- a/ext/tag_categories/main.php +++ b/ext/tag_categories/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + require_once "config.php"; class TagCategories extends Extension @@ -111,10 +113,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Tag Categories"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Tag Categories", $this->theme->get_help_html()); } } diff --git a/ext/tag_categories/theme.php b/ext/tag_categories/theme.php index f3281a22f..7f76b5044 100644 --- a/ext/tag_categories/theme.php +++ b/ext/tag_categories/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + class TagCategoriesTheme extends Themelet { /** @@ -104,20 +106,19 @@ public function show_tag_categories(Page $page, array $tc_dict): void // add html to stuffs $page->set_title("Tag Categories"); - $page->set_heading("Tag Categories"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Editing", $html, "main", 10)); + $page->add_block(new Block("Editing", rawHTML($html), "main", 10)); } public function get_help_html(): string { return '

                  Search for posts containing a certain number of tags with the specified tag category.

                  -
                  persontags=1
                  + persontags=1

                  Returns posts with exactly 1 tag with the tag category "person".

                  -
                  cattags>0
                  + cattags>0

                  Returns posts with 1 or more tags with the tag category "cat".

                  Can use <, <=, >, >=, or =.

                  diff --git a/ext/tag_history/main.php b/ext/tag_history/main.php index 1af6c08d8..b63849208 100644 --- a/ext/tag_history/main.php +++ b/ext/tag_history/main.php @@ -301,6 +301,22 @@ public function get_global_tag_history(int $page_id): array ", ["offset" => ($page_id - 1) * 100]); } + /** + * @return array|null + */ + public function get_previous_tags(int $image_id, int $id): ?array + { + global $database; + $row = $database->get_row(" + SELECT tags + FROM tag_histories + WHERE image_id = :image_id AND id < :id + ORDER BY id DESC + LIMIT 1 + ", ["image_id" => $image_id, "id" => $id]); + return ($row ? $row : null); + } + /** * This function attempts to revert all changes by a given IP within an (optional) timeframe. */ @@ -313,13 +329,8 @@ public function process_revert_all_changes(?string $name, ?string $ip, ?string $ if (!is_null($name)) { $duser = User::by_name($name); - if (is_null($duser)) { - $this->theme->add_status($name, "user not found"); - return; - } else { - $select_code[] = 'user_id = :user_id'; - $select_args['user_id'] = $duser->id; - } + $select_code[] = 'user_id = :user_id'; + $select_args['user_id'] = $duser->id; } if (!is_null($ip)) { @@ -356,7 +367,7 @@ public function process_revert_all_changes(?string $name, ?string $ip, ?string $ FROM tag_histories WHERE image_id='.$image_id.' AND NOT ('.implode(" AND ", $select_code).') - ORDER BY date_set DESC LIMIT 1 + ORDER BY date_set DESC, id DESC LIMIT 1 ', $select_args); if (!empty($row)) { diff --git a/ext/tag_history/style.css b/ext/tag_history/style.css new file mode 100644 index 000000000..506c9d7d8 --- /dev/null +++ b/ext/tag_history/style.css @@ -0,0 +1,2 @@ +.added-tag{background:lightgreen;} +.deleted-tag{background:pink;text-decoration:line-through;} \ No newline at end of file diff --git a/ext/tag_history/theme.php b/ext/tag_history/theme.php index ed20cc9cf..b3f582528 100644 --- a/ext/tag_history/theme.php +++ b/ext/tag_history/theme.php @@ -29,7 +29,7 @@ public function display_history_page(Page $page, int $image_id, array $history): $page->set_title('Post '.$image_id.' Tag History'); $page->set_heading('Tag History: '.$image_id); $page->add_block(new NavBlock()); - $page->add_block(new Block("Tag History", $history_html, "main", 10)); + $page->add_block(new Block("Tag History", rawHTML($history_html), "main", 10)); } /** @@ -40,8 +40,7 @@ public function display_global_page(Page $page, array $history, int $page_number $history_html = $this->history_list($history, false); $page->set_title("Global Tag History"); - $page->set_heading("Global Tag History"); - $page->add_block(new Block("Tag History", $history_html, "main", 10)); + $page->add_block(new Block("Tag History", rawHTML($history_html), "main", 10)); $h_prev = ($page_number <= 1) ? "Prev" : 'Prev'; @@ -49,7 +48,7 @@ public function display_global_page(Page $page, array $history, int $page_number $h_next = 'Next'; $nav = $h_prev.' | '.$h_index.' | '.$h_next; - $page->add_block(new Block("Navigation", $nav, "left", 0)); + $page->add_block(new Block("Navigation", rawHTML($nav), "left", 0)); } /** @@ -76,7 +75,7 @@ public function display_admin_block(string $validation_msg = ''): void "; - $page->add_block(new Block("Mass Tag Revert", $html)); + $page->add_block(new Block("Mass Tag Revert", rawHTML($html))); } /* @@ -86,7 +85,7 @@ public function display_revert_ip_results(): void { global $page; $html = implode("\n", $this->messages); - $page->add_block(new Block("Bulk Revert Results", $html)); + $page->add_block(new Block("Bulk Revert Results", rawHTML($html))); } public function add_status(string $title, string $body): void @@ -132,10 +131,30 @@ protected function history_entry(array $fields, bool $selected): string : null; $setter = A(["href" => make_link("user/" . url_escape($name))], $name); - $current_tags = Tag::explode($current_tags); + $th = new TagHistory(); + $pt = $th->get_previous_tags($image_id, $current_id); + if ($pt) { + $previous_tags = explode(" ", $pt["tags"]); + } + $current_tags = explode(" ", $current_tags); + if ($pt) { + $tags = array_unique(array_merge($current_tags, $previous_tags)); + sort($tags); + } else { + $tags = $current_tags; + } $taglinks = SPAN(); - foreach ($current_tags as $tag) { - $taglinks->appendChild(A(["href" => search_link([$tag])], $tag)); + foreach ($tags as $tag) { + $class = ""; + if ($pt) { + if (!in_array($tag, $previous_tags)) { + $class = "added-tag"; + } + if (!in_array($tag, $current_tags)) { + $class = "deleted-tag"; + } + } + $taglinks->appendChild(A(["href" => search_link([$tag]), "class" => $class], $tag)); $taglinks->appendChild(" "); } diff --git a/ext/tag_list/main.php b/ext/tag_list/main.php index 5e6fba444..ecf82d426 100644 --- a/ext/tag_list/main.php +++ b/ext/tag_list/main.php @@ -32,7 +32,6 @@ public function onPageRequest(PageRequestEvent $event): void global $config, $page; if ($event->page_matches("tags/{sub}", method: "GET")) { - $this->theme->set_navigation($this->build_navigation()); $sub = $event->get_arg('sub'); if ($event->get_GET('starts_with')) { @@ -219,16 +218,6 @@ private function build_az(int $tags_min): string return $html; } - private function build_navigation(): string - { - $h_index = "Index"; - $h_map = "Map"; - $h_alphabetic = "Alphabetic"; - $h_popularity = "Popularity"; - $h_all = "Show All"; - return "$h_index
                   
                  $h_map
                  $h_alphabetic
                  $h_popularity
                   
                  $h_all"; - } - private function build_tag_map(string $starts_with, int $tags_min): string { global $config, $database; @@ -275,7 +264,7 @@ private function build_tag_map(string $starts_with, int $tags_min): string $html .= " $h_tag_no_underscores \n"; } - if (SPEED_HAX) { + if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::CACHE_TAG_LISTS)) { file_put_contents($cache_key, $html); } @@ -349,7 +338,7 @@ private function build_tag_alphabetic(string $starts_with, int $tags_min): strin $html .= "$h_tag\n"; } - if (SPEED_HAX) { + if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::CACHE_TAG_LISTS)) { file_put_contents($cache_key, $html); } @@ -358,7 +347,7 @@ private function build_tag_alphabetic(string $starts_with, int $tags_min): strin private function build_tag_popularity(int $tags_min): string { - global $database; + global $config, $database; // Make sure that the value of $tags_min is at least 1. // Otherwise the database will complain if you try to do: LOG(0) @@ -396,7 +385,7 @@ private function build_tag_popularity(int $tags_min): string $html .= "$h_tag ($count)\n"; } - if (SPEED_HAX) { + if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::CACHE_TAG_LISTS)) { file_put_contents($cache_key, $html); } diff --git a/ext/tag_list/theme.php b/ext/tag_list/theme.php index 3922ba8e8..ec9840b7e 100644 --- a/ext/tag_list/theme.php +++ b/ext/tag_list/theme.php @@ -4,11 +4,12 @@ namespace Shimmie2; +use function MicroHTML\{A, BR, rawHTML, emptyHTML}; + class TagListTheme extends Themelet { public string $heading = ""; public string $list = ""; - public ?string $navigation; private mixed $tagcategories = null; public function set_heading(string $text): void @@ -21,17 +22,29 @@ public function set_tag_list(string $list): void $this->list = $list; } - public function set_navigation(string $nav): void - { - $this->navigation = $nav; - } - public function display_page(Page $page): void { $page->set_title("Tag List"); $page->set_heading($this->heading); - $page->add_block(new Block("Tags", $this->list)); - $page->add_block(new Block("Navigation", $this->navigation, "left", 0)); + $page->add_block(new Block("Tags", rawHTML($this->list))); + + $nav = emptyHTML( + A(["href" => make_link()], "Index"), + BR(), + rawHTML(" "), + BR(), + A(["href" => make_link("tags/map")], "Map"), + BR(), + A(["href" => make_link("tags/alphabetic")], "Alphabetic"), + BR(), + A(["href" => make_link("tags/popularity")], "Popularity"), + BR(), + rawHTML(" "), + BR(), + A(["href" => modify_current_url(["mincount" => 1])], "Show All"), + ); + + $page->add_block(new Block("Navigation", $nav, "left", 0)); } // ======================================================================= @@ -113,11 +126,11 @@ public function display_split_related_block(Page $page, array $tag_infos): void } else { $category_display_name = html_escape($tag_category_dict[$category]['display_multiple']); } - $page->add_block(new Block($category_display_name, $tag_categories_html[$category], "left", 9)); + $page->add_block(new Block($category_display_name, rawHTML($tag_categories_html[$category]), "left", 9)); } if ($main_html !== null) { - $page->add_block(new Block("Tags", $main_html, "left", 10)); + $page->add_block(new Block("Tags", rawHTML($main_html), "left", 10)); } } @@ -162,7 +175,7 @@ public function display_related_block(Page $page, array $tag_infos, string $bloc $config->get_string(TagListConfig::RELATED_SORT) ); - $page->add_block(new Block($block_name, $main_html, "left", 10)); + $page->add_block(new Block($block_name, rawHTML($main_html), "left", 10)); } /** @@ -178,7 +191,7 @@ public function display_popular_block(Page $page, array $tag_infos): void ); $main_html .= " 
                  Full List\n"; - $page->add_block(new Block("Popular Tags", $main_html, "left", 60)); + $page->add_block(new Block("Popular Tags", rawHTML($main_html), "left", 60)); } /** @@ -195,7 +208,7 @@ public function display_refine_block(Page $page, array $tag_infos, array $search ); $main_html .= " 
                  Full List\n"; - $page->add_block(new Block("Refine Search", $main_html, "left", 60)); + $page->add_block(new Block("Refine Search", rawHTML($main_html), "left", 60)); } /** diff --git a/ext/tag_tools/main.php b/ext/tag_tools/main.php index 2b5673a39..7095b3227 100644 --- a/ext/tag_tools/main.php +++ b/ext/tag_tools/main.php @@ -18,7 +18,7 @@ public function onAdminBuilding(AdminBuildingEvent $event): void public function onAdminAction(AdminActionEvent $event): void { global $database; - switch($event->action) { + switch ($event->action) { case "set_tag_case": $database->execute( "UPDATE tags SET tag=:tag1 WHERE LOWER(tag) = LOWER(:tag2)", diff --git a/ext/tag_tools/theme.php b/ext/tag_tools/theme.php index 02186c62b..867c40961 100644 --- a/ext/tag_tools/theme.php +++ b/ext/tag_tools/theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -use function MicroHTML\INPUT; +use function MicroHTML\{INPUT,rawHTML}; class TagToolsTheme extends Themelet { @@ -35,13 +35,13 @@ public function display_form(): void $html = ""; $html .= $this->button("All tags to lowercase", "lowercase_all_tags", true); $html .= $this->button("Recount tag use", "recount_tag_use", false); - $page->add_block(new Block("Misc Admin Tools", $html)); + $page->add_block(new Block("Misc Admin Tools", rawHTML($html))); $html = (string)SHM_SIMPLE_FORM( "admin/set_tag_case", 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)); + $page->add_block(new Block("Set Tag Case", rawHTML($html))); } } diff --git a/ext/tagger_xml/main.php b/ext/tagger_xml/main.php index c9f39f246..312e7cf77 100644 --- a/ext/tagger_xml/main.php +++ b/ext/tagger_xml/main.php @@ -53,7 +53,7 @@ private function match_tag_list(string $s): string ]; // Match - $match = "concat(:p, tag) LIKE :sq"; + $match = "(:p || tag) LIKE :sq"; // Exclude // $exclude = $event->get_arg('exclude')? "AND NOT IN ".$this->image_tags($event->get_arg('exclude')) : null; diff --git a/ext/terms/info.php b/ext/terms/info.php new file mode 100644 index 000000000..466d25df7 --- /dev/null +++ b/ext/terms/info.php @@ -0,0 +1,16 @@ + ""]; + public string $license = "GPLv2"; + public string $description = "Show a page of terms which must be accepted before continuing"; +} diff --git a/ext/terms/main.php b/ext/terms/main.php new file mode 100644 index 000000000..7ce0959ad --- /dev/null +++ b/ext/terms/main.php @@ -0,0 +1,47 @@ +set_default_string("terms_message", "Cookies may be used. Please read our [url=site://wiki/privacy]privacy policy[/url] for more information.\nBy accepting to enter you agree to our [url=site://wiki/rules]rules[/url] and [url=site://wiki/terms_of_service]terms of service[/url]."); + } + + public function onSetupBuilding(SetupBuildingEvent $event): void + { + $sb = $event->panel->create_new_block("Terms & Conditions Gate"); + $sb->add_longtext_option("terms_message", 'Message (Use BBCode)'); + } + + public function onPageRequest(PageRequestEvent $event): void + { + global $config, $page, $user; + if ($event->page_starts_with("accept_terms")) { + $page->add_cookie("accepted_terms", "true", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link(explode('/', $event->path, 2)[1])); + } else { + // run on all pages unless any of: + // - user is logged in + // - cookie exists + // - user is viewing the wiki (because that's where the privacy policy / TOS / etc are) + if ( + $user->is_anonymous() + && !$page->get_cookie('accepted_terms') + && !$event->page_starts_with("wiki") + ) { + $sitename = $config->get_string(SetupConfig::TITLE); + $body = format_text($config->get_string("terms_message")); + $this->theme->display_page($page, $sitename, $event->path, $body); + } + } + } +} diff --git a/ext/terms/style.css b/ext/terms/style.css new file mode 100644 index 000000000..f70f7f50e --- /dev/null +++ b/ext/terms/style.css @@ -0,0 +1,19 @@ +.terms-modal-enter { + margin: 10px; +} +#terms-modal { + margin: auto; + margin-top: 20vh; + padding: 5px 20px; + text-align: center; + width: fit-content; +} +#terms-modal-bg { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; + backdrop-filter: blur(10px); +} \ No newline at end of file diff --git a/ext/terms/test.php b/ext/terms/test.php new file mode 100644 index 000000000..fd0f01b3d --- /dev/null +++ b/ext/terms/test.php @@ -0,0 +1,44 @@ +request('GET', 'post/list', cookies: []); + $this->assert_text("terms-modal-enter"); + } + + public function testLoggedIn(): void + { + $this->log_in_as_user(); + $this->request('GET', 'post/list', cookies: []); + $this->assert_no_text("terms-modal-enter"); + } + + public function testCookie(): void + { + $this->request('GET', 'post/list', cookies: ['shm_accepted_terms' => 'true']); + $this->assert_no_text("terms-modal-enter"); + } + + public function testWiki(): void + { + $this->request('GET', 'wiki/rules'); + $this->assert_no_text("terms-modal-enter"); + } + + public function testAcceptTerms(): void + { + $page = $this->request('POST', 'accept_terms/post/list'); + $this->assertEquals($page->mode, PageMode::REDIRECT); + $this->assertEquals($page->redirect, make_link('post/list')); + + $page = $this->request('POST', 'accept_terms/'); + $this->assertEquals($page->mode, PageMode::REDIRECT); + $this->assertEquals($page->redirect, make_link('')); + } +} diff --git a/ext/terms/theme.php b/ext/terms/theme.php new file mode 100644 index 000000000..f67d90623 --- /dev/null +++ b/ext/terms/theme.php @@ -0,0 +1,25 @@ + + +

                  $sitename

                  + $body +
                  + +
                  +
                  + "; + $page->add_block(new Block(null, rawHTML($html), "main", 1)); + } +} diff --git a/ext/tips/theme.php b/ext/tips/theme.php index 01f106e98..64c81e78c 100644 --- a/ext/tips/theme.php +++ b/ext/tips/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + /** * @phpstan-type Tip array{id: int, image: string, text: string, enable: bool} */ @@ -46,9 +48,8 @@ public function manageTips(string $url, array $images): void "; $page->set_title("Tips List"); - $page->set_heading("Tips List"); $page->add_block(new NavBlock()); - $page->add_block(new Block("Add Tip", $html, "main", 10)); + $page->add_block(new Block("Add Tip", rawHTML($html), "main", 10)); } /** @@ -63,7 +64,7 @@ public function showTip(string $url, array $tip): void $img = " "; } $html = "
                  ".$img.html_escape($tip['text'])."
                  "; - $page->add_block(new Block(null, $html, "subheading", 10)); + $page->add_block(new Block(null, rawHTML($html), "subheading", 10)); } /** @@ -110,6 +111,6 @@ public function showAll(string $url, array $tips): void } $html .= ""; - $page->add_block(new Block("All Tips", $html, "main", 20)); + $page->add_block(new Block("All Tips", rawHTML($html), "main", 20)); } } diff --git a/ext/transcode/info.php b/ext/transcode/info.php index 085687789..b3c456524 100644 --- a/ext/transcode/info.php +++ b/ext/transcode/info.php @@ -15,7 +15,7 @@ class TranscodeImageInfo extends ExtensionInfo public ExtensionCategory $category = ExtensionCategory::FILE_HANDLING; public string $description = "Allows admins to automatically and manually transcode images."; public ?string $documentation = -"Can transcode on-demand and automatically on upload. Config screen allows choosing an output format for each of the supported input formats. + "Can transcode on-demand and automatically on upload. Config screen allows choosing an output format for each of the supported input formats. Supports GD and ImageMagick. Both support bmp, gif, jpg, png, and webp as inputs, and jpg, png, and lossy webp as outputs. ImageMagick additionally supports tiff and psd inputs, and webp lossless output. If and image is unable to be transcoded for any reason, the upload will continue unaffected."; diff --git a/ext/transcode/main.php b/ext/transcode/main.php index 068aa9838..c90d85378 100644 --- a/ext/transcode/main.php +++ b/ext/transcode/main.php @@ -210,13 +210,9 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("transcode/{image_id}", method: "POST", permission: Permissions::EDIT_FILES)) { $image_id = $event->get_iarg('image_id'); $image_obj = Image::by_id_ex($image_id); - try { - $this->transcode_and_replace_image($image_obj, $event->req_POST('transcode_mime')); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/".$image_id)); - } catch (ImageTranscodeException $e) { - $this->theme->display_transcode_error($page, "Error Transcoding", $e->getMessage()); - } + $this->transcode_and_replace_image($image_obj, $event->req_POST('transcode_mime')); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$image_id)); } } @@ -260,9 +256,8 @@ public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event): { global $user, $config; - $engine = $config->get_string(TranscodeConfig::ENGINE); - if ($user->can(Permissions::EDIT_FILES)) { + $engine = $config->get_string(TranscodeConfig::ENGINE); $event->add_action(self::ACTION_BULK_TRANSCODE, "Transcode Image", null, "", $this->theme->get_transcode_picker_html($this->get_supported_output_mimes($engine))); } } diff --git a/ext/transcode/theme.php b/ext/transcode/theme.php index cf9c09ceb..5ceedde73 100644 --- a/ext/transcode/theme.php +++ b/ext/transcode/theme.php @@ -39,12 +39,4 @@ public function get_transcode_picker_html(array $options): string } return $html.""; } - - public function display_transcode_error(Page $page, string $title, string $message): void - { - $page->set_title("Transcode Image"); - $page->set_heading("Transcode Image"); - $page->add_block(new NavBlock()); - $page->add_block(new Block($title, $message)); - } } diff --git a/ext/transcode_video/main.php b/ext/transcode_video/main.php index 203557779..43b6ac4a2 100644 --- a/ext/transcode_video/main.php +++ b/ext/transcode_video/main.php @@ -104,13 +104,9 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("transcode_video/{image_id}", method: "POST", permission: Permissions::EDIT_FILES)) { $image_id = $event->get_iarg('image_id'); $image_obj = Image::by_id_ex($image_id); - try { - $this->transcode_and_replace_video($image_obj, $event->req_POST('transcode_format')); - $page->set_mode(PageMode::REDIRECT); - $page->set_redirect(make_link("post/view/".$image_id)); - } catch (VideoTranscodeException $e) { - $this->theme->display_transcode_error($page, "Error Transcoding", $e->getMessage()); - } + $this->transcode_and_replace_video($image_obj, $event->req_POST('transcode_format')); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/".$image_id)); } } diff --git a/ext/transcode_video/theme.php b/ext/transcode_video/theme.php index eb3a6b0cb..088544c53 100644 --- a/ext/transcode_video/theme.php +++ b/ext/transcode_video/theme.php @@ -39,12 +39,4 @@ public function get_transcode_picker_html(array $options): string } return $html.""; } - - public function display_transcode_error(Page $page, string $title, string $message): void - { - $page->set_title("Transcode Video"); - $page->set_heading("Transcode Video"); - $page->add_block(new NavBlock()); - $page->add_block(new Block($title, $message)); - } } diff --git a/ext/trash/main.php b/ext/trash/main.php index 41122316e..ca96c2f0e 100644 --- a/ext/trash/main.php +++ b/ext/trash/main.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use function MicroHTML\rawHTML; + abstract class TrashConfig { public const VERSION = "ext_trash_version"; @@ -119,10 +121,7 @@ public function onHelpPageBuilding(HelpPageBuildingEvent $event): void global $user; if ($event->key === HelpPages::SEARCH) { if ($user->can(Permissions::VIEW_TRASH)) { - $block = new Block(); - $block->header = "Trash"; - $block->body = $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Trash", $this->theme->get_help_html()); } } } diff --git a/ext/trash/theme.php b/ext/trash/theme.php index 2a5f9600d..01679b927 100644 --- a/ext/trash/theme.php +++ b/ext/trash/theme.php @@ -10,7 +10,7 @@ public function get_help_html(): string { return '

                  Search for posts in the trash.

                  -
                  in:trash
                  + in:trash

                  Returns posts that are in the trash.

                  '; diff --git a/ext/upload/main.php b/ext/upload/main.php index a7e2219c8..be3d11aef 100644 --- a/ext/upload/main.php +++ b/ext/upload/main.php @@ -219,15 +219,13 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("upload", method: "GET", permission: Permissions::CREATE_IMAGE)) { if ($this->is_full) { - $this->theme->display_error(507, "Error", "Can't upload images: disk nearly full"); - return; + throw new ServerError("Can't upload images: disk nearly full"); } $this->theme->display_page($page); } if ($event->page_matches("upload", method: "POST", permission: Permissions::CREATE_IMAGE)) { if ($this->is_full) { - $this->theme->display_error(507, "Error", "Can't upload images: disk nearly full"); - return; + throw new ServerError("Can't upload images: disk nearly full"); } $results = []; @@ -319,7 +317,7 @@ private function try_upload(array $file, int $slot, array $metadata): array } return $event->images; }); - foreach($new_images as $image) { + foreach ($new_images as $image) { $results[] = new UploadSuccess($name, $image->id); } } catch (UploadException $ex) { @@ -351,7 +349,7 @@ private function try_transload(string $url, int $slot, array $metadata): array // Parse metadata $s_filename = find_header($headers, 'Content-Disposition'); - $h_filename = ($s_filename ? preg_replace('/^.*filename="([^ ]+)"/i', '$1', $s_filename) : null); + $h_filename = ($s_filename ? preg_replace_ex('/^.*filename="([^ ]+)"/i', '$1', $s_filename) : null); $filename = $h_filename ?: basename($url); $new_images = $database->with_savepoint(function () use ($tmp_filename, $filename, $slot, $metadata) { @@ -361,7 +359,7 @@ private function try_transload(string $url, int $slot, array $metadata): array } return $event->images; }); - foreach($new_images as $image) { + foreach ($new_images as $image) { $results[] = new UploadSuccess($url, $image->id); } } catch (UploadException $ex) { diff --git a/ext/upload/theme.php b/ext/upload/theme.php index 0773a0986..435a43b8c 100644 --- a/ext/upload/theme.php +++ b/ext/upload/theme.php @@ -20,21 +20,20 @@ use function MicroHTML\BR; use function MicroHTML\A; use function MicroHTML\SPAN; - use function MicroHTML\P; class UploadTheme extends Themelet { public function display_block(Page $page): void { - $b = new Block("Upload", (string)$this->build_upload_block(), "left", 20); + $b = new Block("Upload", $this->build_upload_block(), "left", 20); $b->is_content = false; $page->add_block($b); } public function display_full(Page $page): void { - $page->add_block(new Block("Upload", "Disk nearly full, uploads disabled", "left", 20)); + $page->add_block(new Block("Upload", rawHTML("Disk nearly full, uploads disabled"), "left", 20)); } public function display_page(Page $page): void @@ -83,7 +82,6 @@ public function display_page(Page $page): void ); $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) { @@ -226,16 +224,14 @@ public function display_upload_status(Page $page, array $results): void if (count($errors) > 0) { $page->set_title("Upload Status"); - $page->set_heading("Upload Status"); $page->add_block(new NavBlock()); - foreach($errors as $error) { - $page->add_block(new Block($error->name, format_text($error->error))); + foreach ($errors as $error) { + $page->add_block(new Block($error->name, rawHTML(format_text($error->error)))); } } elseif (count($successes) == 0) { $page->set_title("No images uploaded"); - $page->set_heading("No images uploaded"); $page->add_block(new NavBlock()); - $page->add_block(new Block("No images uploaded", "Upload attempted, but nothing succeeded and nothing failed?")); + $page->add_block(new Block("No images uploaded", rawHTML("Upload attempted, but nothing succeeded and nothing failed?"))); } elseif (count($successes) == 1) { $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("post/view/{$successes[0]->image_id}")); diff --git a/ext/user/events.php b/ext/user/events.php index a96ed0331..82042efe9 100644 --- a/ext/user/events.php +++ b/ext/user/events.php @@ -24,7 +24,7 @@ class UserOperationsBuildingEvent extends PartListBuildingEvent { public function __construct( public User $user, - public BaseConfig $user_config, + public Config $user_config, ) { parent::__construct(); } @@ -44,6 +44,8 @@ public function __construct( class UserCreationEvent extends Event { + private ?User $user; + public function __construct( public string $username, public string $password, @@ -53,6 +55,19 @@ public function __construct( ) { parent::__construct(); } + + public function set_user(User $user): void + { + $this->user = $user; + } + + public function get_user(): User + { + if (is_null($this->user)) { + throw new \Exception("User not created"); + } + return $this->user; + } } class UserLoginEvent extends Event diff --git a/ext/user/main.php b/ext/user/main.php index 8bec2cf16..15f26d72e 100644 --- a/ext/user/main.php +++ b/ext/user/main.php @@ -9,7 +9,6 @@ use GQLA\Field; use GQLA\Type; use GQLA\Mutation; - use MicroHTML\HTMLElement; use MicroCRUD\ActionColumn; use MicroCRUD\EnumColumn; @@ -18,7 +17,7 @@ use MicroCRUD\DateColumn; use MicroCRUD\Table; -use function MicroHTML\A; +use function MicroHTML\{A, STYLE, emptyHTML}; class UserNameColumn extends TextColumn { @@ -91,17 +90,16 @@ public function __construct( public static function login(string $username, string $password): LoginResult { global $config; - $duser = User::by_name_and_pass($username, $password); - if (!is_null($duser)) { + try { + $duser = User::by_name_and_pass($username, $password); return new LoginResult( $duser, - UserPage::get_session_id($duser->name), + $duser->get_session_id(), null ); - } else { - $anon = User::by_id($config->get_int("anon_id", 0)); + } catch (UserNotFound $ex) { return new LoginResult( - $anon, + User::by_id($config->get_int("anon_id", 0)), null, "No user found" ); @@ -115,8 +113,8 @@ public static function create_user(string $username, string $password1, string $ try { $uce = send_event(new UserCreationEvent($username, $password1, $password2, $email, true)); return new LoginResult( - User::by_name($username), - UserPage::get_session_id($username), + $uce->get_user(), + $uce->get_user()->get_session_id(), null ); } catch (UserCreationException $ex) { @@ -160,9 +158,9 @@ public function onPageRequest(PageRequestEvent $event): void $this->show_user_info(); if ($user->can(Permissions::VIEW_HELLBANNED)) { - $page->add_html_header(""); + $page->add_html_header(STYLE("DIV.hb, TR.hb TD {border: 1px solid red !important;}")); } elseif (!$user->can(Permissions::HELLBANNED)) { - $page->add_html_header(""); + $page->add_html_header(STYLE(".hb {display: none !important;}")); } if ($event->page_matches("user_admin/login", method: "GET")) { @@ -198,11 +196,11 @@ public function onPageRequest(PageRequestEvent $event): void true ) ); - $this->set_login_cookie($uce->username); + $uce->get_user()->set_login_cookie(); $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("user")); } catch (UserCreationException $ex) { - $this->theme->display_error(400, "User Creation Error", $ex->getMessage()); + throw new InvalidInput($ex->getMessage()); } } if ($event->page_matches("user_admin/create_other", method: "POST", permission: Permissions::CREATE_OTHER_USER)) { @@ -210,7 +208,7 @@ public function onPageRequest(PageRequestEvent $event): void new UserCreationEvent( $event->req_POST("name"), $event->req_POST("pass1"), - $event->req_POST("pass1"), + $event->req_POST("pass2"), $event->req_POST("email"), false ) @@ -228,7 +226,9 @@ public function onPageRequest(PageRequestEvent $event): void // $t->columns[] = $col; array_splice($t->columns, 2, 0, [$col]); } - $this->theme->display_crud("Users", $t->table($t->query()), $t->paginator()); + $page->set_title("Users"); + $page->add_block(new NavBlock()); + $page->add_block(new Block(null, emptyHTML($t->table($t->query()), $t->paginator()))); } if ($event->page_matches("user_admin/classes", method: "GET")) { $this->theme->display_user_classes( @@ -269,7 +269,7 @@ public function onPageRequest(PageRequestEvent $event): void // FIXME: send_event() $duser->set_password($input['pass1']); if ($duser->id == $user->id) { - $this->set_login_cookie($duser->name); + $duser->set_login_cookie(); } $page->flash("Password changed"); $this->redirect_to_user($duser); @@ -312,18 +312,12 @@ public function onPageRequest(PageRequestEvent $event): void if ($event->page_matches("user/{name}")) { $display_user = User::by_name($event->get_arg('name')); - if (!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..." - ); + if ($display_user->id == $config->get_int("anon_id")) { + throw new UserNotFound("No such user"); } - } elseif($event->page_matches("user")) { + $e = send_event(new UserPageBuildingEvent($display_user)); + $this->display_stats($e); + } elseif ($event->page_matches("user")) { $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("user/" . $user->name)); } @@ -333,27 +327,27 @@ public function onUserPageBuilding(UserPageBuildingEvent $event): void { 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; + $duser = $event->display_user; + $h_join_date = autodate($duser->join_date); + $class = $duser->class; + if ($duser->can(Permissions::HELLBANNED) && $class->parent) { + $h_class = $class->parent->name; } else { - $h_class = $event->display_user->class->name; + $h_class = $class->name; } $event->add_part("Joined: $h_join_date", 10); - if ($user->name == $event->display_user->name) { + if ($user->name == $duser->name) { $event->add_part("Current IP: " . get_real_ip(), 80); } $event->add_part("Class: $h_class", 90); - $av = $event->display_user->get_avatar_html(); + $av = $duser->get_avatar_html(); if ($av) { $event->add_part($av, 0); } elseif ( - ( - $config->get_string("avatar_host") == "gravatar" - ) && - ($user->id == $event->display_user->id) + ($config->get_string("avatar_host") == "gravatar") && + ($user->id == $duser->id) ) { $event->add_part( "No avatar? This gallery uses Gravatar for avatar hosting, use the" . @@ -393,7 +387,10 @@ private function display_stats(UserPageBuildingEvent $event): void $this->theme->display_user_links($page, $user, $ubbe->get_parts()); } if ( - ($user->can(Permissions::VIEW_IP) || ($user->is_logged_in() && $user->id == $event->display_user->id)) && # admin or self-user + ( + $user->can(Permissions::VIEW_IP) || # user can view all IPS + ($user->id == $event->display_user->id) # or user is viewing themselves + ) && ($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( @@ -496,7 +493,7 @@ public function onAdminBuilding(AdminBuildingEvent $event): void public function onUserCreation(UserCreationEvent $event): void { - global $config, $page, $user; + global $config, $database, $page, $user; $name = $event->username; //$pass = $event->password; @@ -517,8 +514,11 @@ public function onUserCreation(UserCreationEvent $event): void "letters, numbers, dash, and underscore" ); } - if (User::by_name($name)) { + try { + User::by_name($name); throw new UserCreationException("That username is already taken"); + } catch (UserNotFound $ex) { + // user not found is good } if (!captcha_check()) { throw new UserCreationException("Error in captcha"); @@ -535,10 +535,25 @@ public function onUserCreation(UserCreationEvent $event): void throw new UserCreationException("Email address is required"); } - $new_user = $this->create_user($event); + $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] + ); + $new_user = User::by_name($event->username); + $new_user->set_password($event->password); + log_info("user", "Created User @{$event->username}"); + if ($event->login) { send_event(new UserLoginEvent($new_user)); } + + $event->set_user($new_user); } public const USER_SEARCH_REGEX = "/^(?:poster|user)(!?)[=|:](.*)$/i"; @@ -571,11 +586,6 @@ public function onSearchTermParse(SearchTermParseEvent $event): void $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]); @@ -589,10 +599,7 @@ public function onSearchTermParse(SearchTermParseEvent $event): void public function onHelpPageBuilding(HelpPageBuildingEvent $event): void { if ($event->key === HelpPages::SEARCH) { - $block = new Block(); - $block->header = "Users"; - $block->body = (string) $this->theme->get_help_html(); - $event->add_block($block); + $event->add_section("Users", $this->theme->get_help_html()); } } @@ -613,19 +620,15 @@ private function page_login(string $name, string $pass): void global $config, $page; $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); + send_event(new UserLoginEvent($duser)); + $duser->set_login_cookie(); + $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")); - } + // Try returning to previous page + if ($config->get_int("user_loginshowprofile", 0)) { + $page->set_redirect(referer_or(make_link(), ["user/"])); } else { - $this->theme->display_error(401, "Error", "No user with those details was found"); + $page->set_redirect(make_link("user")); } } @@ -633,7 +636,7 @@ private function page_logout(): void { global $page, $config; $page->add_cookie("session", "", time() + 60 * 60 * 24 * $config->get_int('login_memory'), "/"); - if (SPEED_HAX) { + if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::PURGE_COOKIE)) { # 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'), "/"); @@ -652,70 +655,17 @@ private function page_logout(): void 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"); + if (is_null($my_user->email)) { + throw new InvalidInput("That user has no registered email address"); } else { throw new ServerError("Email sending not implemented"); } } - 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; + throw new PermissionDenied("You aren't logged in"); } if ( @@ -725,8 +675,7 @@ private function user_can_edit_user(User $a, User $b): bool ) { return true; } else { - $this->theme->display_error(401, "Error", "You need to be an admin to change other people's details"); - return false; + throw new PermissionDenied("You need to be an admin to change other people's details"); } } @@ -799,7 +748,6 @@ private function delete_user(Page $page, int $uid, bool $with_images = false, bo global $user, $config, $database; $page->set_title("Error"); - $page->set_heading("Error"); $page->add_block(new NavBlock()); $duser = User::by_id($uid); diff --git a/ext/user/test.php b/ext/user/test.php index cd1df1b6e..f9f03686d 100644 --- a/ext/user/test.php +++ b/ext/user/test.php @@ -16,8 +16,9 @@ public function testUserPage(): void $this->assert_text("Joined:"); $this->assert_no_text("Operations"); - $this->get_page('user/MauMau'); - $this->assert_title("No Such User"); + $this->assertException(UserNotFound::class, function () { + $this->get_page('user/MauMau'); + }); $this->log_in_as_user(); // should be on the user page @@ -62,18 +63,20 @@ public function testCreateOther(): void $this->post_page('user_admin/create_other', [ 'name' => 'testnew', 'pass1' => 'testnew', + 'pass2' => 'testnew', 'email' => '', ]); }); - $this->assertNull(User::by_name('testnew'), "Anon can't create others"); + $this->assertException(UserNotFound::class, function () {User::by_name('testnew');}); $this->log_in_as_admin(); $this->post_page('user_admin/create_other', [ 'name' => 'testnew', 'pass1' => 'testnew', + 'pass2' => 'testnew', 'email' => '', ]); $this->assertEquals(302, $page->code); - $this->assertNotNull(User::by_name('testnew'), "Admin can create others"); + $this->assertNotNull(User::by_name('testnew')); } } diff --git a/ext/user/theme.php b/ext/user/theme.php index 4b75004d1..1ae1dc8b3 100644 --- a/ext/user/theme.php +++ b/ext/user/theme.php @@ -28,11 +28,10 @@ class UserPageTheme extends Themelet public function display_login_page(Page $page): void { $page->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" + rawHTML("There should be a login box to the left") )); } @@ -110,7 +109,6 @@ public function display_signup_page(Page $page): void ); $page->set_title("Create Account"); - $page->set_heading("Create Account"); $page->add_block(new NavBlock()); $page->add_block(new Block("Signup", $html)); } @@ -149,17 +147,16 @@ public function display_user_creator(): void ) ) ); - $page->add_block(new Block("Create User", (string)$form, "main", 75)); + $page->add_block(new Block("Create User", $form, "main", 75)); } public function display_signups_disabled(Page $page): void { $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~" + rawHTML("The board admin has disabled the ability to create new accounts~") )); } @@ -250,13 +247,12 @@ public function display_user_page(User $duser, array $stats): void $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)); + $page->add_block(new Block("Stats", rawHTML(join("
                  ", $stats)), "main", 10)); } - public function build_operations(User $duser, UserOperationsBuildingEvent $event): string + public function build_operations(User $duser, UserOperationsBuildingEvent $event): HTMLElement { global $config, $user; $html = emptyHTML(); @@ -337,10 +333,10 @@ public function build_operations(User $duser, UserOperationsBuildingEvent $event } foreach ($event->get_parts() as $part) { - $html .= $part; + $html->appendChild($part); } } - return (string)$html; + return $html; } public function get_help_html(): HTMLElement @@ -402,7 +398,7 @@ public function display_user_classes(Page $page, array $classes, array $permissi $doc = $perm->getDocComment(); if ($doc) { - $doc = preg_replace('/\/\*\*|\n\s*\*\s*|\*\//', '', $doc); + $doc = preg_replace_ex('/\/\*\*|\n\s*\*\s*|\*\//', '', $doc); $row->appendChild(TD(["style" => "text-align: left;"], $doc)); } else { $row->appendChild(TD("")); @@ -412,7 +408,6 @@ public function display_user_classes(Page $page, array $classes, array $permissi } $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/main.php b/ext/user_config/main.php index d3802d5bf..12aa7c29c 100644 --- a/ext/user_config/main.php +++ b/ext/user_config/main.php @@ -61,12 +61,10 @@ public function onUserLogin(UserLoginEvent $event): void $user_config = self::get_for_user($event->user->id); } - public static function get_for_user(int $id): BaseConfig + public static function get_for_user(int $id): Config { global $database; - $user = User::by_id($id); - $user_config = new DatabaseConfig($database, "user_config", "user_id", "$id"); send_event(new InitUserConfigEvent($user, $user_config)); return $user_config; @@ -148,8 +146,7 @@ public function onPageRequest(PageRequestEvent $event): void } $target_config = UserConfig::get_for_user($duser->id); - send_event(new ConfigSaveEvent($target_config, $event->POST)); - $target_config->save(); + send_event(new ConfigSaveEvent($target_config, ConfigSaveEvent::postToSettings($event->POST))); $page->flash("Config saved"); $page->set_mode(PageMode::REDIRECT); $page->set_redirect(make_link("user_config")); diff --git a/ext/user_config/theme.php b/ext/user_config/theme.php index 69f410b27..e984110f5 100644 --- a/ext/user_config/theme.php +++ b/ext/user_config/theme.php @@ -65,23 +65,18 @@ public function display_user_config_page(Page $page, User $user, SetupPanel $pan "; $page->set_title("User Options"); - $page->set_heading("User Options"); $page->add_block(new NavBlock()); - $page->add_block(new Block("User Options", $table)); + $page->add_block(new Block("User Options", rawHTML($table))); $page->set_mode(PageMode::PAGE); } protected function sb_to_html(SetupBlock $block): string { - $h = $block->header; - $b = $block->body; - $i = preg_replace('/[^a-zA-Z0-9]/', '_', $h) . "-setup"; - $html = " + return "
                  - $h -
                  $b
                  + {$block->header} +
                  {$block->str_body}
                  "; - return $html; } } diff --git a/ext/view/events/image_info_set_event.php b/ext/view/events/image_info_set_event.php index 3ad7ff2ee..28909f88b 100644 --- a/ext/view/events/image_info_set_event.php +++ b/ext/view/events/image_info_set_event.php @@ -38,10 +38,10 @@ public function __construct(Image $image, int $slot, array $params) */ public function get_param(string $name): ?string { - if(!empty($this->params["$name{$this->slot}"])) { + if (!empty($this->params["$name{$this->slot}"])) { return $this->params["$name{$this->slot}"]; } - if(!empty($this->params[$name])) { + if (!empty($this->params[$name])) { return $this->params[$name]; } return null; diff --git a/ext/view/main.php b/ext/view/main.php index d31c7369e..41942225c 100644 --- a/ext/view/main.php +++ b/ext/view/main.php @@ -43,8 +43,7 @@ public function onPageRequest(PageRequestEvent $event): void } if (is_null($image)) { - $this->theme->display_error(404, "Post not found", "No more posts"); - return; + throw new PostNotFound("No more posts"); } $page->set_mode(PageMode::REDIRECT); @@ -55,8 +54,7 @@ public function onPageRequest(PageRequestEvent $event): void // who follows up every request to '/post/view/123' with // '/post/view/12300000000000Image 123: tags' which spams the // database log with 'integer out of range' - $this->theme->display_error(404, "Post not found", "Invalid post ID"); - return; + throw new PostNotFound("Invalid post ID"); } $image_id = $event->get_iarg('image_id'); @@ -76,7 +74,7 @@ public function onPageRequest(PageRequestEvent $event): void } $page->set_redirect(make_link("post/view/$image_id", null, $query)); } else { - $this->theme->display_error(403, "Post Locked", "An admin has locked this post"); + throw new PermissionDenied("An admin has locked this post"); } } } diff --git a/ext/view/test.php b/ext/view/test.php index 0d0d86ead..20c984322 100644 --- a/ext/view/test.php +++ b/ext/view/test.php @@ -41,8 +41,9 @@ public function testPrevNext(): void $image_id_3 = $this->post_image("tests/favicon.png", "test"); // Front image: no next, has prev - $page = $this->get_page("post/next/$image_id_1"); - $this->assertEquals(404, $page->code); + $this->assertException(PostNotFound::class, function () use ($image_id_1) { + $this->get_page("post/next/$image_id_1"); + }); $page = $this->get_page("post/prev/$image_id_1"); $this->assertEquals("/test/post/view/$image_id_2", $page->redirect); @@ -62,8 +63,9 @@ public function testPrevNext(): void // Last image has next, no prev $page = $this->get_page("post/next/$image_id_3"); $this->assertEquals("/test/post/view/$image_id_2", $page->redirect); - $page = $this->get_page("post/prev/$image_id_3"); - $this->assertEquals(404, $page->code); + $this->assertException(PostNotFound::class, function () use ($image_id_3) { + $this->get_page("post/prev/$image_id_3"); + }); } public function testPrevNextDisabledWhenOrdered(): void @@ -90,10 +92,10 @@ public function testView404(): void $image_id_1 = $this->post_image("tests/favicon.png", "test"); $idp1 = $image_id_1 + 1; - $this->assertException(ImageNotFound::class, function () use ($idp1) { + $this->assertException(PostNotFound::class, function () use ($idp1) { $this->get_page("post/view/$idp1"); }); - $this->assertException(ImageNotFound::class, function () { + $this->assertException(PostNotFound::class, function () { $this->get_page('post/view/-1'); }); } diff --git a/ext/view/theme.php b/ext/view/theme.php index 800ef830a..5166a6561 100644 --- a/ext/view/theme.php +++ b/ext/view/theme.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -use function MicroHTML\{A, joinHTML, TABLE, TR, TD, INPUT, emptyHTML, DIV, BR}; +use function MicroHTML\{A, joinHTML, TABLE, TR, TD, INPUT, emptyHTML, rawHTML, DIV, BR, META, LINK}; class ViewPostTheme extends Themelet { @@ -14,12 +14,17 @@ public function display_meta_headers(Image $image): void { global $page; - $h_metatags = str_replace(" ", ", ", html_escape($image->get_tag_list())); - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header(""); - $page->add_html_header("get_thumb_link())."\">"); - $page->add_html_header("id}"))."\">"); + $h_metatags = str_replace(" ", ", ", $image->get_tag_list()); + $page->add_html_header(META(["name" => "keywords", "content" => $h_metatags])); + $page->add_html_header(META(["property" => "og:title", "content" => $h_metatags])); + $page->add_html_header(META(["property" => "og:type", "content" => "article"])); + $page->add_html_header(META(["property" => "og:image", "content" => make_http($image->get_image_link())])); + $page->add_html_header(META(["property" => "og:url", "content" => make_http(make_link("post/view/{$image->id}"))])); + $page->add_html_header(META(["property" => "og:image:width", "content" => $image->width])); + $page->add_html_header(META(["property" => "og:image:height", "content" => $image->height])); + $page->add_html_header(META(["property" => "twitter:title", "content" => $h_metatags])); + $page->add_html_header(META(["property" => "twitter:card", "content" => "summary_large_image"])); + $page->add_html_header(META(["property" => "twitter:image:src", "content" => make_http($image->get_image_link())])); } /** @@ -37,9 +42,9 @@ public function display_page(Image $image, array $editor_parts): void //$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(""); + if (!$this->is_ordered_search()) { + $page->add_html_header(LINK(["id" => "nextlink", "rel" => "next", "href" => make_link("post/next/{$image->id}", $query)])); + $page->add_html_header(LINK(["id" => "prevlink", "rel" => "previous", "href" => make_link("post/prev/{$image->id}", $query)])); } } @@ -69,10 +74,10 @@ protected function get_query(): ?string */ protected function is_ordered_search(): bool { - if(isset($_GET['search'])) { + if (isset($_GET['search'])) { $tags = Tag::explode($_GET['search']); - foreach($tags as $tag) { - if(preg_match("/^order[=:]/", $tag) == 1) { + foreach ($tags as $tag) { + if (preg_match("/^order[=:]/", $tag) == 1) { return true; } } @@ -83,7 +88,7 @@ protected function is_ordered_search(): bool protected function build_pin(Image $image): HTMLElement { $query = $this->get_query(); - if($this->is_ordered_search()) { + if ($this->is_ordered_search()) { return A(["href" => make_link()], "Index"); } else { return joinHTML(" | ", [ @@ -94,7 +99,7 @@ protected function build_pin(Image $image): HTMLElement } } - protected function build_navigation(Image $image): string + protected function build_navigation(Image $image): HTMLElement { $h_pin = $this->build_pin($image); $h_search = " @@ -105,7 +110,7 @@ protected function build_navigation(Image $image): string "; - return "$h_pin
                  $h_search"; + return rawHTML("$h_pin
                  $h_search"); } /** @@ -119,7 +124,7 @@ protected function build_info(Image $image, array $editor_parts): HTMLElement return emptyHTML($image->is_locked() ? "[Post Locked]" : ""); } - if( + if ( (!$image->is_locked() || $user->can(Permissions::EDIT_IMAGE_LOCK)) && $user->can(Permissions::EDIT_IMAGE_TAG) ) { diff --git a/ext/wiki/main.php b/ext/wiki/main.php index 3c79cf90d..ea67fcab5 100644 --- a/ext/wiki/main.php +++ b/ext/wiki/main.php @@ -66,7 +66,7 @@ class WikiPage /** * @param array $row */ - public function __construct(array $row = null) + public function __construct(?array $row = null) { //assert(!empty($row)); global $database; @@ -176,10 +176,10 @@ public function onPageRequest(PageRequestEvent $event): void $title = $event->get_arg('title'); $action = $event->get_arg('action'); - if($action == "history") { + if ($action == "history") { $history = $this->get_history($title); $this->theme->display_page_history($page, $title, $history); - } elseif($action == "edit") { + } elseif ($action == "edit") { $content = $this->get_page($title); if ($this->can_edit($user, $content)) { $this->theme->display_page_editor($page, $content); @@ -192,7 +192,7 @@ public function onPageRequest(PageRequestEvent $event): void $title = $event->get_arg('title'); $action = $event->get_arg('action'); - if($action == "save") { + if ($action == "save") { $rev = int_escape($event->req_POST('revision')); $body = $event->req_POST('body'); $lock = $user->can(Permissions::WIKI_ADMIN) && ($event->get_POST('lock') == "on"); @@ -209,7 +209,7 @@ public function onPageRequest(PageRequestEvent $event): void } else { throw new PermissionDenied("You are not allowed to edit this page"); } - } elseif($action == "delete_revision") { + } elseif ($action == "delete_revision") { $content = $this->get_page($title); if ($user->can(Permissions::WIKI_ADMIN)) { $revision = int_escape($event->req_POST('revision')); @@ -220,7 +220,7 @@ public function onPageRequest(PageRequestEvent $event): void } else { throw new PermissionDenied("You are not allowed to edit this page"); } - } elseif($action == "delete_all") { + } elseif ($action == "delete_all") { if ($user->can(Permissions::WIKI_ADMIN)) { send_event(new WikiDeletePageEvent($title)); $u_title = url_escape($title); diff --git a/ext/wiki/theme.php b/ext/wiki/theme.php index 4b4458622..e0d79d8b5 100644 --- a/ext/wiki/theme.php +++ b/ext/wiki/theme.php @@ -4,6 +4,8 @@ namespace Shimmie2; +use MicroHTML\HTMLElement; + use function MicroHTML\{FORM, INPUT, TABLE, TR, TD, emptyHTML, rawHTML, BR, TEXTAREA, DIV, HR, P, A}; class WikiTheme extends Themelet @@ -43,9 +45,8 @@ public function display_page(Page $page, WikiPage $wiki_page, ?WikiPage $nav_pag } $page->set_title(html_escape($wiki_page->title)); - $page->set_heading(html_escape($wiki_page->title)); $page->add_block(new NavBlock()); - $page->add_block(new Block("Wiki Index", $tfe->formatted, "left", 20)); + $page->add_block(new Block("Wiki Index", rawHTML($tfe->formatted), "left", 20)); $page->add_block(new Block($title_html, $this->create_display_html($wiki_page))); } @@ -62,20 +63,18 @@ public function display_page_history(Page $page, string $title, array $history): } $html .= ""; $page->set_title(html_escape($title)); - $page->set_heading(html_escape($title)); $page->add_block(new NavBlock()); - $page->add_block(new Block(html_escape($title), $html)); + $page->add_block(new Block(html_escape($title), rawHTML($html))); } public function display_page_editor(Page $page, WikiPage $wiki_page): void { $page->set_title(html_escape($wiki_page->title)); - $page->set_heading(html_escape($wiki_page->title)); $page->add_block(new NavBlock()); $page->add_block(new Block("Editor", $this->create_edit_html($wiki_page))); } - protected function create_edit_html(WikiPage $page): string + protected function create_edit_html(WikiPage $page): HTMLElement { global $user; @@ -88,7 +87,7 @@ protected function create_edit_html(WikiPage $page): string emptyHTML(); $u_title = url_escape($page->title); - return (string)SHM_SIMPLE_FORM( + return SHM_SIMPLE_FORM( "wiki/$u_title/save", INPUT(["type" => "hidden", "name" => "revision", "value" => $page->revision + 1]), TEXTAREA(["name" => "body", "style" => "width: 100%", "rows" => 20], $page->body), @@ -98,7 +97,7 @@ protected function create_edit_html(WikiPage $page): string ); } - protected function create_display_html(WikiPage $page): string + protected function create_display_html(WikiPage $page): HTMLElement { global $user; @@ -108,7 +107,7 @@ protected function create_display_html(WikiPage $page): string $formatted_body = rawHTML(Wiki::format_tag_wiki_page($page)); $edit = TR(); - if(Wiki::can_edit($user, $page)) { + if (Wiki::can_edit($user, $page)) { $edit->appendChild(TD(FORM( ["action" => make_link("wiki/$u_title/edit", "revision={$page->revision}")], INPUT(["type" => "submit", "value" => "Edit"]) @@ -128,7 +127,7 @@ protected function create_display_html(WikiPage $page): string ))); } - return (string)DIV( + return DIV( ["class" => "wiki-page"], $formatted_body, HR(), diff --git a/ext/word_filter/main.php b/ext/word_filter/main.php index b456a24f1..c2f070b6c 100644 --- a/ext/word_filter/main.php +++ b/ext/word_filter/main.php @@ -32,11 +32,12 @@ private function filter(string $text): string $search = trim($search); $replace = trim($replace); if ($search[0] == '/') { - $text = preg_replace($search, $replace, $text); + $text = preg_replace_ex($search, $replace, $text); } else { $search = "/\\b" . str_replace("/", "\\/", $search) . "\\b/i"; - $text = preg_replace($search, $replace, $text); + $text = preg_replace_ex($search, $replace, $text); } + assert(is_string($text)); } return $text; } diff --git a/index.php b/index.php index a061d9965..3a26b8b9e 100644 --- a/index.php +++ b/index.php @@ -52,7 +52,7 @@ $config = new DatabaseConfig($database); _load_extension_files(); _load_theme_files(); -$page = new Page(); +$page = get_theme_class("Page"); _load_event_listeners(); $_tracer->end(); @@ -71,7 +71,7 @@ ] ); - if (!SPEED_HAX) { + if (!(Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::NO_AUTO_DB_UPGRADE))) { send_event(new DatabaseUpgradeEvent()); } send_event(new InitExtEvent()); @@ -84,7 +84,7 @@ ob_implicit_flush(true); $app = new CliApp(); send_event(new CliGenEvent($app)); - if($app->run() !== 0) { + if ($app->run() !== 0) { throw new \Exception("CLI command failed"); } } else { @@ -105,7 +105,7 @@ if ($database->is_transaction_open()) { $database->rollback(); } - if(is_a($e, \Shimmie2\UserError::class)) { + if (is_a($e, \Shimmie2\UserError::class)) { $page->set_mode(PageMode::PAGE); $page->set_code($e->http_code); $page->set_title("Error"); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ec8f9b744..c556b09fb 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -53,7 +53,7 @@ // 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()) { +if ($database->is_transaction_open()) { $database->commit(); } $_tracer->end(); diff --git a/tests/defines.php b/tests/defines.php index 3d5a9a4c9..95c423575 100644 --- a/tests/defines.php +++ b/tests/defines.php @@ -14,7 +14,6 @@ define("CACHE_DSN", null); define("DEBUG", false); define("COOKIE_PREFIX", 'shm'); -define("SPEED_HAX", false); define("WH_SPLITS", 1); define("VERSION", 'unit-tests'); define("TRACE_FILE", null); @@ -23,3 +22,4 @@ define("CLI_LOG_LEVEL", 50); define("STATSD_HOST", null); define("TRUSTED_PROXIES", []); +define("SECRET", "asdfghjkl"); diff --git a/tests/phpstan.neon b/tests/phpstan.neon index d80d46f14..abd1f702e 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -1,6 +1,6 @@ parameters: errorFormat: raw - level: 7 + level: 8 paths: - ../core - ../ext @@ -8,7 +8,6 @@ parameters: - ../themes dynamicConstantNames: - DEBUG - - SPEED_HAX - TRUSTED_PROXIES - TIMEZONE - BASE_HREF diff --git a/tests/phpunit.xml b/tests/phpunit.xml index de47f9ef5..f00f7880d 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,11 +1,16 @@ - - + + @@ -17,9 +22,9 @@ - ../core - ../ext - ../themes/default + ../core/ + ../ext/ + ../themes/ diff --git a/themes/danbooru/comment.theme.php b/themes/danbooru/comment.theme.php index 73caabc63..909585d94 100644 --- a/themes/danbooru/comment.theme.php +++ b/themes/danbooru/comment.theme.php @@ -4,7 +4,9 @@ namespace Shimmie2; -class CustomCommentListTheme extends CommentListTheme +use function MicroHTML\rawHTML; + +class DanbooruCommentListTheme extends CommentListTheme { /** * @param array $images @@ -28,8 +30,7 @@ public function display_comment_list(array $images, int $page_number, int $total $nav = "$h_prev | $h_index | $h_next"; $page->set_title("Comments"); - $page->set_heading("Comments"); - $page->add_block(new Block("Navigation", $nav, "left")); + $page->add_block(new Block("Navigation", rawHTML($nav), "left")); $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); // parts for each image @@ -85,7 +86,7 @@ public function display_comment_list(array $images, int $page_number, int $total "; - $page->add_block(new Block(" ", $html, "main", $position++)); + $page->add_block(new Block(null, rawHTML($html), "main", $position++)); } } diff --git a/themes/danbooru/themelet.class.php b/themes/danbooru/common_elements.theme.php similarity index 97% rename from themes/danbooru/themelet.class.php rename to themes/danbooru/common_elements.theme.php index 422c62ecc..93c9d007c 100644 --- a/themes/danbooru/themelet.class.php +++ b/themes/danbooru/common_elements.theme.php @@ -8,7 +8,7 @@ use function MicroHTML\{A, B, DIV, joinHTML}; -class Themelet extends BaseThemelet +class DanbooruCommonElementsTheme extends CommonElementsTheme { public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false): void { diff --git a/themes/danbooru/index.theme.php b/themes/danbooru/index.theme.php index 7526f57a8..40857a5fe 100644 --- a/themes/danbooru/index.theme.php +++ b/themes/danbooru/index.theme.php @@ -4,7 +4,11 @@ namespace Shimmie2; -class CustomIndexTheme extends IndexTheme +use MicroHTML\HTMLElement; + +use function MicroHTML\rawHTML; + +class DanbooruIndexTheme extends IndexTheme { /** * @param Image[] $images @@ -26,7 +30,6 @@ public function display_page(Page $page, array $images): void $nav = $this->build_navigation($this->page_number, $this->total_pages, $this->search_terms); $page->set_title($page_title); - $page->set_heading($page_title); $page->add_block(new Block("Search", $nav, "left", 0)); if (count($images) > 0) { if ($query) { @@ -37,27 +40,27 @@ public function display_page(Page $page, array $images): void $this->display_paginator($page, "post/list", null, $this->page_number, $this->total_pages); } } else { - $page->add_block(new Block("No Posts Found", "No images were found to match the search criteria")); + throw new PostNotFound("No posts were found to match the search criteria"); } } /** * @param string[] $search_terms */ - protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string + protected function build_navigation(int $page_number, int $total_pages, array $search_terms): HTMLElement { $h_search_string = count($search_terms) == 0 ? "" : html_escape(implode(" ", $search_terms)); $h_search_link = search_link(); - return " + return rawHTML("

                  -
                  "; +
                  "); } - protected function build_table(array $images, ?string $query): string + protected function build_table(array $images, ?string $query): HTMLElement { $h_query = html_escape($query); $table = "
                  "; @@ -65,6 +68,6 @@ protected function build_table(array $images, ?string $query): string $table .= $this->build_thumb_html($image) . "\n"; } $table .= "
                  "; - return $table; + return rawHTML($table); } } diff --git a/themes/danbooru/page.class.php b/themes/danbooru/page.class.php index d0bafacc4..42d461e3b 100644 --- a/themes/danbooru/page.class.php +++ b/themes/danbooru/page.class.php @@ -6,6 +6,8 @@ use MicroHTML\HTMLElement; +use function MicroHTML\{DIV, LI, A, rawHTML, emptyHTML, UL, ARTICLE, FOOTER, EM, HEADER, H1, NAV}; + /** * Name: Danbooru Theme * Author: Bzchan @@ -48,35 +50,35 @@ back and forward to other themes all you like. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -class Page extends BasePage +class DanbooruPage extends Page { - public function body_html(): string + public function body_html(): HTMLElement { global $config; list($nav_links, $sub_links) = $this->get_nav_links(); - $left_block_html = ""; - $user_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; + $left_block_html = []; + $user_block_html = []; + $main_block_html = []; + $sub_block_html = []; foreach ($this->blocks as $block) { switch ($block->section) { case "left": - $left_block_html .= $block->get_html(true); + $left_block_html[] = $this->block_html($block, true); break; case "user": - $user_block_html .= $block->body; + $user_block_html[] = $block->body; break; case "subheading": - $sub_block_html .= $block->body; + $sub_block_html[] = $block->body; break; case "main": if ($block->header == "Posts") { $block->header = " "; } - $main_block_html .= $block->get_html(false); + $main_block_html[] = $this->block_html($block, false); break; default: print "

                  error: {$block->header} using an unknown section ({$block->section})"; @@ -85,76 +87,63 @@ public function body_html(): string } if (empty($this->subheading)) { - $subheading = ""; + $subheading = null; } else { - $subheading = "

                  {$this->subheading}
                  "; + $subheading = DIV(["id" => "subtitle"], $this->subheading); } $site_name = $config->get_string(SetupConfig::TITLE); // bzchan: change from normal default to get title for top of page $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page - $custom_links = ""; + $custom_links = emptyHTML(); foreach ($nav_links as $nav_link) { - $custom_links .= "
                • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
                • "; + $custom_links->appendChild(LI($this->navlinks($nav_link->link, $nav_link->description, $nav_link->active))); } $custom_sublinks = ""; if (!empty($sub_links)) { - $custom_sublinks = "
                  "; + $custom_sublinks = DIV(["class" => "sbar"]); foreach ($sub_links as $nav_link) { - $custom_sublinks .= "
                • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
                • "; + $custom_sublinks->appendChild(LI($this->navlinks($nav_link->link, $nav_link->description, $nav_link->active))); } - $custom_sublinks .= "
                  "; } - // bzchan: failed attempt to add heading after title_link (failure was it looked bad) - //if($this->heading==$site_name)$this->heading = ''; - //$title_link = "

                  $site_name/$this->heading

                  "; - - // bzchan: prepare main title link - $title_link = "

                  $site_name

                  "; + $title_link = H1(["id" => "site-title"], A(["href" => make_link($main_page)], $site_name)); if ($this->left_enabled) { - $left = ""; + $left = NAV(...$left_block_html); $withleft = "withleft"; } else { $left = ""; $withleft = "noleft"; } - $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $flash_html = $this->flash_html(); $footer_html = $this->footer_html(); - return << - $title_link - - - - $subheading - $sub_block_html - $left -
                  - $flash_html - $main_block_html -
                  -
                  $footer_html
                  -EOD; + return emptyHTML( + HEADER( + $title_link, + UL(["id" => "navbar", "class" => "flat-list"], $custom_links), + UL(["id" => "subnavbar", "class" => "flat-list"], $custom_sublinks), + ), + $subheading, + emptyHTML(...$sub_block_html), + $left, + ARTICLE( + ["class" => $withleft], + $flash_html, + ...$main_block_html + ), + FOOTER(EM($footer_html)) + ); } - public function navlinks(Link $link, HTMLElement|string $desc, bool $active): ?string + public function navlinks(Link $link, HTMLElement|string $desc, bool $active): HTMLElement { - $html = null; - if ($active) { - $html = "{$desc}"; - } else { - $html = "{$desc}"; - } - - return $html; + return A([ + "class" => $active ? "current-page" : "tab", + "href" => $link->make_link(), + ], $desc); } } diff --git a/themes/danbooru/style.css b/themes/danbooru/style.css index 32946a14d..42180d0ca 100644 --- a/themes/danbooru/style.css +++ b/themes/danbooru/style.css @@ -1,8 +1,20 @@ -.noleft{ -padding-left:20px; +.withleft { +margin-left: 10px; +} +ARTICLE { +grid-column: 2; +grid-row: 2; +} +ARTICLE SECTION H3 { +margin-top: 0; } HEADER { -margin-bottom:1em; +grid-column: 1 / 3; +grid-row: 1; +text-align:left; +} +HEADER h1 { +text-align:left; } HEADER #site-title { padding:10px 20px 0; @@ -34,6 +46,9 @@ HEADER #site-title { padding:10px 20px 0; } body { +display: grid; +grid-template-columns: 150px auto; +grid-gap: 0 20px; -x-system-font:none; background-color:#FFFFFF; font-family:verdana,sans-serif; @@ -94,12 +109,18 @@ padding:0 1.5em; padding-bottom:0.2em; } FOOTER { +grid-column: 1 / 3; +grid-row: 3; clear:both; color:#CCCCCC; font-size:0.9em; padding-left:10px; padding-top:8px; } +CODE { +background: #DEDEDE; +font-size: 0.9rem; +} form { margin:0; } @@ -110,7 +131,8 @@ a:hover { text-decoration:underline; } NAV { -float:left; +grid-column: 1; +grid-row: 2; text-align:left; width:150px; padding:5px 20px 2px 10px; @@ -168,9 +190,6 @@ color: gray; .comment TD { text-align: left; } -.withleft { -margin-left:180px; -} div#paginator { clear:both; display:block; @@ -216,12 +235,6 @@ width:350px; font-size:1.5em; font-weight:bold; } -HEADER { -text-align:left; -} -HEADER h1 { -text-align:left; -} * { font-family:verdana,sans-serif; margin:0; @@ -260,6 +273,7 @@ text-align:left; } ul.flat-list li a { font-weight:normal; +display: inline-block; } #tips { margin-left:16px; @@ -275,3 +289,32 @@ margin-left:16px; margin-right:16px; font-size: 90%; } +@media screen and (width <= 800px) { +BODY { +grid-template-columns: auto; +} +HEADER { +grid-column: 1; +grid-row: 1; +} +ARTICLE { +grid-column: 1; +grid-row: 2; +} +NAV { +grid-column: 1; +grid-row: 3; +margin: auto; +width: auto; +} +FOOTER { +grid-column: 1; +grid-row: 4; +} +.withleft { +margin-left: 0; +} +.shm-image-list { +justify-content: center; +} +} diff --git a/themes/danbooru/tag_list.theme.php b/themes/danbooru/tag_list.theme.php index 118019e0a..ed7f0ffa3 100644 --- a/themes/danbooru/tag_list.theme.php +++ b/themes/danbooru/tag_list.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomTagListTheme extends TagListTheme +class DanbooruTagListTheme extends TagListTheme { public function display_page(Page $page): void { diff --git a/themes/danbooru/upload.theme.php b/themes/danbooru/upload.theme.php index 411d7e978..7005f62d8 100644 --- a/themes/danbooru/upload.theme.php +++ b/themes/danbooru/upload.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomUploadTheme extends UploadTheme +class DanbooruUploadTheme extends UploadTheme { public function display_block(Page $page): void { diff --git a/themes/danbooru/user.theme.php b/themes/danbooru/user.theme.php index 3772db162..fa60aa06f 100644 --- a/themes/danbooru/user.theme.php +++ b/themes/danbooru/user.theme.php @@ -4,13 +4,14 @@ namespace Shimmie2; -class CustomUserPageTheme extends UserPageTheme +use function MicroHTML\rawHTML; + +class DanbooruUserPageTheme extends UserPageTheme { public function display_login_page(Page $page): void { global $config; $page->set_title("Login"); - $page->set_heading("Login"); $page->disable_left(); $html = "
                  @@ -30,7 +31,7 @@ public function display_login_page(Page $page): void if ($config->get_bool("login_signup_enabled")) { $html .= "Create Account"; } - $page->add_block(new Block("Login", $html, "main", 90)); + $page->add_block(new Block("Login", rawHTML($html), "main", 90)); } /** @@ -58,7 +59,7 @@ public function display_user_block(Page $page, User $user, array $parts): void } $html .= "
                • {$part["name"]}"; } - $b = new Block("User Links", $html, "user", 90); + $b = new Block("User Links", rawHTML($html), "user", 90); $b->is_content = false; $page->add_block($b); } @@ -92,9 +93,8 @@ public function display_signup_page(Page $page): void "; $page->set_title("Create Account"); - $page->set_heading("Create Account"); $page->disable_left(); - $page->add_block(new Block("Signup", $html)); + $page->add_block(new Block("Signup", rawHTML($html))); } /** @@ -116,7 +116,7 @@ public function display_ip_list(Page $page, array $uploads, array $comments, arr $html .= ""; $html .= "(Most recent at top)"; - $page->add_block(new Block("IPs", $html)); + $page->add_block(new Block("IPs", rawHTML($html))); } /** diff --git a/themes/danbooru/view.theme.php b/themes/danbooru/view.theme.php index eed15aea2..27cdd131a 100644 --- a/themes/danbooru/view.theme.php +++ b/themes/danbooru/view.theme.php @@ -6,7 +6,9 @@ use MicroHTML\HTMLElement; -class CustomViewPostTheme extends ViewPostTheme +use function MicroHTML\rawHTML; + +class DanbooruViewPostTheme extends ViewPostTheme { /** * @param HTMLElement[] $editor_parts @@ -21,7 +23,7 @@ public function display_page(Image $image, array $editor_parts): void $page->add_block(new Block(null, $this->build_pin($image), "main", 11)); } - private function build_stats(Image $image): string + private function build_stats(Image $image): HTMLElement { $h_owner = html_escape($image->get_owner()->name); $h_ownerlink = "$h_owner"; @@ -53,13 +55,14 @@ private function build_stats(Image $image): string } if (Extension::is_enabled(RatingsInfo::KEY)) { - if ($image['rating'] === null || $image['rating'] == "?") { - $image['rating'] = "?"; + $rating = $image['rating']; + if ($rating === null) { + $rating = "?"; } - $h_rating = Ratings::rating_to_human($image['rating']); - $html .= "
                  Rating: $h_rating"; + $h_rating = Ratings::rating_to_human($rating); + $html .= "
                  Rating: $h_rating"; } - return $html; + return rawHTML($html); } } diff --git a/themes/danbooru2/admin.theme.php b/themes/danbooru2/admin.theme.php index 563938631..c5b0889ca 100644 --- a/themes/danbooru2/admin.theme.php +++ b/themes/danbooru2/admin.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomAdminPageTheme extends AdminPageTheme +class Danbooru2AdminPageTheme extends AdminPageTheme { public function display_page(): void { diff --git a/themes/danbooru2/comment.theme.php b/themes/danbooru2/comment.theme.php index 2d3650883..f740af6a9 100644 --- a/themes/danbooru2/comment.theme.php +++ b/themes/danbooru2/comment.theme.php @@ -4,7 +4,9 @@ namespace Shimmie2; -class CustomCommentListTheme extends CommentListTheme +use function MicroHTML\rawHTML; + +class Danbooru2CommentListTheme extends CommentListTheme { /** * @param array $images @@ -28,8 +30,7 @@ public function display_comment_list(array $images, int $page_number, int $total $nav = "$h_prev | $h_index | $h_next"; $page->set_title("Comments"); - $page->set_heading("Comments"); - $page->add_block(new Block("Navigation", $nav, "left")); + $page->add_block(new Block("Navigation", rawHTML($nav), "left")); $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); // parts for each image @@ -84,7 +85,7 @@ public function display_comment_list(array $images, int $page_number, int $total "; - $page->add_block(new Block(" ", $html, "main", $position++)); + $page->add_block(new Block(null, rawHTML($html), "main", $position++)); } } diff --git a/themes/danbooru2/themelet.class.php b/themes/danbooru2/common_elements.theme.php similarity index 97% rename from themes/danbooru2/themelet.class.php rename to themes/danbooru2/common_elements.theme.php index 5f810471e..85a6d5fdd 100644 --- a/themes/danbooru2/themelet.class.php +++ b/themes/danbooru2/common_elements.theme.php @@ -8,7 +8,7 @@ use function MicroHTML\{A,B,DIV,joinHTML}; -class Themelet extends BaseThemelet +class Danbooru2CommonElementsTheme extends CommonElementsTheme { public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false): void { diff --git a/themes/danbooru2/ext_manager.theme.php b/themes/danbooru2/ext_manager.theme.php index 1d283157a..50f0903c6 100644 --- a/themes/danbooru2/ext_manager.theme.php +++ b/themes/danbooru2/ext_manager.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomExtManagerTheme extends ExtManagerTheme +class Danbooru2ExtManagerTheme extends ExtManagerTheme { public function display_table(Page $page, array $extensions, bool $editable): void { diff --git a/themes/danbooru2/index.theme.php b/themes/danbooru2/index.theme.php index 01f8b23cc..0d7fcfab8 100644 --- a/themes/danbooru2/index.theme.php +++ b/themes/danbooru2/index.theme.php @@ -4,7 +4,11 @@ namespace Shimmie2; -class CustomIndexTheme extends IndexTheme +use MicroHTML\HTMLElement; + +use function MicroHTML\rawHTML; + +class Danbooru2IndexTheme extends IndexTheme { /** * @param Image[] $images @@ -21,30 +25,30 @@ public function display_page(Page $page, array $images): void if (count($images) > 0) { $this->display_page_images($page, $images); } else { - $this->display_error(404, "No Posts Found", "No images were found to match the search criteria"); + throw new PostNotFound("No posts were found to match the search criteria"); } } /** * @param string[] $search_terms */ - protected function build_navigation(int $page_number, int $total_pages, array $search_terms): string + protected function build_navigation(int $page_number, int $total_pages, array $search_terms): HTMLElement { $h_search_string = count($search_terms) == 0 ? "" : html_escape(implode(" ", $search_terms)); $h_search_link = search_link(); - return " + return rawHTML("

                  -
                  "; +
                  "); } /** * @param Image[] $images */ - protected function build_table(array $images, ?string $query): string + protected function build_table(array $images, ?string $query): HTMLElement { $h_query = html_escape($query); $table = "
                  "; @@ -52,6 +56,6 @@ protected function build_table(array $images, ?string $query): string $table .= $this->build_thumb_html($image) . "\n"; } $table .= "
                  "; - return $table; + return rawHTML($table); } } diff --git a/themes/danbooru2/page.class.php b/themes/danbooru2/page.class.php index d714cbdac..e191acae5 100644 --- a/themes/danbooru2/page.class.php +++ b/themes/danbooru2/page.class.php @@ -6,6 +6,8 @@ use MicroHTML\HTMLElement; +use function MicroHTML\{DIV, LI, A, rawHTML, emptyHTML, UL, ARTICLE, FOOTER, EM, HEADER, H1, NAV}; + /** * Name: Danbooru 2 Theme * Author: Bzchan , updated by Daniel Oaks @@ -49,35 +51,35 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -class Page extends BasePage +class Danbooru2Page extends Page { - public function body_html(): string + public function body_html(): HTMLElement { global $config; list($nav_links, $sub_links) = $this->get_nav_links(); - $left_block_html = ""; - $user_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; + $left_block_html = []; + $user_block_html = []; + $main_block_html = []; + $sub_block_html = []; foreach ($this->blocks as $block) { switch ($block->section) { case "left": - $left_block_html .= $block->get_html(true); + $left_block_html[] = $this->block_html($block, true); break; case "user": - $user_block_html .= $block->body; + $user_block_html[] = $block->body; break; case "subheading": - $sub_block_html .= $block->body; + $sub_block_html[] = $block->body; break; case "main": if ($block->header == "Posts") { $block->header = " "; } - $main_block_html .= $block->get_html(false); + $main_block_html[] = $this->block_html($block, false); break; default: print "

                  error: {$block->header} using an unknown section ({$block->section})"; @@ -86,76 +88,63 @@ public function body_html(): string } if (empty($this->subheading)) { - $subheading = ""; + $subheading = null; } else { - $subheading = "

                  {$this->subheading}
                  "; + $subheading = DIV(["id" => "subtitle"], $this->subheading); } $site_name = $config->get_string(SetupConfig::TITLE); // bzchan: change from normal default to get title for top of page $main_page = $config->get_string(SetupConfig::MAIN_PAGE); // bzchan: change from normal default to get main page for top of page - $custom_links = ""; + $custom_links = emptyHTML(); foreach ($nav_links as $nav_link) { - $custom_links .= "
                • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
                • "; + $custom_links->appendChild(LI($this->navlinks($nav_link->link, $nav_link->description, $nav_link->active))); } $custom_sublinks = ""; if (!empty($sub_links)) { - $custom_sublinks = "
                  "; + $custom_sublinks = DIV(["class" => "sbar"]); foreach ($sub_links as $nav_link) { - $custom_sublinks .= "
                • ".$this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)."
                • "; + $custom_sublinks->appendChild(LI($this->navlinks($nav_link->link, $nav_link->description, $nav_link->active))); } - $custom_sublinks .= "
                  "; } - // bzchan: failed attempt to add heading after title_link (failure was it looked bad) - //if($this->heading==$site_name)$this->heading = ''; - //$title_link = "

                  $site_name/$this->heading

                  "; - - // bzchan: prepare main title link - $title_link = "

                  $site_name

                  "; + $title_link = H1(["id" => "site-title"], A(["href" => make_link($main_page)], $site_name)); if ($this->left_enabled) { - $left = ""; + $left = NAV(...$left_block_html); $withleft = "withleft"; } else { $left = ""; $withleft = "noleft"; } - $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $flash_html = $this->flash_html(); $footer_html = $this->footer_html(); - return << - $title_link - - - - $subheading - $sub_block_html - $left -
                  - $flash_html - $main_block_html -
                  -
                  $footer_html
                  -EOD; + return emptyHTML( + HEADER( + $title_link, + UL(["id" => "navbar", "class" => "flat-list"], $custom_links), + UL(["id" => "subnavbar", "class" => "flat-list"], $custom_sublinks), + ), + $subheading, + emptyHTML(...$sub_block_html), + $left, + ARTICLE( + ["class" => $withleft], + $flash_html, + ...$main_block_html + ), + FOOTER(DIV($footer_html)) + ); } - public function navlinks(Link $link, HTMLElement|string $desc, bool $active): ?string + public function navlinks(Link $link, HTMLElement|string $desc, bool $active): HTMLElement { - $html = null; - if ($active) { - $html = "{$desc}"; - } else { - $html = "{$desc}"; - } - - return $html; + return A([ + "class" => $active ? "current-page" : "tab", + "href" => $link->make_link(), + ], $desc); } } diff --git a/themes/danbooru2/style.css b/themes/danbooru2/style.css index 2dd608e3f..7cb7df576 100644 --- a/themes/danbooru2/style.css +++ b/themes/danbooru2/style.css @@ -56,7 +56,13 @@ padding-left:2rem; } HEADER { +grid-column: 1 / 3; +grid-row: 1; margin-bottom:0.9rem; +text-align:left; +} +HEADER h1 { +text-align:left; } HEADER #site-title { margin-left: 30px; @@ -89,7 +95,6 @@ background-color: var(--header-selected); font-weight:bold; } HEADER ul#subnavbar { -margin:0 0 0.5rem; padding:0 30px 0 30px; background-color: var(--header-selected); } @@ -102,6 +107,9 @@ HEADER ul#subnavbar li:first-child { margin-left: -0.6rem; } body { +display: grid; +grid-template-columns: 11.5rem auto; +grid-gap: 0 2rem; background-color: var(--page); color: var(--text); } @@ -155,6 +163,8 @@ font-size:1.2em; padding-bottom:0.2em; } FOOTER { +grid-column: 1 / 3; +grid-row: 3; clear:both; border-top:solid 1px #E7E7F7; margin-top:1rem; @@ -165,6 +175,10 @@ font-size:0.8rem; FOOTER > DIV { margin: 1rem 2rem; } +CODE { +background: #DEDEDE; +font-size: 0.9rem; +} form { margin:0; } @@ -175,7 +189,8 @@ a:hover { text-decoration:underline; } NAV { -float:left; +grid-column: 1; +grid-row: 2; padding:0 1rem 0.2rem 2rem; width:11.5rem; text-align:left; @@ -240,7 +255,7 @@ color: var(--comment-meta); text-align: left; } .withleft { -margin-left:14.5rem; +margin-left: 1rem; } div#paginator { display:block; @@ -313,12 +328,6 @@ padding:0.2rem 0.6rem; font-weight:bold; font-size:1.5em; } -HEADER { -text-align:left; -} -HEADER h1 { -text-align:left; -} * { margin:0; padding:0; @@ -381,6 +390,8 @@ text-align:center; border-radius:0.5rem; } ARTICLE { +grid-column: 2; +grid-row: 2; margin-right:1rem; } ARTICLE section + section { @@ -392,3 +403,36 @@ margin-top:0.5rem; #Imagemain h3 { display:none; } +@media screen and (width <= 800px) { +BODY { +grid-template-columns: auto; +} +HEADER { +grid-column: 1; +grid-row: 1; +} +ARTICLE { +grid-column: 1; +grid-row: 2; +margin: 0 16px; +} +NAV { +grid-column: 1; +grid-row: 3; +margin: auto; +width: auto; +} +FOOTER { +grid-column: 1; +grid-row: 4; +} +.withleft { +margin: 0; +} +#image-list .blockbody { +margin: 0; +} +.shm-image-list { +justify-content: center; +} +} diff --git a/themes/danbooru2/tag_list.theme.php b/themes/danbooru2/tag_list.theme.php index 118019e0a..8df661593 100644 --- a/themes/danbooru2/tag_list.theme.php +++ b/themes/danbooru2/tag_list.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomTagListTheme extends TagListTheme +class Danbooru2TagListTheme extends TagListTheme { public function display_page(Page $page): void { diff --git a/themes/danbooru2/upload.theme.php b/themes/danbooru2/upload.theme.php index 411d7e978..ecdb221a1 100644 --- a/themes/danbooru2/upload.theme.php +++ b/themes/danbooru2/upload.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomUploadTheme extends UploadTheme +class Danbooru2UploadTheme extends UploadTheme { public function display_block(Page $page): void { diff --git a/themes/danbooru2/user.theme.php b/themes/danbooru2/user.theme.php index 3772db162..621d7057d 100644 --- a/themes/danbooru2/user.theme.php +++ b/themes/danbooru2/user.theme.php @@ -4,13 +4,14 @@ namespace Shimmie2; -class CustomUserPageTheme extends UserPageTheme +use function MicroHTML\rawHTML; + +class Danbooru2UserPageTheme extends UserPageTheme { public function display_login_page(Page $page): void { global $config; $page->set_title("Login"); - $page->set_heading("Login"); $page->disable_left(); $html = "
                  @@ -30,7 +31,7 @@ public function display_login_page(Page $page): void if ($config->get_bool("login_signup_enabled")) { $html .= "Create Account"; } - $page->add_block(new Block("Login", $html, "main", 90)); + $page->add_block(new Block("Login", rawHTML($html), "main", 90)); } /** @@ -58,20 +59,26 @@ public function display_user_block(Page $page, User $user, array $parts): void } $html .= "
                • {$part["name"]}"; } - $b = new Block("User Links", $html, "user", 90); + $b = new Block("User Links", rawHTML($html), "user", 90); $b->is_content = false; $page->add_block($b); } public function display_signup_page(Page $page): void { - global $config; + global $config, $user; $tac = $config->get_string("login_tac", ""); $tac = send_event(new TextFormattingEvent($tac))->formatted; $reca = "".captcha_get_html().""; + $email_required = ( + $config->get_bool("user_email_required") && + !$user->can(Permissions::CREATE_OTHER_USER) + ); + $email_text = $email_required ? "Email" : "Email (Optional)"; + if (empty($tac)) { $html = ""; } else { @@ -84,17 +91,16 @@ public function display_signup_page(Page $page): void Name Password Repeat Password - Email (Optional) - $reca; + $email_text + $reca
                • "; $page->set_title("Create Account"); - $page->set_heading("Create Account"); $page->disable_left(); - $page->add_block(new Block("Signup", $html)); + $page->add_block(new Block("Signup", rawHTML($html))); } /** @@ -116,7 +122,7 @@ public function display_ip_list(Page $page, array $uploads, array $comments, arr $html .= ""; $html .= "(Most recent at top)"; - $page->add_block(new Block("IPs", $html)); + $page->add_block(new Block("IPs", rawHTML($html))); } /** diff --git a/themes/danbooru2/view.theme.php b/themes/danbooru2/view.theme.php index 1b6403ffc..18c3bd07d 100644 --- a/themes/danbooru2/view.theme.php +++ b/themes/danbooru2/view.theme.php @@ -6,7 +6,9 @@ use MicroHTML\HTMLElement; -class CustomViewPostTheme extends ViewPostTheme +use function MicroHTML\rawHTML; + +class Danbooru2ViewPostTheme extends ViewPostTheme { /** * @param HTMLElement[] $editor_parts @@ -20,7 +22,7 @@ public function display_page(Image $image, array $editor_parts): void $page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 15)); } - private function build_information(Image $image): string + private function build_information(Image $image): HTMLElement { $h_owner = html_escape($image->get_owner()->name); $h_ownerlink = "$h_owner"; @@ -54,30 +56,28 @@ private function build_information(Image $image): string } if (Extension::is_enabled(RatingsInfo::KEY)) { - if ($image['rating'] === null || $image['rating'] == "?") { - $image['rating'] = "?"; - } - // @phpstan-ignore-next-line - ??? - if (Extension::is_enabled(RatingsInfo::KEY)) { - $h_rating = Ratings::rating_to_human($image['rating']); - $html .= "
                  Rating: $h_rating"; + $rating = $image['rating']; + if ($rating === null) { + $rating = "?"; } + $h_rating = Ratings::rating_to_human($rating); + $html .= "
                  Rating: $h_rating"; } - return $html; + return rawHTML($html); } - protected function build_navigation(Image $image): string + protected function build_navigation(Image $image): HTMLElement { //$h_pin = $this->build_pin($image); $h_search = "
                  - +
                  "; - return "$h_search"; + return rawHTML($h_search); } } diff --git a/themes/default/page.class.php b/themes/default/page.class.php index 026403af6..ec1b624dd 100644 --- a/themes/default/page.class.php +++ b/themes/default/page.class.php @@ -4,6 +4,6 @@ namespace Shimmie2; -class Page extends BasePage +class DefaultPage extends Page { } diff --git a/themes/default/style.css b/themes/default/style.css index 0746f9ac0..88005e397 100644 --- a/themes/default/style.css +++ b/themes/default/style.css @@ -52,14 +52,21 @@ font-family: sans-serif; } +HEADER { + grid-column: 1 / 3; + grid-row: 1; +} HEADER H1 { max-height: 3em; overflow: hidden; } BODY { + display: grid; + grid-template-columns: 216px auto; + grid-gap: 0 10px; background: var(--page); color: var(--text); - margin: 0; + margin: 0; } H1 { background: var(--title); @@ -105,11 +112,14 @@ TABLE.zebra TR:nth-child(odd) {background: var(--zebra-odd);} TABLE.zebra TR:nth-child(even) {background: var(--zebra-even);} FOOTER { + grid-column: 1 / 3; + grid-row: 3; clear: both; font-size: 0.7rem; text-align: center; background: var(--title); border: 1px solid var(--title-border); + margin-top: 0; } A { @@ -127,13 +137,18 @@ UL { text-align: left; } +CODE { + background: #BBB; + font-size: 0.9rem; +} + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * the navigation bar, and all its blocks * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ NAV { - width: 200px; - float: left; + grid-column: 1; + grid-row: 2; text-align: center; margin-left: 16px; } @@ -188,9 +203,10 @@ TABLE.tag_list>TBODY>TR>TD:after { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ ARTICLE { - margin-left: 226px; + grid-column: 2; + grid-row: 2; margin-right: 16px; - margin-top: 16px; + margin-top: 0; text-align: center; height: 1%; } @@ -199,6 +215,39 @@ ARTICLE TABLE { margin: auto; } +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* mobile screens * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +@media screen and (width <= 800px) { + BODY { + grid-template-columns: auto; + } + HEADER { + grid-column: 1; + grid-row: 1; + } + ARTICLE { + grid-column: 1; + grid-row: 2; + margin-right: 0; + } + NAV { + grid-column: 1; + grid-row: 3; + margin: auto; + } + FOOTER { + grid-column: 1; + grid-row: 4; + } + #image-list .blockbody { + margin: 0; + } + .shm-image-list { + justify-content: center; + } +} /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * specific page types * diff --git a/themes/default/themelet.class.php b/themes/default/themelet.class.php deleted file mode 100644 index ec8b0f043..000000000 --- a/themes/default/themelet.class.php +++ /dev/null @@ -1,9 +0,0 @@ -get_string(SetupConfig::TITLE); $page->set_title($page_title); - $page->set_heading($page_title); $page->disable_left(); $page->add_block(new Block(null, $this->build_upload_box(), "main", 0)); - $page->add_block(new Block(null, "
                  ", "main", 80)); + $page->add_block(new Block(null, rawHTML("
                  "), "main", 80)); $this->display_paginator($page, "comment/list", null, $page_number, $total_pages); $this->post_page = false; @@ -54,7 +57,7 @@ public function display_comment_list(array $images, int $page_number, int $total $html .= "
                  $comment_html
                  "; $html .= ""; - $page->add_block(new Block(null, $html, "main", $position++)); + $page->add_block(new Block(null, rawHTML($html), "main", $position++)); } } @@ -64,9 +67,9 @@ public function display_recent_comments(array $comments): void parent::display_recent_comments($comments); } - public function build_upload_box(): string + public function build_upload_box(): HTMLElement { - return "[[ insert upload-and-comment extension here ]]"; + return rawHTML("[[ insert upload-and-comment extension here ]]"); } @@ -87,14 +90,14 @@ protected function comment_to_html(Comment $comment, bool $trim = false): string } else { $h_comment = $tfe->formatted; } - $h_comment = preg_replace("/(^|>)(>[^<\n]*)(<|\n|$)/", '${1}${2}${3}', $h_comment); + $h_comment = preg_replace_ex("/(^|>)(>[^<\n]*)(<|\n|$)/", '${1}${2}${3}', $h_comment); // handles discrepency in comment page and homepage $h_comment = str_replace("
                  ", "", $h_comment); $h_comment = str_replace("\n", "
                  ", $h_comment); $i_comment_id = $comment->comment_id; $i_image_id = $comment->image_id; - $h_userlink = "$h_name"; + $h_userlink = "$h_name"; $h_date = $comment->posted; $h_del = ""; if ($user->can(Permissions::DELETE_COMMENT)) { diff --git a/themes/futaba/themelet.class.php b/themes/futaba/common_elements.theme.php similarity index 94% rename from themes/futaba/themelet.class.php rename to themes/futaba/common_elements.theme.php index 1c0f53e6d..081abaa5d 100644 --- a/themes/futaba/themelet.class.php +++ b/themes/futaba/common_elements.theme.php @@ -4,7 +4,9 @@ namespace Shimmie2; -class Themelet extends BaseThemelet +use function MicroHTML\rawHTML; + +class FutabaCommonElementsTheme extends CommonElementsTheme { /** * Add a generic paginator. @@ -15,7 +17,7 @@ public function display_paginator(Page $page, string $base, ?string $query, int $total_pages = 1; } $body = $this->futaba_build_paginator($page_number, $total_pages, $base, $query); - $page->add_block(new Block(null, $body, "main", 90)); + $page->add_block(new Block(null, rawHTML($body), "main", 90)); } /** diff --git a/themes/futaba/page.class.php b/themes/futaba/page.class.php index bfb570407..488bc629f 100644 --- a/themes/futaba/page.class.php +++ b/themes/futaba/page.class.php @@ -4,24 +4,28 @@ namespace Shimmie2; -class Page extends BasePage +use MicroHTML\HTMLElement; + +use function MicroHTML\{DIV, LI, A, rawHTML, emptyHTML, UL, ARTICLE, FOOTER, HR, HEADER, H1, NAV}; + +class FutabaPage extends Page { - public function body_html(): string + public function body_html(): HTMLElement { - $left_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; + $left_block_html = []; + $main_block_html = []; + $sub_block_html = []; foreach ($this->blocks as $block) { switch ($block->section) { case "left": - $left_block_html .= $block->get_html(true); + $left_block_html[] = $this->block_html($block, true); break; case "main": - $main_block_html .= $block->get_html(false); + $main_block_html[] = $this->block_html($block, false); break; case "subheading": - $sub_block_html .= $block->body; + $sub_block_html[] = $block->body; break; default: print "

                  error: {$block->header} using an unknown section ({$block->section})"; @@ -30,37 +34,35 @@ public function body_html(): string } if (empty($this->subheading)) { - $subheading = ""; + $subheading = null; } else { - $subheading = "

                  {$this->subheading}
                  "; + $subheading = DIV(["id" => "subtitle"], $this->subheading); } if ($this->left_enabled) { - $left = ""; - $withleft = "withleft"; + $left = NAV(...$left_block_html); } else { - $left = ""; - $withleft = ""; + $left = null; } - $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $flash_html = $this->flash_html(); $footer_html = $this->footer_html(); - return << -

                  {$this->heading}

                  - $subheading - $sub_block_html - - $left -
                  - $flash_html - $main_block_html -
                  -
                  -
                  - $footer_html -
                  -EOD; + return emptyHTML( + HEADER( + H1($this->heading), + $subheading, + ...$sub_block_html + ), + $left, + ARTICLE( + $flash_html, + ...$main_block_html + ), + FOOTER( + HR(), + $footer_html + ) + ); } } diff --git a/themes/futaba/style.css b/themes/futaba/style.css index 19f127e23..2a1273359 100644 --- a/themes/futaba/style.css +++ b/themes/futaba/style.css @@ -8,13 +8,16 @@ } BODY { + display: grid; + grid-template-columns: 200px auto; + grid-gap: 0 16px; + margin: 0; background: #FFFFEE url(fade.png) top center repeat-x; color: #800000; padding-left: 5px; padding-right: 5px; - margin-right: 0; - margin-left: 0; - margin-top: 5px; + margin: 0; + margin-bottom: 8px; } H1 { text-align: center; @@ -23,7 +26,13 @@ H1 { display: none; } +HEADER { + grid-column: 1 / 3; + grid-row: 1; +} FOOTER { + grid-column: 1 / 3; + grid-row: 3; clear: both; padding-top: 8px; font-size: 0.7rem; @@ -33,14 +42,16 @@ FOOTER { A, A:visited {text-decoration: none; color: #0000EE;} A:hover {text-decoration: underline; color: #DD0000;} HR {border: none; border-top: 1px solid #D9BFB7; height: 0; clear: both;} +CODE {background: #DEDEDE; font-size: 0.9rem;} /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * the navigation bar, and all its blocks * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ NAV { + grid-column: 1; + grid-row: 2; width: 200px; - float: left; text-align: center; margin-left: 16px; } @@ -64,9 +75,6 @@ NAV SELECT { NAV H3 { text-align: center; } -.withleft { - margin-left: 160px; -} #paginator .blockbody { background: none; @@ -79,9 +87,11 @@ NAV H3 { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ ARTICLE { - margin-left: 226px; + grid-column: 2; + grid-row: 2; + margin-left: 0; margin-right: 16px; - margin-top: 16px; + margin-top: 0; text-align: center; height: 1%; } @@ -89,6 +99,9 @@ ARTICLE TABLE { width: 90%; margin: auto; } +ARTICLE SECTION H3 { + margin-top: 0; +} /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * the navigation bar, and all its blocks * @@ -109,6 +122,41 @@ TABLE.tag_list>TBODY>TR>TD:after { content: " "; } +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* mobile screens * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +@media screen and (width <= 800px) { + BODY { + grid-template-columns: auto; + } + HEADER { + grid-column: 1; + grid-row: 1; + } + ARTICLE { + grid-column: 1; + grid-row: 2; + margin-right: 0; + } + NAV { + grid-column: 1; + grid-row: 3; + margin: auto; + width: auto; + } + FOOTER { + grid-column: 1; + grid-row: 4; + } + #image-list .blockbody { + margin: 0; + } + .shm-image-list { + justify-content: center; + } +} + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * specific page types * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ diff --git a/themes/futaba/view.theme.php b/themes/futaba/view.theme.php index c983f98bd..f2e1b18c1 100644 --- a/themes/futaba/view.theme.php +++ b/themes/futaba/view.theme.php @@ -6,7 +6,7 @@ use MicroHTML\HTMLElement; -class CustomViewPostTheme extends ViewPostTheme +class FutabaViewPostTheme extends ViewPostTheme { /** * @param HTMLElement[] $editor_parts diff --git a/themes/lite/comment.theme.php b/themes/lite/comment.theme.php index 3be430a8d..a73c787d6 100644 --- a/themes/lite/comment.theme.php +++ b/themes/lite/comment.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomCommentListTheme extends CommentListTheme +class LiteCommentListTheme extends CommentListTheme { protected function comment_to_html(Comment $comment, bool $trim = false): string { diff --git a/themes/lite/themelet.class.php b/themes/lite/common_elements.theme.php similarity index 98% rename from themes/lite/themelet.class.php rename to themes/lite/common_elements.theme.php index 1717e769f..c3208ad2f 100644 --- a/themes/lite/themelet.class.php +++ b/themes/lite/common_elements.theme.php @@ -8,7 +8,7 @@ use function MicroHTML\{A,DIV,SPAN,joinHTML}; -class Themelet extends BaseThemelet +class LiteCommonElementsTheme extends CommonElementsTheme { public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false): void { diff --git a/themes/lite/page.class.php b/themes/lite/page.class.php index 9f2ef8bf4..60b8e5410 100644 --- a/themes/lite/page.class.php +++ b/themes/lite/page.class.php @@ -6,6 +6,8 @@ use MicroHTML\HTMLElement; +use function MicroHTML\{emptyHTML, HEADER, FOOTER, DIV, SCRIPT, A, B, IMG, NAV, ARTICLE, rawHTML, SECTION}; + /** * Name: Lite Theme * Author: Zach Hall @@ -15,9 +17,9 @@ * some other sites, packaged in a light blue color. */ -class Page extends BasePage +class LitePage extends Page { - public function body_html(): string + public function body_html(): HTMLElement { global $config; @@ -26,36 +28,45 @@ public function body_html(): string $site_name = $config->get_string(SetupConfig::TITLE); $data_href = get_base_href(); - $menu = ""; + $menu->appendChild($custom_links); - $left_block_html = ""; - $main_block_html = ""; - $sub_block_html = ""; - $user_block_html = ""; + $left_block_html = []; + $main_block_html = []; + $sub_block_html = []; + $user_block_html = []; foreach ($this->blocks as $block) { switch ($block->section) { case "left": - $left_block_html .= $this->block_to_html($block, true); + $left_block_html[] = $this->block_html($block, true); break; case "main": - $main_block_html .= $this->block_to_html($block, false); + $main_block_html[] = $this->block_html($block, false); break; case "user": - $user_block_html .= $block->body; + $user_block_html[] = $block->body; break; case "subheading": - $sub_block_html .= $this->block_to_html($block, false); + $sub_block_html[] = $this->block_html($block, false); break; default: print "

                  error: {$block->header} using an unknown section ({$block->section})"; @@ -63,69 +74,58 @@ public function body_html(): string } } - $custom_sublinks = ""; + $custom_sublinks = null; if (!empty($sub_links)) { - $custom_sublinks = "

                  "; + $custom_sublinks = DIV(["class" => "sbar"]); foreach ($sub_links as $nav_link) { - $custom_sublinks .= $this->navlinks($nav_link->link, $nav_link->description, $nav_link->active); + $custom_sublinks->appendChild($this->navlinks($nav_link->link, $nav_link->description, $nav_link->active)); } - $custom_sublinks .= "
                  "; } - $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $flash_html = $this->flash_html(); if (!$this->left_enabled) { - $left_block_html = ""; - $main_block_html = "
                  {$main_block_html}
                  "; + $left_block_el = emptyHTML(); + $main_block_el = ARTICLE(["id" => "body_noleft"], ...$main_block_html); } else { - $left_block_html = ""; - $main_block_html = "
                  $flash_html{$main_block_html}
                  "; + $left_block_el = NAV(...$left_block_html); + $main_block_el = ARTICLE($flash_html, ...$main_block_html); } $footer_html = $this->footer_html(); - return << - $menu - $custom_sublinks - $sub_block_html - - $left_block_html - $main_block_html -
                  - $footer_html -
                  -EOD; + return emptyHTML( + HEADER( + $menu, + $custom_sublinks, + ...$sub_block_html + ), + $left_block_el, + $main_block_el, + FOOTER($footer_html) + ); } /* end of function display_page() */ - public function block_to_html(Block $block, bool $hidable = false): string + protected function block_html(Block $block, bool $hidable = false): HTMLElement { $h = $block->header; - $b = $block->body; $i = $block->id; - $html = $b; - if ($h != "Paginator") { - $html = "
                  "; - if (!is_null($h)) { - $html .= ""; - } - if (!is_null($b)) { - $html .= ""; - } - $html .= "
                  "; + if ($h == "Paginator") { + return $block->body; + } + $html = SECTION(["id" => $i]); + if (!is_null($block->header)) { + $html->appendChild(DIV(["class" => "navtop navside tab shm-toggler", "data-toggle-sel" => "#{$i}"], $block->header)); } + $html->appendChild(DIV(["class" => "navside tab".($hidable ? " blockbody" : "")], $block->body)); return $html; } - public function navlinks(Link $link, HTMLElement|string $desc, bool $active): ?string + public function navlinks(Link $link, HTMLElement|string $desc, bool $active): HTMLElement { - $html = null; - if ($active) { - $html = "{$desc}"; - } else { - $html = "{$desc}"; - } - - return $html; + return A([ + "class" => $active ? "tab-selected" : "tab", + "href" => $link->make_link(), + ], $desc); } } diff --git a/themes/lite/setup.theme.php b/themes/lite/setup.theme.php index b1e90085b..1c567cb80 100644 --- a/themes/lite/setup.theme.php +++ b/themes/lite/setup.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomSetupTheme extends SetupTheme +class LiteSetupTheme extends SetupTheme { protected function sb_to_html(SetupBlock $block): string { diff --git a/themes/lite/style.css b/themes/lite/style.css index 03cb7f5ba..293a33d6a 100644 --- a/themes/lite/style.css +++ b/themes/lite/style.css @@ -8,6 +8,9 @@ font-size: 14px; } BODY { + display: grid; + grid-template-columns: 210px auto; + grid-gap: 0 16px; background: #F0F7FF; margin: 0; } @@ -30,6 +33,9 @@ a.tab:hover, a.tab:active, .tab-selected { background-color:#FFFFFE; text-decoration:none; } +.bar .tab, .bar .tab-selected { + display: inline-block; +} .tab, .tab-selected, .tframe, #tips { -moz-border-radius:4px; -webkit-border-radius:4px; @@ -147,6 +153,8 @@ INPUT:hover, button:hover, TEXTAREA:hover { } FOOTER { + grid-column: 1 / 3; + grid-row: 3; clear: both; padding: 8px; font-size: 0.7rem; @@ -155,6 +163,11 @@ FOOTER { background: #E3EFFA; } +HEADER { + grid-column: 1 / 3; + grid-row: 1; +} + A {text-decoration: none;} A:hover {text-decoration: underline;} @@ -173,8 +186,9 @@ UL { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ NAV { + grid-column: 1; + grid-row: 2; width: 200px; - float: left; text-align: center; margin-left: 16px; } @@ -257,7 +271,8 @@ TABLE.tag_list>TBODY>TR>TD:after { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ ARTICLE { - margin-left: 226px; + grid-column: 2; + grid-row: 2; margin-right: 16px; text-align: center; height: 1%; @@ -275,6 +290,38 @@ ARTICLE TABLE { width: 90%; } +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* mobile screens * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +@media screen and (width <= 800px) { + BODY { + grid-template-columns: auto; + } + HEADER { + grid-column: 1; + grid-row: 1; + } + ARTICLE { + grid-column: 1; + grid-row: 2; + margin: 0 16px; + } + NAV { + grid-column: 1; + grid-row: 3; + margin: auto; + width: auto; + } + FOOTER { + grid-column: 1; + grid-row: 4; + } + .shm-image-list { + justify-content: center; + } +} + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * specific page types * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ diff --git a/themes/lite/user.theme.php b/themes/lite/user.theme.php index 8a422abff..f025bcf50 100644 --- a/themes/lite/user.theme.php +++ b/themes/lite/user.theme.php @@ -4,13 +4,14 @@ namespace Shimmie2; -class CustomUserPageTheme extends UserPageTheme +use function MicroHTML\rawHTML; + +class LiteUserPageTheme extends UserPageTheme { public function display_login_page(Page $page): void { global $config; $page->set_title("Login"); - $page->set_heading("Login"); $page->disable_left(); $html = "
                  @@ -30,7 +31,7 @@ public function display_login_page(Page $page): void if ($config->get_bool("login_signup_enabled")) { $html .= "Create Account"; } - $page->add_block(new Block("Login", $html, "main", 90)); + $page->add_block(new Block("Login", rawHTML($html), "main", 90)); } /** @@ -58,7 +59,7 @@ public function display_user_block(Page $page, User $user, array $parts): void } $html .= "{$part["name"]}"; } - $b = new Block("User Links", $html, "user", 90); + $b = new Block("User Links", rawHTML($html), "user", 90); $b->is_content = false; $page->add_block($b); } @@ -88,7 +89,7 @@ public function display_ip_list(Page $page, array $uploads, array $comments, arr $html .= ""; $html .= "(Most recent at top)"; - $page->add_block(new Block("IPs", $html)); + $page->add_block(new Block("IPs", rawHTML($html))); } /** diff --git a/themes/lite/user_config.theme.php b/themes/lite/user_config.theme.php index 46d748184..058a3b781 100644 --- a/themes/lite/user_config.theme.php +++ b/themes/lite/user_config.theme.php @@ -4,7 +4,7 @@ namespace Shimmie2; -class CustomUserConfigTheme extends UserConfigTheme +class LiteUserConfigTheme extends UserConfigTheme { protected function sb_to_html(SetupBlock $block): string { diff --git a/themes/lite/view.theme.php b/themes/lite/view.theme.php index 467477ea0..c4da12af3 100644 --- a/themes/lite/view.theme.php +++ b/themes/lite/view.theme.php @@ -6,7 +6,9 @@ use MicroHTML\HTMLElement; -class CustomViewPostTheme extends ViewPostTheme +use function MicroHTML\rawHTML; + +class LiteViewPostTheme extends ViewPostTheme { /** * @param HTMLElement[] $editor_parts @@ -22,7 +24,7 @@ public function display_page(Image $image, array $editor_parts): void $page->add_block(new Block(null, $this->build_pin($image), "main", 11)); } - private function build_stats(Image $image): string + private function build_stats(Image $image): HTMLElement { $h_owner = html_escape($image->get_owner()->name); $h_ownerlink = "$h_owner"; @@ -58,13 +60,14 @@ private function build_stats(Image $image): string } if (Extension::is_enabled(RatingsInfo::KEY)) { - if ($image['rating'] === null || $image['rating'] == "?") { - $image['rating'] = "?"; + $rating = $image['rating']; + if ($rating === null) { + $rating = "?"; } - $h_rating = Ratings::rating_to_human($image['rating']); - $html .= "
                  Rating: $h_rating"; + $h_rating = Ratings::rating_to_human($rating); + $html .= "
                  Rating: $h_rating"; } - return $html; + return rawHTML($html); } } diff --git a/themes/warm/page.class.php b/themes/warm/page.class.php index 7e38aa5e7..8e921065b 100644 --- a/themes/warm/page.class.php +++ b/themes/warm/page.class.php @@ -4,9 +4,13 @@ namespace Shimmie2; -class Page extends BasePage +use MicroHTML\HTMLElement; + +use function MicroHTML\{A, TABLE, TR, TD, SMALL, rawHTML, emptyHTML, DIV, ARTICLE, FOOTER, HEADER, H1, NAV}; + +class WarmPage extends Page { - public function body_html(): string + public function body_html(): HTMLElement { global $config; @@ -14,24 +18,24 @@ public function body_html(): string $data_href = get_base_href(); $main_page = $config->get_string(SetupConfig::MAIN_PAGE); - $left_block_html = ""; - $main_block_html = ""; - $head_block_html = ""; - $sub_block_html = ""; + $left_block_html = []; + $main_block_html = []; + $head_block_html = []; + $sub_block_html = []; foreach ($this->blocks as $block) { switch ($block->section) { case "left": - $left_block_html .= $block->get_html(true); + $left_block_html[] = $this->block_html($block, true); break; case "head": - $head_block_html .= "".$block->get_html(false).""; + $head_block_html[] = TD(["style" => "width: 250px;"], SMALL($this->block_html($block, false))); break; case "main": - $main_block_html .= $block->get_html(false); + $main_block_html[] = $this->block_html($block, false); break; case "subheading": - $sub_block_html .= $block->body; + $sub_block_html[] = $block->body; break; default: print "

                  error: {$block->header} using an unknown section ({$block->section})"; @@ -39,32 +43,25 @@ public function body_html(): string } } - $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : ""; + $flash_html = $this->flash_html(); $footer_html = $this->footer_html(); - return << - - - - $head_block_html - - - $sub_block_html - -

                  -
                  - $flash_html - $main_block_html -
                  -
                  - $footer_html -
                  -EOD; + return emptyHTML( + HEADER( + DIV( + ["style" => "text-align: center;"], + H1(A(["href" => "$data_href/$main_page"], $site_name)) + // Navigation links go here + ), + ...$head_block_html, + ...$sub_block_html + ), + NAV(...$left_block_html), + ARTICLE( + $flash_html, + ...$main_block_html + ), + FOOTER($footer_html) + ); } } diff --git a/themes/warm/style.css b/themes/warm/style.css index ae5e0c175..e13bb6ff6 100644 --- a/themes/warm/style.css +++ b/themes/warm/style.css @@ -8,13 +8,21 @@ font-size: 14px; } BODY { + display: grid; + grid-template-columns: 250px auto; + grid-gap: 16px; + margin: 0; background: url(bg.png); margin: 0; } HEADER { + display: grid; + grid-template-columns: auto 250px 250px; + grid-column: 1 / 3; + grid-row: 1; border-bottom: 1px solid #B89F7C; margin-top: 0; - margin-bottom: 16px; + margin-bottom: 0; padding: 8px; background: #FCD9A9; text-align: center; @@ -52,6 +60,8 @@ TABLE.zebra TR:nth-child(odd) {background: #FCD9A9;} TABLE.zebra TR:nth-child(even) {background: #DABC92;} FOOTER { + grid-column: 1 / 3; + grid-row: 3; clear: both; padding: 8px; font-size: 0.7rem; @@ -85,8 +95,9 @@ SECTION>H3 {background: #DABC92;} * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ NAV { + grid-column: 1; + grid-row: 2; width: 250px; - float: left; text-align: center; margin-left: 16px; } @@ -148,7 +159,8 @@ TABLE.tag_list>TBODY>TR>TD:after { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ ARTICLE { - margin-left: 276px; + grid-column: 2; + grid-row: 2; margin-right: 16px; text-align: center; height: 1%; @@ -159,6 +171,46 @@ ARTICLE TABLE { } +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* mobile screens * +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +@media screen and (width <= 800px) { + BODY { + grid-template-columns: auto; + } + HEADER { + display: revert; + grid-column: 1; + grid-row: 1; + } + HEADER SPAN { + display: inline-block; + } + ARTICLE { + grid-column: 1; + grid-row: 2; + margin: 0 16px; + } + NAV { + grid-column: 1; + grid-row: 3; + margin: auto; + width: auto; + } + FOOTER { + grid-column: 1; + grid-row: 4; + } + #image-list .blockbody { + margin: 0; + } + .shm-image-list { + justify-content: center; + } +} + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * specific page types * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ diff --git a/themes/warm/themelet.class.php b/themes/warm/themelet.class.php deleted file mode 100644 index ec8b0f043..000000000 --- a/themes/warm/themelet.class.php +++ /dev/null @@ -1,9 +0,0 @@ -add_block(new Block("Upload", "Disk nearly full, uploads disabled", "head", 20)); + $page->add_block(new Block("Upload", rawHTML("Disk nearly full, uploads disabled"), "head", 20)); } } diff --git a/themes/warm/user.theme.php b/themes/warm/user.theme.php index c52398769..f4cb59970 100644 --- a/themes/warm/user.theme.php +++ b/themes/warm/user.theme.php @@ -4,7 +4,9 @@ namespace Shimmie2; -class CustomUserPageTheme extends UserPageTheme +use function MicroHTML\rawHTML; + +class WarmUserPageTheme extends UserPageTheme { /** * @param array $parts @@ -16,7 +18,7 @@ public function display_user_block(Page $page, User $user, array $parts): void foreach ($parts as $part) { $html .= "{$part["name"]} | "; } - $page->add_block(new Block("Logged in as $h_name", $html, "head", 90)); + $page->add_block(new Block("Logged in as $h_name", rawHTML($html), "head", 90)); } public function display_login_block(Page $page): void @@ -34,6 +36,6 @@ public function display_login_block(Page $page): void 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", rawHTML($html), "head", 90)); } }