From 6612006f009c61b16985fdb89e6e53d65e186ebf Mon Sep 17 00:00:00 2001 From: theWituch Date: Tue, 27 Oct 2026 10:19:45 +0100 Subject: [PATCH 1/2] Nginx backend instead Apache --- .dockerignore | 13 + .gitattributes | 1 + .github/workflows/docker-image.yml | 38 +- .gitignore | 1 - .htaccess | 22 - Dockerfile | 81 ++-- README.md | 132 ++++- composer.lock | 752 +++++++++++++++++++++++++++++ config/config.yml | 2 +- docker/debian.sources.list | 4 + docker/healthcheck.sh | 7 + docker/nginx.conf | 53 ++ docker/php-fpm.conf | 8 + docker/php.ini | 3 + docker/start.sh | 40 ++ index.php | 2 +- tests/container_test.py | 257 ++++++++++ 17 files changed, 1332 insertions(+), 84 deletions(-) create mode 100644 .dockerignore delete mode 100644 .htaccess create mode 100644 composer.lock create mode 100644 docker/debian.sources.list create mode 100644 docker/healthcheck.sh create mode 100644 docker/nginx.conf create mode 100644 docker/php-fpm.conf create mode 100644 docker/php.ini create mode 100644 docker/start.sh create mode 100644 tests/container_test.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1aa5dba --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +.agents +.codex +.aws +vendor +composer.phar +tests +README.md +.htaccess +*.log +Thumbs.db +.DS_Store diff --git a/.gitattributes b/.gitattributes index d163e51..032af6e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ /.build export-ignore +*.sh text eol=lf /.github export-ignore /assets/.gitignore export-ignore /config/.gitignore export-ignore diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 82b2457..cd1de5a 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,6 +9,8 @@ on: jobs: docker: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout @@ -18,21 +20,37 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: name/app + images: minecraftplus/website tags: | - type=raw,value={{date 'YYYYMMDD'}} + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value={{date 'YYYYMMDD-HHmmss'}} + - + name: Build candidate + uses: docker/build-push-action@v3 + with: + context: . + load: true + push: false + tags: | + ${{ steps.meta.outputs.tags }} + minecraftplus/website:ci + labels: ${{ steps.meta.outputs.labels }} + - + name: Test candidate + run: python3 -u tests/container_test.py minecraftplus/website:ci - name: Login to DockerHub - if: github.event_name != 'pull_request' + if: github.event_name == 'push' && github.ref == 'refs/heads/master' uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v3 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + name: Push tested image + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + env: + IMAGE_TAGS: ${{ steps.meta.outputs.tags }} + run: | + while IFS= read -r tag; do + if [ -n "$tag" ]; then docker push "$tag"; fi + done <<< "$IMAGE_TAGS" diff --git a/.gitignore b/.gitignore index fa8ae7a..950955b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,3 @@ desktop.ini # Composer /composer.phar /vendor -composer.lock diff --git a/.htaccess b/.htaccess deleted file mode 100644 index 5b2d88b..0000000 --- a/.htaccess +++ /dev/null @@ -1,22 +0,0 @@ - - RewriteEngine On - # May be required to access sub directories - #RewriteBase / - - # Deny access to internal dirs and files by passing the URL to Pico - RewriteRule ^(config|content|vendor|CHANGELOG\.md|composer\.(json|lock|phar))(/|$) index.php [L] - RewriteRule (^\.|/\.)(?!well-known(/|$)) index.php [L] - - # Enable URL rewriting - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^ index.php [L] - - - # Let Pico know about available URL rewriting - SetEnv PICO_URL_REWRITING 1 - - - -# Prevent file browsing -Options -Indexes -MultiViews diff --git a/Dockerfile b/Dockerfile index 76b10c7..3bc2808 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,54 @@ -# Use the official PHP Apache base image -FROM php:7.4-apache +# Keep PHP unchanged while migrating the web server; upgrade it separately. +FROM php:7.4-fpm-bullseye@sha256:3ac7c8c74b2b047c7cb273469d74fc0d59b857aa44043e6ea6a0084372811d5b AS base + +# Bullseye security packages have moved off the live security mirror. +COPY docker/debian.sources.list /etc/apt/sources.list + +FROM base AS dependencies + +RUN apt-get update \ + && apt-get install -y --no-install-recommends unzip \ + && rm -rf /var/lib/apt/lists/* + +# Keep the Composer version used to create the Apache reference image. +RUN curl -fsS https://getcomposer.org/installer -o /tmp/composer-setup.php \ + && php /tmp/composer-setup.php --version=2.1.14 --install-dir=/usr/local/bin --filename=composer \ + && rm /tmp/composer-setup.php + +WORKDIR /var/www/html +COPY composer.json composer.lock ./ +RUN composer install --no-dev --prefer-dist --no-interaction --no-progress \ + && composer check-platform-reqs --no-dev + +FROM base AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends nginx-light tini \ + && rm -rf /var/lib/apt/lists/* \ + && rm /etc/nginx/sites-enabled/default + +WORKDIR /var/www/html +COPY --from=dependencies /var/www/html/ ./ +# Repository plugins intentionally override the versions installed by Composer. +COPY plugins/ ./plugins/ +COPY themes/ ./themes/ +COPY config/ ./config/ +COPY content/ ./content/ +COPY language/ ./language/ +COPY assets/ ./assets/ +COPY index.php favicon.ico ./ + +COPY docker/nginx.conf /etc/nginx/sites-enabled/website.conf +COPY docker/php-fpm.conf /usr/local/etc/php-fpm.d/zz-website.conf +COPY docker/php.ini /usr/local/etc/php/conf.d/website.ini +COPY docker/start.sh docker/healthcheck.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/start.sh /usr/local/bin/healthcheck.sh \ + && php -r 'exit(extension_loaded("dom") && extension_loaded("mbstring") ? 0 : 1);' \ + && nginx -t \ + && php-fpm -t -# Update the package list and install system tools -RUN apt-get update && apt-get install -y \ - unzip \ - libzip-dev - -# Enable the Apache rewritemod module -RUN a2enmod rewrite - -# Install Composer -RUN curl -sS https://getcomposer.org/installer | php -- --version=2.1.14 --install-dir=/usr/local/bin --filename=composer - -# Copy composer file to the /var/www/html directory -COPY composer.json /var/www/html/ - -# Install Composer dependencies -RUN composer install - -# Copy project files to the /var/www/html directory -COPY . /var/www/html - -# Ensure that the owner of files in the /var/www/html directory is www-data -RUN chown -R www-data:www-data /var/www/html - -# Port on which Apache will run EXPOSE 80 - -# Start command -CMD ["apache2-foreground"] +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["/usr/local/bin/healthcheck.sh"] +STOPSIGNAL SIGTERM +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["/usr/local/bin/start.sh"] diff --git a/README.md b/README.md index 39af52c..01303d5 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,121 @@ -# README # +# MinecraftPlus website -This README would normally document whatever steps are necessary to get your application up and running. +The MinecraftPlus project website uses Pico CMS 2.1, Markdown content in English +and Polish, and the `mcplus` Twig theme. It needs no database or administration +backend. -### What is this repository for? ### +## Build and run -* Quick summary -* Version -* [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo) +Requires Docker with Linux containers. Build from the repository root: -### How do I get set up? ### +```sh +docker build -t mcplus-website:local . +docker run -d --name mcplus-website -p 127.0.0.1:8080:80 mcplus-website:local +``` -* Summary of set up -* Configuration -* Dependencies -* Database configuration -* How to run tests -* Deployment instructions +Visit or . The root URL +redirects according to `Accept-Language`, falling back to English. -### Contribution guidelines ### +One container runs nginx and PHP-FPM, managed by a Bash wrapper under `tini`. +Only HTTP port 80 needs publishing. PHP-FPM listens on the container's loopback +address, `127.0.0.1:9000`; do not publish port 9000. The inherited PHP image may +still list that unused external port in image metadata. -* Writing tests -* Code review -* Other guidelines +`composer.lock` preserves the dependencies extracted from the previous Apache +image, `minecraftplus/website@sha256:ad08f5461d974438eb6ae4c79b712090f45bdb06614fae5240dab10c18b702f8`. +The build uses Composer 2.1.14, installs locked dependencies first, and then +overlays the repository's modified plugins and theme. Keep that order when +editing the Dockerfile. PHP `dom` and `mbstring` are checked during the build. +No dependencies are installed at container startup. -### Who do I talk to? ### +## HTTP and reverse proxy configuration -* Repo owner or admin -* Other community or team contact \ No newline at end of file +TLS remains on the existing external reverse proxy. Forward requests to +container port 80, preserving the public `Host` header, including a nonstandard +public port if one is used. The proxy must **overwrite** client-supplied +`X-Forwarded-Proto` and `X-Forwarded-Port` with the public scheme and port. +Restrict production access to the container to that trusted proxy: the PHP entry +point trusts these headers. Configure the proxy to preserve the original public +path; the application is served at the domain root. + +nginx preserves the original request URI and query string when routing pages to +`index.php`. Pico has `rewrite_url: true`; generated links use paths such as +`/en/status`, while older `/?en` URLs continue to work. Automatic nginx directory +redirects use relative locations so they preserve the public scheme and port. + +Only the root `index.php` is executable through FastCGI. Internal configuration, +content, dependencies, language sources, dotfiles and source templates are routed +to Pico's 404 page before any static-file handling. Static images, scripts, +styles, fonts and videos remain accessible, including plugin stylesheets. Directory +listing is disabled. The `.well-known` exception permits static challenge files; +certificate provisioning remains the external proxy's responsibility. + +Server configuration and startup scripts live in `docker/` and are copied outside +the document root. Apache and `.htaccess` are no longer used. There is no FastCGI +response cache, so language negotiation retains its existing behavior. + +## Tests and operations + +Requires Python 3 and access to Docker; no Python packages need installing: + +```sh +python tests/container_test.py mcplus-website:local +``` + +The suite starts isolated containers on temporary loopback ports and removes +them afterwards. It checks page rendering, language redirects, legacy URLs, +query strings, canonical/alternate links behind HTTPS proxies (including a custom +port), 404s, static-file bytes and MIME types, video range requests, protection of +internal files and arbitrary PHP scripts, plugin overrides, application health, +process failure handling and graceful shutdown. + +To compare HTML responses with an Apache reference built from the same application +source and lock file: + +```sh +python tests/container_test.py mcplus-website:local --reference mcplus-website:apache-reference +``` + +The migration was compared against a local Apache reference created by overlaying +the pre-migration working tree on the previous image above. An unrelated or older +application revision is unsuitable for byte-for-byte comparisons. + +GitHub Actions builds and loads a candidate image, runs the suite, and publishes +the **same image** only after tests pass on a push to `master`. Pull requests only +build and test. Images are published to `minecraftplus/website` with `latest` +and timestamp tags (`YYYYMMDD-HHmmss`). + +```sh +docker logs --follow mcplus-website +docker inspect --format '{{json .State.Health}}' mcplus-website +docker exec mcplus-website /usr/local/bin/healthcheck.sh +docker stop --time 10 mcplus-website +``` + +Health checks render `/en` through nginx and PHP-FPM every 30 seconds, with a +20-second startup grace period and three retries. Logs go to stdout/stderr. If +either server exits unexpectedly, the wrapper stops its peer and exits with an +error. Docker `stop` gracefully stops both servers within the default timeout. +An unhealthy status alone does not restart a container; use the deployment +platform's health monitoring and restart policy. + +## Cutover and rollback + +Before replacing an existing deployment, record its exact image digest or retain +its timestamp tag. Keep its network, mounts, environment and restart settings. +Run the nginx candidate alongside it on a separate internal port, execute the +test suite and check the real proxy's redirects, canonical URLs, assets and logs. +Then switch the proxy upstream to the candidate. Monitor health, nginx/PHP errors +and unexpected 4xx/5xx responses. Retain the previous container until validation +finishes. + +For rollback, point the proxy back to the retained Apache container or recreate +it from its saved image digest and original settings. No database or content +migration is involved. Use an immutable digest for rollback: a mutable tag +such as `latest` can refer to nginx after publication. + +PHP remains 7.4.33 to isolate the server migration. Its base image is pinned by +digest. Bullseye security packages now come from Debian's official archive; +archive metadata expiry is disabled for that source only, while signature +verification remains enabled. PHP 7.4 is unsupported: upgrading PHP, Pico/Twig +compatibility and the old Composer installer is a separate follow-up. diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..3b33452 --- /dev/null +++ b/composer.lock @@ -0,0 +1,752 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ab613beed761671676e908fbd291d1be", + "packages": [ + { + "name": "erusev/parsedown", + "version": "1.8.0-beta-7", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "fe7a50eceb4a3c867cc9fa9c0aa906b1067d1955" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/fe7a50eceb4a3c867cc9fa9c0aa906b1067d1955", + "reference": "fe7a50eceb4a3c867cc9fa9c0aa906b1067d1955", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.8.0-beta-7" + }, + "time": "2019-03-17T18:47:21+00:00" + }, + { + "name": "erusev/parsedown-extra", + "version": "0.8.0-beta-1", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown-extra.git", + "reference": "e756b1bf8642ab1091403e902b0503f1cec7527d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/e756b1bf8642ab1091403e902b0503f1cec7527d", + "reference": "e756b1bf8642ab1091403e902b0503f1cec7527d", + "shasum": "" + }, + "require": { + "erusev/parsedown": "^1.8.0|^1.8.0-beta-4", + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=5.3.6" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "ParsedownExtra": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "An extension of Parsedown that adds support for Markdown Extra.", + "homepage": "https://github.com/erusev/parsedown-extra", + "keywords": [ + "markdown", + "markdown extra", + "parsedown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown-extra/issues", + "source": "https://github.com/erusev/parsedown-extra/tree/master" + }, + "time": "2018-05-08T21:54:32+00:00" + }, + { + "name": "iridescent-dev/pico-multilanguage-plugin", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/iridescent-dev/pico-multilanguage-plugin.git", + "reference": "94a22ba1a4496114101247aec5ec55525d1b0c51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/iridescent-dev/pico-multilanguage-plugin/zipball/94a22ba1a4496114101247aec5ec55525d1b0c51", + "reference": "94a22ba1a4496114101247aec5ec55525d1b0c51", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "picocms/pico": "^2.1" + }, + "type": "pico-plugin", + "extra": { + "installer-name": "MultiLanguage" + }, + "autoload": { + "classmap": [ + "MultiLanguage.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Véronique Seguela", + "email": "veronique@iridescent.dev", + "role": "Developer" + }, + { + "name": "Richard Martin-Nielsen", + "email": "richard.martin@utoronto.ca", + "role": "Developer" + }, + { + "name": "Til Boerner", + "email": "tilmanboerner@gmx.net", + "role": "Developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/iridescent-dev/pico-multilanguage-plugin/graphs/contributors" + } + ], + "description": "A plugin to support content and theme in different languages.", + "homepage": "https://github.com/iridescent-dev/pico-multilanguage-plugin", + "keywords": [ + "iridescent", + "pico", + "pico-multilanguage-plugin", + "picocms", + "picocms-plugin" + ], + "support": { + "docs": "https://github.com/iridescent-dev/pico-multilanguage-plugin#readme", + "issues": "https://github.com/iridescent-dev/pico-multilanguage-plugin/issues", + "source": "https://github.com/iridescent-dev/pico-multilanguage-plugin" + }, + "time": "2021-10-29T14:29:09+00:00" + }, + { + "name": "iridescent-dev/pico-toc-plugin", + "version": "2.1", + "source": { + "type": "git", + "url": "https://github.com/iridescent-dev/pico-toc-plugin.git", + "reference": "79fed34f87efbcb8b93e7672850af9497e305801" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/iridescent-dev/pico-toc-plugin/zipball/79fed34f87efbcb8b93e7672850af9497e305801", + "reference": "79fed34f87efbcb8b93e7672850af9497e305801", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "picocms/pico": "^2.1" + }, + "type": "pico-plugin", + "extra": { + "installer-name": "TableOfContents" + }, + "autoload": { + "classmap": [ + "TableOfContents.php", + "style.css" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Véronique Seguela", + "email": "veronique@iridescent.dev", + "role": "Lead Developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/iridescent-dev/pico-toc-plugin/graphs/contributors" + } + ], + "description": "A Table Of Contents plugin for Pico CMS.", + "homepage": "https://github.com/iridescent-dev/pico-toc-plugin", + "keywords": [ + "iridescent", + "pico", + "pico-toc-plugin", + "picocms", + "picocms-plugin" + ], + "support": { + "docs": "https://github.com/iridescent-dev/pico-toc-plugin#readme", + "issues": "https://github.com/iridescent-dev/pico-toc-plugin/issues", + "source": "https://github.com/iridescent-dev/pico-toc-plugin" + }, + "time": "2022-01-18T17:13:12+00:00" + }, + { + "name": "picocms/composer-installer", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/picocms/composer-installer.git", + "reference": "6b5036c83aa091ed76e2a76ed9335885f95a7db7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/picocms/composer-installer/zipball/6b5036c83aa091ed76e2a76ed9335885f95a7db7", + "reference": "6b5036c83aa091ed76e2a76ed9335885f95a7db7", + "shasum": "" + }, + "type": "composer-installer", + "extra": { + "class": "Pico\\Composer\\Installer\\PluginInstaller" + }, + "autoload": { + "psr-4": { + "Pico\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Rudolf", + "email": "picocms.org@daniel-rudolf.de", + "role": "Lead Developer" + }, + { + "name": "The Pico Community", + "homepage": "http://picocms.org/" + }, + { + "name": "Contributors", + "homepage": "https://github.com/picocms/composer-installer/graphs/contributors" + } + ], + "description": "A composer plugin responsible for installing plugins and themes for Pico, a stupidly simple, blazing fast, flat file CMS.", + "homepage": "http://picocms.org/", + "keywords": [ + "composer", + "composer-installer", + "composer-plugin", + "pico", + "pico-cms", + "picocms", + "picocms-installer", + "picocms-plugin", + "picocms-theme" + ], + "support": { + "issues": "https://github.com/picocms/composer-installer/issues", + "source": "https://github.com/picocms/composer-installer" + }, + "time": "2019-11-24T22:50:47+00:00" + }, + { + "name": "picocms/pico", + "version": "v2.1.4", + "source": { + "type": "git", + "url": "https://github.com/picocms/Pico.git", + "reference": "7228129cade3f812f22904b503c939b04a75c9dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/picocms/Pico/zipball/7228129cade3f812f22904b503c939b04a75c9dd", + "reference": "7228129cade3f812f22904b503c939b04a75c9dd", + "shasum": "" + }, + "require": { + "erusev/parsedown": "1.8.0-beta-7", + "erusev/parsedown-extra": "0.8.0-beta-1", + "ext-mbstring": "*", + "php": ">=5.3.6", + "symfony/yaml": "^2.8", + "twig/twig": "^1.36" + }, + "suggest": { + "picocms/composer-installer": "This Composer plugin is responsible for installing Pico plugins and themes using the Composer package manager.", + "picocms/pico-deprecated": "PicoDeprecated's purpose is to maintain backward compatibility to older versions of Pico.", + "picocms/pico-theme": "Pico requires a theme to actually display the contents of your website. This is Pico's official default theme." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-pico-3.0": "3.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pico": "lib/", + "AbstractPicoPlugin": "lib/", + "PicoPluginInterface": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gilbert Pellegrom", + "email": "gilbert@pellegrom.me", + "role": "Project Founder" + }, + { + "name": "Daniel Rudolf", + "email": "picocms.org@daniel-rudolf.de", + "role": "Lead Developer" + }, + { + "name": "The Pico Community", + "homepage": "http://picocms.org/" + }, + { + "name": "Contributors", + "homepage": "https://github.com/picocms/Pico/graphs/contributors" + } + ], + "description": "Pico is a flat file CMS, this means there is no administration backend and database to deal with. You simply create .md files in the \"content\" folder and that becomes a page.", + "homepage": "http://picocms.org/", + "keywords": [ + "Simple", + "cms", + "content-management", + "flat-file", + "markdown", + "markdown-to-html", + "php", + "pico", + "pico-cms", + "picocms", + "twig", + "website", + "yaml" + ], + "support": { + "docs": "http://picocms.org/docs", + "issues": "https://github.com/picocms/Pico/issues", + "source": "https://github.com/picocms/Pico" + }, + "funding": [ + { + "url": "https://www.bountysource.com/teams/picocms", + "type": "custom" + } + ], + "time": "2020-08-29T14:15:52+00:00" + }, + { + "name": "picocms/pico-deprecated", + "version": "v2.1.4", + "source": { + "type": "git", + "url": "https://github.com/picocms/pico-deprecated.git", + "reference": "8d1a4056ecc71cc2857e91d50bcb38db91d424e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/picocms/pico-deprecated/zipball/8d1a4056ecc71cc2857e91d50bcb38db91d424e0", + "reference": "8d1a4056ecc71cc2857e91d50bcb38db91d424e0", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "picocms/pico": "self.version" + }, + "type": "pico-plugin", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-pico-3.0": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "PicoDeprecated.php", + "lib/", + "plugins/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Rudolf", + "email": "picocms.org@daniel-rudolf.de", + "role": "Lead Developer" + }, + { + "name": "The Pico Community", + "homepage": "http://picocms.org/" + }, + { + "name": "Contributors", + "homepage": "https://github.com/picocms/pico-deprecated/graphs/contributors" + } + ], + "description": "This is Pico's official PicoDeprecated plugin. Pico is a stupidly simple, blazing fast, flat file CMS.", + "homepage": "http://picocms.org/", + "keywords": [ + "compatibility", + "deprecation", + "pico", + "pico-deprecated", + "picocms", + "picocms-plugin" + ], + "support": { + "docs": "http://picocms.org/plugins/deprecated/", + "issues": "https://github.com/picocms/pico-deprecated/issues", + "source": "https://github.com/picocms/pico-deprecated" + }, + "time": "2020-08-29T15:11:07+00:00" + }, + { + "name": "picocms/pico-theme", + "version": "v2.1.4", + "source": { + "type": "git", + "url": "https://github.com/picocms/pico-theme.git", + "reference": "d4ec8df28356f1e034a97d37327b9aecb3129eed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/picocms/pico-theme/zipball/d4ec8df28356f1e034a97d37327b9aecb3129eed", + "reference": "d4ec8df28356f1e034a97d37327b9aecb3129eed", + "shasum": "" + }, + "require": { + "picocms/pico": "self.version" + }, + "type": "pico-theme", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-pico-3.0": "3.0.x-dev" + }, + "installer-name": "default" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Rudolf", + "email": "picocms.org@daniel-rudolf.de", + "role": "Lead Developer" + }, + { + "name": "The Pico Community", + "homepage": "http://picocms.org/" + }, + { + "name": "Contributors", + "homepage": "https://github.com/picocms/pico-theme/graphs/contributors" + } + ], + "description": "This is Pico's official default theme. Pico is a stupidly simple, blazing fast, flat file CMS.", + "homepage": "http://picocms.org/", + "keywords": [ + "default-theme", + "pico", + "pico-theme", + "picocms", + "picocms-theme" + ], + "support": { + "docs": "http://picocms.org/themes/default/", + "issues": "https://github.com/picocms/pico-theme/issues", + "source": "https://github.com/picocms/pico-theme" + }, + "time": "2020-08-29T15:11:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/yaml", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/02c1859112aa779d9ab394ae4f3381911d84052b", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v2.8.52" + }, + "time": "2018-11-11T11:18:13+00:00" + }, + { + "name": "twig/twig", + "version": "v1.44.8", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "b1f009c449e435a0384814e67205d9190a4d050e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/b1f009c449e435a0384814e67205d9190a4d050e", + "reference": "b1f009c449e435a0384814e67205d9190a4d050e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.44-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v1.44.8" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-09-09T17:17:16+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "beta", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/config/config.yml b/config/config.yml index 8dc2bd7..b226f06 100644 --- a/config/config.yml +++ b/config/config.yml @@ -4,7 +4,7 @@ site_title: MinecraftPlus # The title of your website base_url: ~ # Pico will try to guess its base URL, if this fails, override it here; # Example: https://example.com/pico/ -rewrite_url: ~ # A boolean (true or false) indicating whether URL rewriting is forced +rewrite_url: true # nginx routes page URLs to index.php debug: ~ # Set this to true to enable Pico's debug mode timezone: ~ # Your PHP installation might require you to manually specify a timezone locale: ~ # Your PHP installation might require you to manually specify a locale to use diff --git a/docker/debian.sources.list b/docker/debian.sources.list new file mode 100644 index 0000000..7d7f1bc --- /dev/null +++ b/docker/debian.sources.list @@ -0,0 +1,4 @@ +deb https://deb.debian.org/debian bullseye main +deb https://deb.debian.org/debian bullseye-updates main +# Archived metadata is signed but no longer refreshed; retain signature checks. +deb [check-valid-until=no] https://archive.debian.org/debian-security bullseye-security main diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 0000000..451a58a --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Check the rendered application, not merely nginx or an open FPM socket. +body=$(curl --fail --silent --show-error --max-time 3 \ + -H 'Host: localhost' -H 'Accept-Language: en' http://127.0.0.1/en) +[[ "$body" == *'MinecraftPlus'* && "$body" == *'rel="canonical"'* ]] diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..c5d817b --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,53 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + root /var/www/html; + index index.php; + autoindex off; + absolute_redirect off; + server_tokens off; + + access_log /dev/stdout; + error_log /dev/stderr warn; + + # Only the front controller can be executed. REQUEST_URI remains unchanged + # after internal rewrites, so Pico still resolves the original page URL. + location = /index.php { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /var/www/html/index.php; + fastcgi_param HTTP_HOST $http_host; + fastcgi_pass 127.0.0.1:9000; + fastcgi_intercept_errors off; + } + + # Match these before serving any existing file, including in asset folders. + # As with the old .htaccess, let Pico render its normal 404 response. + location ~* ^/(config|content|vendor|language)(/|$) { + rewrite ^ /index.php last; + } + location ~ (^|/)\.(?!well-known(/|$)) { + rewrite ^ /index.php last; + } + location ~* ^/(composer\.(json|lock|phar)|CHANGELOG\.md)(/|$) { + rewrite ^ /index.php last; + } + location ~* \.(php[0-9]*|phtml|phar|twig|ya?ml|md|conf|ini|sh)(/|$) { + rewrite ^ /index.php last; + } + + # Bullseye's default MIME table predates the current font media types. + location ~* \.(woff2?|ttf|otf)$ { + types { + font/woff woff; + font/woff2 woff2; + font/ttf ttf; + font/otf otf; + } + try_files $uri /index.php$is_args$args; + } + + location / { + try_files $uri $uri/ /index.php$is_args$args; + } +} diff --git a/docker/php-fpm.conf b/docker/php-fpm.conf new file mode 100644 index 0000000..e2996b1 --- /dev/null +++ b/docker/php-fpm.conf @@ -0,0 +1,8 @@ +[global] +pid = /usr/local/var/run/php-fpm.pid + +[www] +listen = 127.0.0.1:9000 +chdir = /var/www/html +catch_workers_output = yes +decorate_workers_output = no diff --git a/docker/php.ini b/docker/php.ini new file mode 100644 index 0000000..11ce64c --- /dev/null +++ b/docker/php.ini @@ -0,0 +1,3 @@ +display_errors = Off +log_errors = On +error_log = /proc/self/fd/2 diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..1a3b95a --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +nginx -t +php-fpm -t + +nginx_pid= +fpm_pid= +shutdown() { + trap '' TERM INT QUIT + # Both servers use QUIT to finish in-flight requests gracefully. + for pid in "$nginx_pid" "$fpm_pid"; do + if [[ -n "$pid" ]]; then kill -QUIT "$pid" 2>/dev/null || true; fi + done + # Bound shutdown so Docker's default 10-second stop timeout is sufficient. + for ((attempt = 0; attempt < 80; attempt++)); do + if ! kill -0 ${nginx_pid:-0} 2>/dev/null && ! kill -0 ${fpm_pid:-0} 2>/dev/null; then + break + fi + sleep 0.1 + done + for pid in "$nginx_pid" "$fpm_pid"; do + if [[ -n "$pid" ]]; then kill -KILL "$pid" 2>/dev/null || true; fi + done + wait 2>/dev/null || true +} +trap 'shutdown; exit 0' TERM INT QUIT + +php-fpm --nodaemonize & +fpm_pid=$! +nginx -g 'daemon off;' & +nginx_pid=$! + +# A healthy-looking nginx must never outlive a failed PHP-FPM (or vice versa). +status=0 +wait -n "$fpm_pid" "$nginx_pid" || status=$? +echo "A web server process exited unexpectedly (status $status); stopping container" >&2 +shutdown +if ((status == 0)); then status=1; fi +exit "$status" diff --git a/index.php b/index.php index 42d4629..e0ec27a 100644 --- a/index.php +++ b/index.php @@ -22,7 +22,7 @@ } // Normalize the public protocol and port reported by a trusted reverse proxy. -// Pico otherwise combines X-Forwarded-Proto with Apache's internal port. +// Pico otherwise combines X-Forwarded-Proto with the web server's internal port. if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { $forwardedProtocols = explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO']); $forwardedProtocol = strtolower(trim($forwardedProtocols[0])); diff --git a/tests/container_test.py b/tests/container_test.py new file mode 100644 index 0000000..d68d1b8 --- /dev/null +++ b/tests/container_test.py @@ -0,0 +1,257 @@ +"""End-to-end checks for a built image. Requires Python 3 and Docker only.""" + +import argparse +import hashlib +import json +import subprocess +import time +import urllib.error +import urllib.request +from contextlib import ExitStack, contextmanager +from html.parser import HTMLParser +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def docker(*args, input=None, check=True): + result = subprocess.run( + ["docker", *args], input=input, capture_output=True, text=True, timeout=90 + ) + if check and result.returncode: + raise AssertionError(f"docker {' '.join(args)}: {result.stderr or result.stdout}") + return result.stdout.strip() + + +def require(condition, message): + if not condition: + raise AssertionError(message) + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + +class Links(HTMLParser): + def __init__(self): + super().__init__() + self.urls = [] + + def handle_starttag(self, tag, attrs): + self.urls.extend(value for key, value in attrs if key in ("href", "src") and value) + + +class Container: + def __init__(self, image): + self.id = docker("run", "-d", "-p", "127.0.0.1::80", image) + self.image = image + self.opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), NoRedirect() + ) + + def request(self, path, headers=None): + port = docker("port", self.id, "80/tcp").rsplit(":", 1)[1] + request_headers = {"Host": "website.test", "Accept-Language": "en"} + request_headers.update(headers or {}) + request_headers = {key: value for key, value in request_headers.items() if value is not None} + request = urllib.request.Request( + f"http://127.0.0.1:{port}{path}", headers=request_headers + ) + try: + response = self.opener.open(request, timeout=5) + except urllib.error.HTTPError as error: + response = error + with response: + return response.status, response.headers, response.read() + + def ready(self): + deadline = time.monotonic() + 45 + while time.monotonic() < deadline: + try: + status, _, body = self.request("/en") + if status == 200 and b'MinecraftPlus' in body: + return + except (OSError, urllib.error.URLError): + pass + if self.state()["Status"] == "exited": + break + time.sleep(0.2) + raise AssertionError(f"{self.image} did not start:\n{docker('logs', self.id)}") + + def state(self): + return json.loads(docker("inspect", "--format", "{{json .State}}", self.id)) + + def wait_exit(self): + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + state = self.state() + if not state["Running"]: + return state["ExitCode"] + time.sleep(0.1) + raise AssertionError(f"Container did not exit:\n{docker('logs', self.id)}") + + +@contextmanager +def running(image): + container = Container(image) + try: + container.ready() + yield container + except BaseException: + print(docker("logs", container.id, check=False)) + raise + finally: + docker("rm", "-f", container.id, check=False) + + +def page_checks(container, reference): + paths = ( + "/en", "/pl", "/en/status", "/pl/updates", "/en/download", + "/en/updates/released/cannon-update", "/en/", "/en?source=migration&test=1", + "/?en", "/index.php?pl", "/en/no-such-page", "/pl/no-such-page", + ) + for path in paths: + status, headers, body = container.request(path) + expected = 404 if "no-such-page" in path else 200 + require(status == expected, f"{path}: expected {expected}, got {status}") + require(b"MinecraftPlus" in body, f"{path}: not a rendered Pico page") + require(b" Date: Tue, 27 Oct 2026 10:36:45 +0100 Subject: [PATCH 2/2] Bump PHP --- Dockerfile | 12 ++---- README.md | 44 ++++++++++++++++----- composer.json | 6 +++ composer.lock | 12 +++--- docker/debian.sources.list | 4 -- docker/nginx.conf | 2 +- index.php | 4 +- plugins/MultiLanguage/MultiLanguage.php | 2 +- plugins/TableOfContents/TableOfContents.php | 10 ++++- tests/container_test.py | 20 +++++++++- 10 files changed, 83 insertions(+), 33 deletions(-) delete mode 100644 docker/debian.sources.list diff --git a/Dockerfile b/Dockerfile index 3bc2808..f1a70d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ -# Keep PHP unchanged while migrating the web server; upgrade it separately. -FROM php:7.4-fpm-bullseye@sha256:3ac7c8c74b2b047c7cb273469d74fc0d59b857aa44043e6ea6a0084372811d5b AS base +FROM composer:2@sha256:b6dcd72ca98fa19d0e5b8476de179859704891fad06dd9e0b96833d4c430a5fc AS composer -# Bullseye security packages have moved off the live security mirror. -COPY docker/debian.sources.list /etc/apt/sources.list +FROM php:8.5-fpm-bookworm@sha256:d3db5340c7ecfb1e3a26af5d3d9de8b4e160cdd75692a830b5cb74b0b85db2cf AS base FROM base AS dependencies @@ -10,10 +8,8 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends unzip \ && rm -rf /var/lib/apt/lists/* -# Keep the Composer version used to create the Apache reference image. -RUN curl -fsS https://getcomposer.org/installer -o /tmp/composer-setup.php \ - && php /tmp/composer-setup.php --version=2.1.14 --install-dir=/usr/local/bin --filename=composer \ - && rm /tmp/composer-setup.php +COPY --from=composer /usr/bin/composer /usr/local/bin/composer +ENV COMPOSER_ALLOW_SUPERUSER=1 WORKDIR /var/www/html COPY composer.json composer.lock ./ diff --git a/README.md b/README.md index 01303d5..fd28760 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ backend. ## Build and run -Requires Docker with Linux containers. Build from the repository root: +Requires Docker with Linux containers. The image runs PHP 8.5.12 on Debian +Bookworm; running the application outside Docker requires PHP 8.5 or newer +(within PHP 8.x). Build from the repository root: ```sh docker build -t mcplus-website:local . @@ -23,7 +25,7 @@ still list that unused external port in image metadata. `composer.lock` preserves the dependencies extracted from the previous Apache image, `minecraftplus/website@sha256:ad08f5461d974438eb6ae4c79b712090f45bdb06614fae5240dab10c18b702f8`. -The build uses Composer 2.1.14, installs locked dependencies first, and then +The build uses Composer 2.10.3, installs locked dependencies first, and then overlays the repository's modified plugins and theme. Keep that order when editing the Dockerfile. PHP `dom` and `mbstring` are checked during the build. No dependencies are installed at container startup. @@ -67,7 +69,11 @@ them afterwards. It checks page rendering, language redirects, legacy URLs, query strings, canonical/alternate links behind HTTPS proxies (including a custom port), 404s, static-file bytes and MIME types, video range requests, protection of internal files and arbitrary PHP scripts, plugin overrides, application health, -process failure handling and graceful shutdown. +process failure handling and graceful shutdown. It also verifies PHP 8.5, +checks logs for runtime warnings/errors and preserves the existing Polish +heading anchors. The table-of-contents plugin explicitly uses the old `C` +transliteration locale while generating anchors, restoring the original locale +afterwards; PHP 8's default `C.UTF-8` would otherwise change public fragment URLs. To compare HTML responses with an Apache reference built from the same application source and lock file: @@ -78,7 +84,10 @@ python tests/container_test.py mcplus-website:local --reference mcplus-website:a The migration was compared against a local Apache reference created by overlaying the pre-migration working tree on the previous image above. An unrelated or older -application revision is unsuitable for byte-for-byte comparisons. +application revision is unsuitable for byte-for-byte comparisons. For the PHP +upgrade, the previous nginx/PHP 7.4 image was retained locally as +`mcplus-website:php74-reference` and used with the same `--reference` option; +rendered page responses remain identical. GitHub Actions builds and loads a candidate image, runs the suite, and publishes the **same image** only after tests pass on a push to `master`. Pull requests only @@ -114,8 +123,25 @@ it from its saved image digest and original settings. No database or content migration is involved. Use an immutable digest for rollback: a mutable tag such as `latest` can refer to nginx after publication. -PHP remains 7.4.33 to isolate the server migration. Its base image is pinned by -digest. Bullseye security packages now come from Debian's official archive; -archive metadata expiry is disabled for that source only, while signature -verification remains enabled. PHP 7.4 is unsupported: upgrading PHP, Pico/Twig -compatibility and the old Composer installer is a separate follow-up. +The PHP and Composer images are pinned by digest. Bookworm uses its normal signed +package repositories; the former Bullseye archive configuration has been removed. +`composer.json` declares PHP `^8.5` and explicitly permits the Pico installer +plugin. The lock file's platform metadata has been refreshed without changing +the installed package versions. + +## Legacy CMS dependencies + +This change upgrades PHP and Composer while preserving Pico 2.1 and the existing +theme/plugin APIs. Pico, Twig 1, Symfony YAML 2 and the old Composer installer +still emit deprecation messages under PHP 8.5. Deprecation reporting has not been +disabled to hide them. The container tests fail on PHP warnings, notices, parse +errors and fatal errors; deprecations remain a documented follow-up. + +An audit during the PHP upgrade reported 18 existing security advisories affecting +Twig and Symfony YAML. This runtime upgrade does not remediate those dependency +advisories. Updating the CMS and its dependency APIs is a separate migration. +To inspect the current report without changing project files: + +```sh +docker run --rm -v "${PWD}:/app:ro" -w /app composer:2 audit --locked --no-plugins +``` diff --git a/composer.json b/composer.json index cdb1b10..3457cec 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "source": "https://github.com/picocms/Pico" }, "require": { + "php": "^8.5", "picocms/pico": "^2.1", "picocms/pico-theme": "^2.1", "picocms/pico-deprecated": "^2.1", @@ -33,6 +34,11 @@ "iridescent-dev/pico-toc-plugin": "^2.0", "iridescent-dev/pico-multilanguage-plugin": "^1.0" }, + "config": { + "allow-plugins": { + "picocms/composer-installer": true + } + }, "minimum-stability": "beta", "prefer-stable": true } diff --git a/composer.lock b/composer.lock index 3b33452..d8e62e3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ab613beed761671676e908fbd291d1be", + "content-hash": "89b5570f157ef3cf285fccdf3c86a189", "packages": [ { "name": "erusev/parsedown", @@ -743,10 +743,12 @@ "packages-dev": [], "aliases": [], "minimum-stability": "beta", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.1.0" + "platform": { + "php": "^8.5" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/docker/debian.sources.list b/docker/debian.sources.list deleted file mode 100644 index 7d7f1bc..0000000 --- a/docker/debian.sources.list +++ /dev/null @@ -1,4 +0,0 @@ -deb https://deb.debian.org/debian bullseye main -deb https://deb.debian.org/debian bullseye-updates main -# Archived metadata is signed but no longer refreshed; retain signature checks. -deb [check-valid-until=no] https://archive.debian.org/debian-security bullseye-security main diff --git a/docker/nginx.conf b/docker/nginx.conf index c5d817b..912ec18 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -36,7 +36,7 @@ server { rewrite ^ /index.php last; } - # Bullseye's default MIME table predates the current font media types. + # Use the standard font media types independently of the system MIME table. location ~* \.(woff2?|ttf|otf)$ { types { font/woff woff; diff --git a/index.php b/index.php index e0ec27a..9fa5c63 100644 --- a/index.php +++ b/index.php @@ -11,8 +11,8 @@ */ // check PHP platform requirements -if (PHP_VERSION_ID < 50306) { - die('Pico requires PHP 5.3.6 or above to run'); +if (PHP_VERSION_ID < 80500) { + die('This website requires PHP 8.5 or above to run'); } if (!extension_loaded('dom')) { die("Pico requires the PHP extension 'dom' to run"); diff --git a/plugins/MultiLanguage/MultiLanguage.php b/plugins/MultiLanguage/MultiLanguage.php index 2f5ef31..e41deb1 100644 --- a/plugins/MultiLanguage/MultiLanguage.php +++ b/plugins/MultiLanguage/MultiLanguage.php @@ -374,7 +374,7 @@ public function onTwigRegistration() */ private function getBrowserLanguage() { - $browser_language = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2); + $browser_language = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', 0, 2); return in_array($browser_language, $this->available_languages) ? $browser_language : $this->default_language; } diff --git a/plugins/TableOfContents/TableOfContents.php b/plugins/TableOfContents/TableOfContents.php index f56fd0e..fa40924 100644 --- a/plugins/TableOfContents/TableOfContents.php +++ b/plugins/TableOfContents/TableOfContents.php @@ -281,7 +281,15 @@ private function slugify($text) // replace non letter or digits by - $text = preg_replace('~[^\pL\d]+~u', '-', $text); // transliterate - $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); + // Preserve existing public anchors: PHP 8 uses C.UTF-8 by default, + // whose transliteration differs from the previous C locale. + $previousLocale = setlocale(LC_CTYPE, 0); + setlocale(LC_CTYPE, 'C'); + try { + $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); + } finally { + setlocale(LC_CTYPE, $previousLocale); + } // remove unwanted characters $text = preg_replace('~[^-\w]+~', '', $text); // trim diff --git a/tests/container_test.py b/tests/container_test.py index d68d1b8..9fae675 100644 --- a/tests/container_test.py +++ b/tests/container_test.py @@ -21,7 +21,10 @@ def docker(*args, input=None, check=True): ) if check and result.returncode: raise AssertionError(f"docker {' '.join(args)}: {result.stderr or result.stdout}") - return result.stdout.strip() + output = result.stdout + if args and args[0] == "logs": + output += result.stderr + return output.strip() def require(condition, message): @@ -118,6 +121,10 @@ def page_checks(container, reference): require(status == expected, f"{path}: expected {expected}, got {status}") require(b"MinecraftPlus" in body, f"{path}: not a rendered Pico page") require(b"