From 33932b78891f66131964d99ee2c71900e33813d1 Mon Sep 17 00:00:00 2001 From: Juri Ehret Date: Wed, 13 May 2026 10:23:11 +0200 Subject: [PATCH 1/5] fix(demo): rewrite composer repositories via PHP script, not CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `composer config repositories.0 '{...}'` doesn't replace index 0 in a sequential `repositories` array — it adds a SECOND entry keyed `"0"` (string), leaving the original path-repo at `../imanager` in place. Modern Composer then fails repository discovery with: In PathRepository.php line 163: The `url` supplied for the path (../imanager) repository does not exist Rewrite composer.json directly with a small PHP helper script that overwrites the `repositories` section with the VCS-only shape we actually want. Helper is COPYed into /tmp during the build and removed afterwards. Verified: iManager's `composer.json` `branch-alias` maps `dev-main` to `2.0.x-dev`, so Scriptor's `bigins/imanager: 2.0.x-dev` constraint resolves against the VCS repo's `main` branch as expected. Cuts directly through the build failure from the first `docker compose up -d --build` run. --- docker/Dockerfile | 20 ++++++++++++-------- docker/composer-rewrite.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 docker/composer-rewrite.php diff --git a/docker/Dockerfile b/docker/Dockerfile index 97fe76b..d9d8a1e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,17 +42,21 @@ ENV COMPOSER_HOME=/tmp/composer \ WORKDIR /var/www/scriptor -# Bring composer.json in first so the install layer caches independently -# from application source changes. +# Bring composer.json + the repo-rewrite helper in first so the install +# layer caches independently from application source changes. COPY composer.json composer.lock ./ +COPY docker/composer-rewrite.php /tmp/composer-rewrite.php # The repo's composer.json declares a `path` repository at `../imanager` -# for local development. Inside the container that path doesn't exist, so -# we swap repository[0] for a VCS repo pointing at the public iManager -# GitHub repo. Then update (not install) so the lock can be regenerated -# against the VCS source. -RUN composer config repositories.0 '{"type": "vcs", "url": "https://github.com/bigin/imanager"}' \ - && rm -f composer.lock \ +# for local development. Inside the container that path doesn't exist, and +# modern Composer treats a missing path-repo as a fatal error during +# discovery — so we rewrite the `repositories` array to a VCS repo +# pointing at the public iManager GitHub repo (iManager's `main` branch +# is `branch-alias`-mapped to `2.0.x-dev`, which is what composer.json +# requires). Then update (not install) so the lock regenerates against +# the VCS source. +RUN php /tmp/composer-rewrite.php \ + && rm -f composer.lock /tmp/composer-rewrite.php \ && composer update --no-dev --no-interaction --prefer-dist --optimize-autoloader # Application code last so the previous layers (large) stay cached diff --git a/docker/composer-rewrite.php b/docker/composer-rewrite.php new file mode 100644 index 0000000..c96dcad --- /dev/null +++ b/docker/composer-rewrite.php @@ -0,0 +1,28 @@ + 'vcs', 'url' => 'https://github.com/bigin/imanager'], +]; + +$out = json_encode($c, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) . \PHP_EOL; +file_put_contents($file, $out); +fwrite(\STDOUT, "[composer-rewrite] swapped path repo for VCS repo.\n"); From a36af615f00f5e189314ca17c60683057afc190e Mon Sep 17 00:00:00 2001 From: Juri Ehret Date: Wed, 13 May 2026 10:34:07 +0200 Subject: [PATCH 2/5] fix(demo): make host port overridable via SCRIPTOR_DEMO_PORT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compose stack hard-coded `8080:80`, which collides with anything else on the host already on 8080 (local web stacks, other containers). Make the host side dynamic via an env var that defaults to 8080: SCRIPTOR_DEMO_PORT=8090 docker compose up -d --build The container itself still listens on 80 internally — only the host-side mapping is configurable. docs/demo.md gets a callout for the override. --- docker-compose.yml | 4 +++- docs/demo.md | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 129bc9c..e9a6e5a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,9 @@ services: scriptor: condition: service_healthy ports: - - "8080:80" + # Host port can be overridden — e.g. `SCRIPTOR_DEMO_PORT=8090 docker compose up` + # — when 8080 is already taken on the host (ServBay, another container, …). + - "${SCRIPTOR_DEMO_PORT:-8080}:80" volumes: - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro - scriptor-app:/var/www/scriptor:ro diff --git a/docs/demo.md b/docs/demo.md index 3acc93a..282a5fa 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -31,6 +31,17 @@ Then open: | **http://localhost:8080/** | The public front. The single seeded page rendered through Scriptor's default theme. | | **http://localhost:8080/editor/** | The editor. Sign in with the credentials below. | +> **Port already in use?** If something else on the host (a local web +> stack, another container) is already on `8080`, override the host +> port via the `SCRIPTOR_DEMO_PORT` env var: +> +> ```bash +> SCRIPTOR_DEMO_PORT=8090 docker compose up -d --build +> ``` +> +> The container itself always listens on `80`; only the host-side +> port is dynamic. + ### Default credentials ``` From ecdb713d477fc250acde135febadfc2e48531ec9 Mon Sep 17 00:00:00 2001 From: Juri Ehret Date: Wed, 13 May 2026 10:44:55 +0200 Subject: [PATCH 3/5] fix(demo): nginx config uses `nginx` user, not `www-data` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The official `nginx:alpine` image ships with a `nginx` system user; it does NOT have `www-data`. The previous config said `user www-data;`, which made nginx die on startup with: nginx: [emerg] getpwnam("www-data") failed …and the web container then restarted in a loop. The php-fpm side still runs as `www-data` (that's the user from the `php:fpm-alpine` base). They're two separate containers, so the user mismatch is fine: nginx only reads the shared volume, and the Scriptor image leaves files world-readable (0644/0755 defaults). Added a comment block explaining the asymmetry so the next person who reads the config doesn't try to "fix" it back to www-data. --- docker/nginx.conf | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index a1015d7..dad8d8e 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -6,7 +6,12 @@ # the logs and backups dirs) is blocked at the URL layer — defence in # depth on top of the fact that `data/` lives outside the webroot anyway. -user www-data; +# The official nginx:alpine image runs as the `nginx` user — not +# `www-data` like the php-fpm image. They're two separate containers, +# so the user mismatch is fine: nginx only needs read access to the +# shared volume, and the volume's files are world-readable (0644/0755 +# defaults from the Scriptor image). +user nginx; worker_processes auto; error_log /dev/stderr warn; pid /run/nginx.pid; From 3c706e7a3de334d11757058a7d8a62b64bcc7838 Mon Sep 17 00:00:00 2001 From: Juri Ehret Date: Wed, 13 May 2026 10:48:35 +0200 Subject: [PATCH 4/5] fix(demo): drop `$uri/` from try_files so /editor/* goes through index.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scriptor's design is single-entry routing: the root `index.php` delegates `//*` (default: `/editor/*`) to the editor in PHP. With `try_files $uri $uri/ /index.php` and the default `index index.php` directive, nginx was resolving `/editor/` to the filesystem directory, then falling through to `editor/index.php` via the index directive — at which point the `\.php$` deny-rule caught it and returned 404. Dropping `$uri/` from try_files makes nginx skip the directory resolution: `/editor/` is not a file → no `$uri` match → falls through to `/index.php?$query_string`, which is exactly what Scriptor's routing expects. Verified: `/`, `/editor/`, and `/hello-world/` (the seeded example page) all return 200 with the expected content. Login form on `/editor/` renders with a fresh CSRF token. --- docker/nginx.conf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index dad8d8e..75a732b 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -58,9 +58,15 @@ http { return 404; } - # Front controller. + # Front controller. We deliberately leave `$uri/` OUT of the + # try_files list — Scriptor's `index.php` at the repo root + # handles `/editor/*` internally via PHP delegation. If we let + # nginx resolve `/editor/` to `editor/index.php` (via the + # `index` directive), the request bypasses the front + # controller and hits the editor's index directly, which + # depends on globals the root index.php sets up. location / { - try_files $uri $uri/ /index.php?$query_string; + try_files $uri /index.php?$query_string; } location ~ ^/index\.php(/|$) { From 7748205462d67b7b2686fc6e6503f401aa2b3132 Mon Sep 17 00:00:00 2001 From: Juri Ehret Date: Wed, 13 May 2026 10:57:34 +0200 Subject: [PATCH 5/5] fix(demo): hash admin password in seed (ItemRepository doesn't validate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seed passed `'password' => 'scriptor'` verbatim to ItemRepository::save(), expecting the registered PasswordFieldType to bcrypt it. It doesn't — SqliteItemRepository::save() writes the data column as-is and never invokes the FieldTypeRegistry. Validation is the host editor's job (Scriptor's PagesModule / ProfileModule run it before save). So the seed stored plaintext "scriptor" in the password field and AuthModule's `password_verify($plaintext, $hashOrPlaintext)` failed on the next login attempt — bcrypt prefix mismatch. Hash directly with `password_hash($pw, PASSWORD_BCRYPT)` in the seed — the same call PasswordFieldType::validate() would have produced. Verified end-to-end on the demo container: - DB now stores `$2y$10$…` bcrypt hash. - POST /editor/ with admin / scriptor returns 302 → /editor/. - The follow-up GET shows the authenticated dashboard ("Pages", "Dashboard", "logout"). Separate follow-up needed in the iManager docs: api/storage.md and docs/field-types.md both claim ItemRepository::save() validates through plugins. That's wrong — it doesn't. Will land in a docs PR on the iManager side. --- docker/seed-demo.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docker/seed-demo.php b/docker/seed-demo.php index 215c9c9..d7a8639 100644 --- a/docker/seed-demo.php +++ b/docker/seed-demo.php @@ -88,8 +88,10 @@ } // -- Admin user -- -// PasswordFieldType validates `scriptor` (8 chars, meets default minLength) -// and bcrypts it on save. Do NOT pre-hash here — let the field type do it. +// `ItemRepository::save()` writes `$data` verbatim — it does NOT run +// the registered FieldTypePlugin's validate() (that's the host editor's +// job). So we have to hash the password ourselves here, the same way +// `PasswordFieldType::validate()` would: bcrypt via `password_hash()`. fwrite(\STDOUT, "[seed] creating admin user (admin/scriptor)…\n"); $items->save(new Item( id: null, @@ -99,7 +101,7 @@ data: [ 'role' => 'admin', 'email' => 'admin@example.com', - 'password' => 'scriptor', + 'password' => password_hash('scriptor', \PASSWORD_BCRYPT), ], ));