From 389005508c5504b8c9ce142d0ff8bb468dc08f6c Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 15 Sep 2026 17:09:47 +0530 Subject: [PATCH 1/4] feat(env): Serve local wp-env over HTTPS The local environment ran on plain HTTP, which hid behaviour that only appears under TLS: is_ssl() picks the delivery URL scheme, auth cookies only get the Secure flag over HTTPS, and the admin enforces FORCE_SSL_ADMIN. Those differences surfaced only in production. Add an nginx proxy that terminates TLS in front of wp-env, using a certificate issued by a locally generated CA. Host names resolve through public wildcard DNS, so no /etc/hosts entry is needed, and the proxy binds the standard ports so no port number leaks into generated URLs. Loopback REST requests verify the certificate rather than skipping the check, so they exercise the same path as production. CI keeps running over plain HTTP through .wp-env.ci.json, because runners have no local CA and no proxy container. --- .github/workflows/ci.yml | 17 +++++-- .gitignore | 3 ++ .wp-env.ci.json | 26 ++++++++++ .wp-env.json | 16 ++++-- .wp-env/docker/mkcert/Dockerfile | 16 ++++++ .wp-env/mu-plugins/https-proxy.php | 79 ++++++++++++++++++++++++++++++ .wp-env/proxy/docker-compose.yml | 45 +++++++++++++++++ .wp-env/proxy/nginx.conf.template | 73 +++++++++++++++++++++++++++ .wp-env/scripts/after-start.sh | 20 ++++++++ .wp-env/scripts/config.sh | 33 +++++++++++++ .wp-env/scripts/proxy-down.sh | 18 +++++++ .wp-env/scripts/proxy-up.sh | 68 +++++++++++++++++++++++++ .wp-env/scripts/trust-ca.sh | 71 +++++++++++++++++++++++++++ README.md | 41 +++++++++++++++- package.json | 13 +++-- tests/e2e/global-setup.js | 2 +- tests/e2e/playwright.config.js | 36 +++++++++++++- 17 files changed, 560 insertions(+), 17 deletions(-) create mode 100644 .wp-env.ci.json create mode 100644 .wp-env/docker/mkcert/Dockerfile create mode 100644 .wp-env/mu-plugins/https-proxy.php create mode 100644 .wp-env/proxy/docker-compose.yml create mode 100644 .wp-env/proxy/nginx.conf.template create mode 100755 .wp-env/scripts/after-start.sh create mode 100755 .wp-env/scripts/config.sh create mode 100755 .wp-env/scripts/proxy-down.sh create mode 100755 .wp-env/scripts/proxy-up.sh create mode 100755 .wp-env/scripts/trust-ca.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3469612e5..16c5dc588 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,15 +125,18 @@ jobs: # The wp-env sources directory is deliberately not cached. A restored # ~/.wp-env carries the previous run's install state, which skipped the # plugin's activation hook and left the relationships table missing. + # .wp-env.ci.json omits the HTTPS URLs and the proxy lifecycle script + # that .wp-env.json uses for local development. Runners have no local CA + # and no proxy container, so they serve the site over plain HTTP. - name: Start wp-env - run: wp-env start + run: wp-env start --config .wp-env.ci.json - name: Run unit tests - run: wp-env run tests-cli --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit + run: wp-env run tests-cli --config .wp-env.ci.json --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit - name: Stop wp-env if: always() - run: wp-env stop + run: wp-env stop --config .wp-env.ci.json e2e: name: E2E (Playwright) @@ -192,7 +195,7 @@ jobs: env: PLAYWRIGHT_CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }} run: | - npm run env:start > wp-env-start.log 2>&1 & + npx wp-env start --config .wp-env.ci.json > wp-env-start.log 2>&1 & wp_env_pid=$! if [ "$PLAYWRIGHT_CACHE_HIT" = "true" ]; then @@ -215,14 +218,18 @@ jobs: exit 1 fi + # Runners have no local CA and no HTTPS proxy, so the suite talks to + # wp-env's published port directly over plain HTTP. WP_BASE_URL is the + # single switch for this; see tests/e2e/playwright.config.js. - name: Run E2E tests env: CLOUDINARY_E2E_URL: ${{ secrets.CLOUDINARY_E2E_URL }} + WP_BASE_URL: http://localhost:8889 run: npm run test:e2e - name: Stop wp-env if: always() - run: npm run env:stop + run: npx wp-env stop --config .wp-env.ci.json - name: Upload Playwright artifacts if: failure() diff --git a/.gitignore b/.gitignore index 686f45bd3..263b138a3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ coverage/html/ # wp-env personal overrides .wp-env.override.json +# Locally generated TLS certificates and CA for the wp-env HTTPS proxy +/.wp-env/certs/ + # IDE .vscode diff --git a/.wp-env.ci.json b/.wp-env.ci.json new file mode 100644 index 000000000..975eba8e4 --- /dev/null +++ b/.wp-env.ci.json @@ -0,0 +1,26 @@ +{ + "core": "WordPress/WordPress#7.1", + "phpVersion": "8.5", + "plugins": ["."], + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + }, + "mappings": { + "wp-content/mu-plugins": "./.wp-env/mu-plugins" + }, + "lifecycleScripts": { + "afterStart": "./.wp-env/scripts/fix-loopback.sh" + }, + "env": { + "tests": { + "phpVersion": "8.2", + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + } + } + } +} diff --git a/.wp-env.json b/.wp-env.json index 975eba8e4..7827d5fec 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -5,13 +5,18 @@ "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, - "SCRIPT_DEBUG": true + "SCRIPT_DEBUG": true, + "WP_HOME": "https://cloudinary.local.wpenv.net", + "WP_SITEURL": "https://cloudinary.local.wpenv.net", + "FORCE_SSL_ADMIN": true, + "WP_CONTENT_URL": "https://cloudinary.local.wpenv.net/wp-content", + "WP_PLUGIN_URL": "https://cloudinary.local.wpenv.net/wp-content/plugins" }, "mappings": { "wp-content/mu-plugins": "./.wp-env/mu-plugins" }, "lifecycleScripts": { - "afterStart": "./.wp-env/scripts/fix-loopback.sh" + "afterStart": "./.wp-env/scripts/after-start.sh" }, "env": { "tests": { @@ -19,7 +24,12 @@ "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, - "SCRIPT_DEBUG": true + "SCRIPT_DEBUG": true, + "WP_HOME": "https://tests.cloudinary.local.wpenv.net", + "WP_SITEURL": "https://tests.cloudinary.local.wpenv.net", + "FORCE_SSL_ADMIN": true, + "WP_CONTENT_URL": "https://tests.cloudinary.local.wpenv.net/wp-content", + "WP_PLUGIN_URL": "https://tests.cloudinary.local.wpenv.net/wp-content/plugins" } } } diff --git a/.wp-env/docker/mkcert/Dockerfile b/.wp-env/docker/mkcert/Dockerfile new file mode 100644 index 000000000..0d9bde6a4 --- /dev/null +++ b/.wp-env/docker/mkcert/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.24-alpine + +# Set the version tag to build. +ENV MKCERT_VERSION="v1.4.4" + +RUN apk add --no-cache git + +RUN git clone https://github.com/FiloSottile/mkcert /go/mkcert \ + && cd /go/mkcert \ + && git checkout "tags/$MKCERT_VERSION" -b "build/$MKCERT_VERSION" \ + && go build -ldflags "-X main.Version=$MKCERT_VERSION" -o /bin/mkcert + +# mkcert reads and writes its CA here. The compose file mounts the project's +# certificate directory over it, so both the CA and the issued certificates +# stay inside the repository instead of the developer's global mkcert store. +WORKDIR /root/.local/share/mkcert diff --git a/.wp-env/mu-plugins/https-proxy.php b/.wp-env/mu-plugins/https-proxy.php new file mode 100644 index 000000000..0b7a4a375 --- /dev/null +++ b/.wp-env/mu-plugins/https-proxy.php @@ -0,0 +1,79 @@ +" | cut -f1 | head -1) + + if [ -n "$container" ]; then + echo "Docker container '$container'" + return + fi + + local process + process=$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -Fc 2>/dev/null | grep '^c' | head -1 | cut -c2-) + + if [ -n "$process" ]; then + echo "process '$process'" + return + fi + + echo "another process" +} + +# Ignore ports already held by our own proxy; those are from a previous start +# and compose will reuse them. +check_port() { + local port="$1" + + if [ -z "$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null)" ]; then + return 0 + fi + + if docker compose --project-name "$PROXY_PROJECT" ps --quiet 2>/dev/null | grep -q .; then + return 0 + fi + + echo "Error: port $port is already in use by $(describe_port_holder "$port")." >&2 + echo "Stop it and run 'npm run env:proxy:up' to finish starting the HTTPS proxy." >&2 + return 1 +} + +check_port "$PROXY_HTTP_PORT" +check_port "$PROXY_HTTPS_PORT" + +mkdir -p "$CERTS_DIR" + +export DEV_HOST TESTS_HOST PROXY_HTTP_PORT PROXY_HTTPS_PORT WP_ENV_PORT WP_ENV_TESTS_PORT + +docker compose \ + --project-name "$PROXY_PROJECT" \ + --file "$PROXY_DIR/docker-compose.yml" \ + up --detach --build --remove-orphans + +echo "HTTPS proxy running:" +echo " Development: https://$DEV_HOST" +echo " Tests: https://$TESTS_HOST" diff --git a/.wp-env/scripts/trust-ca.sh b/.wp-env/scripts/trust-ca.sh new file mode 100755 index 000000000..af1670346 --- /dev/null +++ b/.wp-env/scripts/trust-ca.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Make HTTPS loopback requests work inside the wp-env containers. +# +# The plugin's sync daemon calls its own REST API. Once WP_HOME is an https:// +# URL those calls leave the container, so the container has to be able to both +# resolve the host name and verify the certificate: +# +# 1. Point the proxy host names at the Docker host gateway, because +# cloudinary.local.wpenv.net resolves to 127.0.0.1, which inside a +# container means the container itself. +# 2. Install the mkcert root CA so the self-signed certificate verifies. +# +# Without step 2 every loopback request would need sslverify disabled, and the +# HTTPS path would never be exercised the way it is in production. +# +# Container IPs and the gateway address change between restarts, so this runs on +# every `wp-env start` and rewrites what it finds. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +CA_FILE="$CERTS_DIR/rootCA.pem" + +if [ ! -f "$CA_FILE" ]; then + echo "Warning: no root CA at $CA_FILE. Run 'npm run env:proxy:up' first; skipping loopback trust setup." + exit 0 +fi + +# The wp-env containers. The CLI containers are included so WP-CLI commands and +# the PHPUnit suite reach the site over HTTPS too. +CONTAINER_PATTERNS='wordpress-1$|cli-1$' + +CONTAINERS=$(docker ps --format '{{.Names}}' | grep -E "$CONTAINER_PATTERNS" || true) + +if [ -z "$CONTAINERS" ]; then + echo "Warning: no wp-env containers found. Loopback trust setup skipped." + exit 0 +fi + +for container in $CONTAINERS; do + # host.docker.internal is mapped to host-gateway in wp-env's compose file, + # so resolving it inside the container gives the address the proxy is + # reachable on. + gateway=$(docker exec "$container" getent hosts host.docker.internal 2>/dev/null | awk '{print $1}' | head -1) + + if [ -z "$gateway" ]; then + echo "Warning: could not resolve the host gateway in $container; skipping." + continue + fi + + # Replace any previous entry so a changed gateway address cannot leave a + # stale line behind, then append the current one. + docker exec --user root "$container" bash -c " + sed -i '/$DEV_HOST/d; /$TESTS_HOST/d' /etc/hosts + echo '$gateway $DEV_HOST $TESTS_HOST' >> /etc/hosts + " 2>/dev/null || echo "Warning: could not update /etc/hosts in $container." + + # update-ca-certificates rebuilds the bundle that both PHP and curl read. + docker cp "$CA_FILE" "$container:/usr/local/share/ca-certificates/mkcert-root-ca.crt" >/dev/null 2>&1 || { + echo "Warning: could not copy the root CA into $container." + continue + } + + docker exec --user root "$container" update-ca-certificates >/dev/null 2>&1 || { + echo "Warning: could not install the root CA in $container." + continue + } +done + +echo "Loopback trust configured: containers resolve $DEV_HOST and trust the local CA." diff --git a/README.md b/README.md index 15ef3665a..bb1db96f8 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), - [npm](https://www.npmjs.com/) v10+ - [Composer](https://getcomposer.org/) - [Docker](https://www.docker.com/) (required for the WordPress local environment via `wp-env`) +- [mkcert](https://github.com/FiloSottile/mkcert) (required once, to trust the local HTTPS certificate) ### Local Development Setup @@ -74,9 +75,23 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), npm run env:start ``` - This spins up a WordPress instance at [http://localhost:8888](http://localhost:8888) with the plugin activated and `WP_DEBUG` enabled. A loopback fix is applied automatically so REST API self-requests work inside the container. + This spins up a WordPress instance at [https://cloudinary.local.wpenv.net](https://cloudinary.local.wpenv.net) with the plugin activated and `WP_DEBUG` enabled, plus a tests instance at [https://tests.cloudinary.local.wpenv.net](https://tests.cloudinary.local.wpenv.net). -5. **Build front-end assets:** + The site is served over HTTPS by an nginx proxy that `npm run env:start` brings up alongside wp-env. The environment runs over TLS by default because several code paths behave differently under HTTPS: `is_ssl()` decides the delivery URL scheme, auth cookies only get the `Secure` flag on HTTPS, and the admin enforces `FORCE_SSL_ADMIN`. Testing on plain HTTP hides those differences until production. + + Loopback REST API self-requests are configured automatically, and they verify the certificate rather than skipping the check, so they exercise the same code path as production. + +5. **Trust the local certificate (first run only):** + + ```bash + npm run env:install-cert + ``` + + This adds the locally generated certificate authority to your OS trust store, so the browser accepts `*.cloudinary.local.wpenv.net` without a warning. It asks for your password, because changing the system trust store requires it. The certificate and its CA are created on first `npm run env:start` and live in `.wp-env/certs/`, which is gitignored. + + No `/etc/hosts` entry is needed: `local.wpenv.net` and all of its subdomains resolve to `127.0.0.1` over public DNS. + +6. **Build front-end assets:** ```bash npm run build # One-time production build @@ -90,6 +105,9 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), | `npm run env:start` | Start the local WordPress environment | | `npm run env:stop` | Stop the local WordPress environment | | `npm run env:destroy` | Remove the local environment completely | +| `npm run env:install-cert` | Trust the local HTTPS certificate (once) | +| `npm run env:proxy:up` | Start the HTTPS proxy on its own | +| `npm run env:proxy:down` | Stop the HTTPS proxy | | `npm run env:logs` | View container logs | | `npm run env:cli` | Run WP-CLI commands inside the container | | `npm run env:clean` | Reset the environment (removes all data) | @@ -103,6 +121,16 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), | `npm run lint:style` | Run stylelint on SCSS files | | `npm run i18n` | Generate translation files | +### Troubleshooting the local environment + +**The browser warns that the certificate is not trusted.** Run `npm run env:install-cert`. If the warning persists, delete `.wp-env/certs/`, run `npm run env:start` to reissue the certificate, then trust it again. + +**`npm run env:start` reports that port 80 or 443 is in use.** Another local project holds the port. The message names the container or process. Stop it, then run `npm run env:proxy:up` to finish starting the proxy. + +**`wp-env stop` leaves the proxy running.** The proxy is a separate Docker Compose project, so wp-env does not manage it. `npm run env:stop` and `npm run env:destroy` stop it for you; `npm run env:proxy:down` does it on its own. + +**CI uses a different config.** GitHub runners have no certificate authority and no proxy, so the workflow passes `--config .wp-env.ci.json`, which is the same environment without the HTTPS URLs. Keep the shared values in both files in sync. + ### Create a Plugin Release Package Run `npm run package` to create the plugin release in the `/build` directory and package it as `cloudinary-image-management-and-manipulation-in-the-cloud-cdn.zip` in the root directory. @@ -134,6 +162,7 @@ E2E tests run against a wp-env site using Playwright. npm install npx playwright install --with-deps chromium npm run env:start +npm run env:install-cert ``` ### Running the tests @@ -142,6 +171,14 @@ npm run env:start npm run test:e2e ``` +The suite runs against the HTTPS tests site at `https://tests.cloudinary.local.wpenv.net`. Certificates are verified rather than ignored, so a broken certificate fails the run instead of passing silently. Always start the suite through the npm scripts: they set `NODE_EXTRA_CA_CERTS`, which Playwright's Node-side request client needs because it does not read the OS trust store. + +To run against a different site, set `WP_BASE_URL`. CI uses this to run over plain HTTP, because GitHub runners have no local certificate authority: + +```bash +WP_BASE_URL=http://localhost:8889 npm run test:e2e +``` + ### Wizard test credentials `tests/e2e/wizard-setup.spec.js` exercises the live Cloudinary connection flow, so it needs a real connection string. Provide one of two ways: diff --git a/package.json b/package.json index f10faf94a..388033375 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,11 @@ "deploy-assets": "grunt deploy-assets", "dev": "wp-scripts start", "env:start": "wp-env start", - "env:stop": "wp-env stop", - "env:destroy": "wp-env destroy", + "env:stop": "./.wp-env/scripts/proxy-down.sh && wp-env stop", + "env:destroy": "./.wp-env/scripts/proxy-down.sh && wp-env destroy", + "env:install-cert": "CAROOT=./.wp-env/certs mkcert -install", + "env:proxy:up": "./.wp-env/scripts/proxy-up.sh", + "env:proxy:down": "./.wp-env/scripts/proxy-down.sh", "env:logs": "wp-env logs", "env:cli": "wp-env run cli", "env:clean": "wp-env clean all", @@ -38,9 +41,9 @@ "readme": "composer readme", "prepare": "husky", "test:e2e": "npm-run-all --silent test:e2e:parallel test:e2e:serial", - "test:e2e:parallel": "playwright test --config tests/e2e/playwright.config.js --grep-invert @serial", - "test:e2e:serial": "playwright test --config tests/e2e/playwright.config.js --grep @serial --workers=1", - "test:e2e:debug": "playwright test --config tests/e2e/playwright.config.js --ui", + "test:e2e:parallel": "NODE_EXTRA_CA_CERTS=./.wp-env/certs/rootCA.pem playwright test --config tests/e2e/playwright.config.js --grep-invert @serial", + "test:e2e:serial": "NODE_EXTRA_CA_CERTS=./.wp-env/certs/rootCA.pem playwright test --config tests/e2e/playwright.config.js --grep @serial --workers=1", + "test:e2e:debug": "NODE_EXTRA_CA_CERTS=./.wp-env/certs/rootCA.pem playwright test --config tests/e2e/playwright.config.js --ui", "test:unit": "wp-env run tests-cli --env-cwd=\"wp-content/plugins/$(basename \"$PWD\")\" vendor/bin/phpunit" }, "lint-staged": { diff --git a/tests/e2e/global-setup.js b/tests/e2e/global-setup.js index 9a241ce97..5eb0f0796 100644 --- a/tests/e2e/global-setup.js +++ b/tests/e2e/global-setup.js @@ -26,7 +26,7 @@ module.exports = async function globalSetup( config ) { fs.mkdirSync( path.dirname( storageStatePath ), { recursive: true } ); const requestContext = await request.newContext( { - baseURL: baseURL || 'http://localhost:8889', + baseURL: baseURL || 'https://tests.cloudinary.local.wpenv.net', } ); const requestUtils = new RequestUtils( requestContext, { diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index 00605eb83..135f140ed 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -2,6 +2,7 @@ * External dependencies */ const { defineConfig, devices } = require( '@playwright/test' ); +const fs = require( 'fs' ); const path = require( 'path' ); // Load env vars from a project-root .env file so devs don't have to @@ -18,6 +19,39 @@ const STORAGE_STATE_PATH = process.env.STORAGE_STATE_PATH || path.join( process.cwd(), 'artifacts/storage-states/admin.json' ); +// @wordpress/e2e-test-utils-playwright reads WP_BASE_URL from the environment +// rather than from Playwright's `baseURL`, and falls back to +// http://localhost:8889 (see its build/config.js). Setting the variable here +// keeps the URL defined in one place: RequestUtils, the storage state and the +// browser contexts all agree, and a stale localhost default cannot send +// requests around the proxy. +const BASE_URL = + process.env.WP_BASE_URL || 'https://tests.cloudinary.local.wpenv.net'; + +process.env.WP_BASE_URL = BASE_URL; + +// The local environment is served over HTTPS by the proxy in .wp-env/proxy/, +// using a certificate from the locally generated CA. Chromium trusts it via the +// OS keychain (`npm run env:install-cert`), but Playwright's Node-side +// APIRequestContext -- which globalSetup uses to authenticate -- ships its own +// CA bundle and ignores the keychain. +// +// NODE_EXTRA_CA_CERTS is read once when Node starts, so it cannot be set from +// here; the test:e2e npm scripts export it instead. Fail loudly rather than let +// the run die later inside globalSetup with an opaque TLS error. +const LOCAL_CA_PATH = path.join( process.cwd(), '.wp-env/certs/rootCA.pem' ); + +if ( + BASE_URL.startsWith( 'https://' ) && + ! process.env.NODE_EXTRA_CA_CERTS && + fs.existsSync( LOCAL_CA_PATH ) +) { + throw new Error( + 'NODE_EXTRA_CA_CERTS is not set, so Node cannot verify the local HTTPS certificate.\n' + + 'Run the suite with `npm run test:e2e`, which sets it for you.' + ); +} + module.exports = defineConfig( { testDir: '.', reporter: process.env.CI ? [ [ 'github' ], [ 'list' ] ] : 'list', @@ -38,7 +72,7 @@ module.exports = defineConfig( { outputDir: path.join( process.cwd(), 'artifacts/test-results' ), globalSetup: require.resolve( './global-setup.js' ), use: { - baseURL: process.env.WP_BASE_URL || 'http://localhost:8889', + baseURL: BASE_URL, trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', From c111fecf2a120a5eb80df3bd50aed5fad48e51f5 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Mon, 21 Sep 2026 16:32:40 +0530 Subject: [PATCH 2/4] fix(env): Scope proxy scripts to this project and its ports Three problems found in review of the HTTPS proxy scripts. Container lookup matched on a name pattern, but wp-env derives its Compose project name from a hash of the config path, so the pattern was loose enough to match other projects. The scripts edited /etc/hosts and the certificate store of an unrelated container. Identify containers by the bind mount of this repository instead. sed -i cannot write /etc/hosts, because Docker bind-mounts it and sed works by renaming a temporary file over the target. The failure was discarded, so stale host entries accumulated while the script reported success. Filter through a temporary file and copy the contents back. Upstream ports were hard-coded, so a developer using wp-env's supported port overrides got an nginx upstream error. Read the port from the environment, then the config files, then fall back to the defaults. --- .wp-env/scripts/config.sh | 71 ++++++++++++++++++++++++++++++--- .wp-env/scripts/fix-loopback.sh | 30 ++++++++------ .wp-env/scripts/trust-ca.sh | 28 ++++++++----- 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/.wp-env/scripts/config.sh b/.wp-env/scripts/config.sh index e2051c517..abd8d745f 100755 --- a/.wp-env/scripts/config.sh +++ b/.wp-env/scripts/config.sh @@ -17,11 +17,6 @@ TESTS_HOST="tests.cloudinary.local.wpenv.net" PROXY_HTTP_PORT=80 PROXY_HTTPS_PORT=443 -# Ports wp-env publishes for the development and tests sites. The proxy sends -# traffic to these over plain HTTP inside the Docker host network. -WP_ENV_PORT=8888 -WP_ENV_TESTS_PORT=8889 - # Compose project name for the proxy stack. Deliberately distinct from the # wp-env project so `wp-env destroy` cannot take the proxy with it. PROXY_PROJECT="cloudinary-wp-env-proxy" @@ -31,3 +26,69 @@ PROXY_PROJECT="cloudinary-wp-env-proxy" WP_ENV_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CERTS_DIR="$WP_ENV_DIR/certs" PROXY_DIR="$WP_ENV_DIR/proxy" +PROJECT_DIR="$(cd "$WP_ENV_DIR/.." && pwd)" + +# Ports wp-env publishes for the development and tests sites. The proxy forwards +# to these over plain HTTP through the Docker host gateway. +# +# wp-env supports overriding them through the WP_ENV_PORT and WP_ENV_TESTS_PORT +# environment variables and through a "port" key in .wp-env.json or +# .wp-env.override.json. Respect an already-exported variable, then fall back to +# the config files, and only then to wp-env's own defaults. A hard-coded value +# here would leave nginx forwarding to a port nothing listens on, which surfaces +# as an upstream error rather than as an obvious misconfiguration. +read_configured_port() { + local key="$1" + local file + + for file in "$PROJECT_DIR/.wp-env.override.json" "$PROJECT_DIR/.wp-env.json"; do + if [ ! -f "$file" ]; then + continue + fi + + local value + value=$(node -e " + try { + const config = require('$file'); + const port = $key; + if ( Number.isInteger( port ) ) { + process.stdout.write( String( port ) ); + } + } catch ( error ) {} + " 2>/dev/null) + + if [ -n "$value" ]; then + echo "$value" + return + fi + done +} + +WP_ENV_PORT="${WP_ENV_PORT:-$(read_configured_port 'config.port')}" +WP_ENV_PORT="${WP_ENV_PORT:-8888}" + +WP_ENV_TESTS_PORT="${WP_ENV_TESTS_PORT:-$(read_configured_port 'config.env && config.env.tests && config.env.tests.port')}" +WP_ENV_TESTS_PORT="${WP_ENV_TESTS_PORT:-8889}" + +# Prints the names of this project's wp-env containers, one per line. +# +# Identifies them by the bind mount of this repository, which wp-env adds to +# every WordPress and CLI container as the plugin directory. Matching on the +# container name is not safe: wp-env derives its Compose project name from a +# hash of the config path, so a pattern loose enough to match it also matches +# other projects' containers, and these scripts would then edit /etc/hosts and +# the certificate store of an unrelated environment. +# +# Only the WordPress and CLI services carry this mount, so the database +# containers, which need neither the host alias nor the certificate authority, +# are excluded automatically. +wp_env_containers() { + local container + + for container in $(docker ps --format '{{.Names}}'); do + if docker inspect "$container" --format '{{json .Mounts}}' 2>/dev/null | + grep -q "\"Source\":\"$PROJECT_DIR\""; then + echo "$container" + fi + done +} diff --git a/.wp-env/scripts/fix-loopback.sh b/.wp-env/scripts/fix-loopback.sh index 423fc6265..1f7684440 100755 --- a/.wp-env/scripts/fix-loopback.sh +++ b/.wp-env/scripts/fix-loopback.sh @@ -1,16 +1,22 @@ #!/bin/bash -# Fix loopback requests in wp-env Docker environment. +# Fix plain-HTTP loopback requests in the wp-env Docker environment. # -# WordPress in wp-env thinks its URL is localhost:8888, but inside the -# container Apache only listens on port 80. This causes self-pinging -# REST API requests (used by Cloudinary's sync daemon) to fail with -# cURL error 7. Adding port 8888 to Apache resolves this. +# wp-env publishes WordPress on port 8888, but inside the container Apache +# only listens on port 80. Self-pinging REST API requests (used by +# Cloudinary's sync daemon) that target the published port therefore fail +# with cURL error 7. Adding port 8888 to Apache resolves this. # -# This script finds the wp-env WordPress container and configures Apache -# to also listen on port 8888, enabling loopback requests to succeed. +# The environment now runs over HTTPS through the proxy in .wp-env/proxy/, +# where loopback goes through the proxy instead. This fix stays because the +# published port remains reachable as a fallback, and because the CI +# environment (.wp-env.ci.json) runs on plain HTTP and relies on it. -# Find the wp-env wordpress container (exclude tests container). -CONTAINER=$(docker ps --format '{{.Names}}' | grep -E 'wordpress-1$' | grep -v tests | head -1) +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +# The development WordPress container. wp_env_containers scopes the search to +# this project; see config.sh for why the container name cannot be matched +# directly. +CONTAINER=$(wp_env_containers | grep -v -- '-tests-' | grep -- '-wordpress-' | head -1) if [ -z "$CONTAINER" ]; then echo "Warning: Could not find wp-env WordPress container. Loopback fix skipped." @@ -18,10 +24,8 @@ if [ -z "$CONTAINER" ]; then fi # Add Listen 8888 if not already present, then graceful restart Apache. -docker exec "$CONTAINER" bash -c \ - "grep -q 'Listen 8888' /etc/apache2/ports.conf || (echo 'Listen 8888' >> /etc/apache2/ports.conf && apache2ctl graceful)" 2>/dev/null - -if [ $? -eq 0 ]; then +if docker exec "$CONTAINER" bash -c \ + "grep -q 'Listen 8888' /etc/apache2/ports.conf || (echo 'Listen 8888' >> /etc/apache2/ports.conf && apache2ctl graceful)" 2>/dev/null; then echo "Loopback fix applied: Apache now also listens on port 8888 inside the container." else echo "Warning: Failed to apply loopback fix." diff --git a/.wp-env/scripts/trust-ca.sh b/.wp-env/scripts/trust-ca.sh index af1670346..a16ac89e9 100755 --- a/.wp-env/scripts/trust-ca.sh +++ b/.wp-env/scripts/trust-ca.sh @@ -27,11 +27,9 @@ if [ ! -f "$CA_FILE" ]; then exit 0 fi -# The wp-env containers. The CLI containers are included so WP-CLI commands and -# the PHPUnit suite reach the site over HTTPS too. -CONTAINER_PATTERNS='wordpress-1$|cli-1$' - -CONTAINERS=$(docker ps --format '{{.Names}}' | grep -E "$CONTAINER_PATTERNS" || true) +# This project's WordPress and CLI containers. The CLI containers are included +# so WP-CLI commands and the PHPUnit suite reach the site over HTTPS too. +CONTAINERS=$(wp_env_containers) if [ -z "$CONTAINERS" ]; then echo "Warning: no wp-env containers found. Loopback trust setup skipped." @@ -51,10 +49,22 @@ for container in $CONTAINERS; do # Replace any previous entry so a changed gateway address cannot leave a # stale line behind, then append the current one. - docker exec --user root "$container" bash -c " - sed -i '/$DEV_HOST/d; /$TESTS_HOST/d' /etc/hosts - echo '$gateway $DEV_HOST $TESTS_HOST' >> /etc/hosts - " 2>/dev/null || echo "Warning: could not update /etc/hosts in $container." + # + # Docker bind-mounts /etc/hosts, so `sed -i` fails with "Device or resource + # busy": it works by renaming a temporary file over the target. Filter into + # a temporary file and copy the contents back instead, which writes through + # the existing inode. Errors are surfaced rather than discarded, because a + # silent failure here leaves a stale address behind and breaks loopback in a + # way that is hard to trace back to this script. + if ! docker exec --user root "$container" bash -c " + set -e + grep -v -e '$DEV_HOST' -e '$TESTS_HOST' /etc/hosts > /tmp/hosts.new + echo '$gateway $DEV_HOST $TESTS_HOST' >> /tmp/hosts.new + cat /tmp/hosts.new > /etc/hosts + rm -f /tmp/hosts.new + "; then + echo "Warning: could not update /etc/hosts in $container." + fi # update-ca-certificates rebuilds the bundle that both PHP and curl read. docker cp "$CA_FILE" "$container:/usr/local/share/ca-certificates/mkcert-root-ca.crt" >/dev/null 2>&1 || { From dc3c977672993694b0d573f1e310adc7643df0b7 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Mon, 21 Sep 2026 16:58:21 +0530 Subject: [PATCH 3/4] fix(ci): Install unit job dependencies from the lockfile The unit job installed @wordpress/env on its own into a scratch prefix to avoid a full npm ci. That install resolved dependency ranges fresh against npm rather than obeying package-lock.json, so the job depended on whatever upstream had published that day. A broken @wp-playground/cli 3.1.55 release, pulled in transitively by @wordpress/env, pinned a @php-wasm/node-8-1 version that was never published. Every run of the job failed while the lockfile-based jobs were unaffected. Use npm ci, which installs the locked tree and cannot break because of a third-party release. Its postinstall hook also runs composer install, so the separate Composer step is no longer needed. Enable npm caching, which this job alone was missing and which explains the slow npm ci timings that motivated the original workaround. Also stop exporting NODE_EXTRA_CA_CERTS unconditionally for e2e runs. CI has no certificate, so Node logged "Ignoring extra certs" on every run; a small wrapper now sets it only when the file exists. --- .github/workflows/ci.yml | 41 ++++++++++++++++++---------------- .wp-env/scripts/run-e2e.sh | 25 +++++++++++++++++++++ README.md | 26 ++++++++------------- package.json | 6 ++--- tests/e2e/playwright.config.js | 5 +++-- 5 files changed, 62 insertions(+), 41 deletions(-) create mode 100755 .wp-env/scripts/run-e2e.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16c5dc588..5638fdb46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,6 +96,7 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' + cache: 'npm' - name: Cache Composer uses: actions/cache@v4 @@ -105,22 +106,24 @@ jobs: restore-keys: | ${{ runner.os }}-composer-unit- - # This job only needs vendor/bin/phpunit (from Composer) and the wp-env - # CLI. A full `npm ci` pulls ~2,200 packages and has taken anywhere from - # 36s to 7 minutes on hosted runners; @wordpress/env alone is ~400 - # packages and installs in ~30s. It is installed into a scratch prefix - # outside the repo so npm does not reconcile against package-lock.json - # and pull the whole tree anyway. The version is read from the lockfile - # so it cannot drift from what developers run locally. - - name: Install Composer dependencies - run: composer install --no-interaction --no-progress - - - name: Install wp-env - run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@wordpress/env'].version") - echo "Installing @wordpress/env@$version" - npm install --prefix "$RUNNER_TEMP/wp-env" --no-audit --no-fund "@wordpress/env@$version" - echo "$RUNNER_TEMP/wp-env/node_modules/.bin" >> "$GITHUB_PATH" + # This job only needs vendor/bin/phpunit and the wp-env CLI, so it used to + # install @wordpress/env on its own into a scratch prefix rather than run + # a full `npm ci`. That install resolved dependency ranges fresh against + # npm instead of obeying package-lock.json, which made the job depend on + # whatever upstream had published that day: a broken @wp-playground/cli + # release, pulled in transitively by @wordpress/env, failed every run + # while the lockfile-based jobs were unaffected. + # + # `npm ci` installs the locked tree, so the job can no longer break + # because of a third-party release. The postinstall hook runs + # `patch-package && composer install`, which is what provides + # vendor/bin/phpunit, so this single step covers both toolchains. + # + # Deliberately not `--ignore-scripts`, for the same reason as the e2e + # job below: besides skipping the Composer install, that flag made this + # step take 1m30s instead of ~36s from a cold cache. + - name: Install dependencies + run: npm ci # The wp-env sources directory is deliberately not cached. A restored # ~/.wp-env carries the previous run's install state, which skipped the @@ -129,14 +132,14 @@ jobs: # that .wp-env.json uses for local development. Runners have no local CA # and no proxy container, so they serve the site over plain HTTP. - name: Start wp-env - run: wp-env start --config .wp-env.ci.json + run: npx wp-env start --config .wp-env.ci.json - name: Run unit tests - run: wp-env run tests-cli --config .wp-env.ci.json --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit + run: npx wp-env run tests-cli --config .wp-env.ci.json --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit - name: Stop wp-env if: always() - run: wp-env stop --config .wp-env.ci.json + run: npx wp-env stop --config .wp-env.ci.json e2e: name: E2E (Playwright) diff --git a/.wp-env/scripts/run-e2e.sh b/.wp-env/scripts/run-e2e.sh new file mode 100755 index 000000000..dc883032e --- /dev/null +++ b/.wp-env/scripts/run-e2e.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Run Playwright with the local certificate authority trusted, when there is one. +# +# Chromium trusts the local CA through the OS keychain (`npm run +# env:install-cert`), but Playwright's Node-side APIRequestContext, which +# globalSetup uses to authenticate, ships its own CA bundle and ignores the +# keychain. NODE_EXTRA_CA_CERTS points Node at the CA. +# +# The variable is only exported when the file is actually present. Node prints +# "Ignoring extra certs ... No such file or directory" when it is not, which is +# noise in CI, where the suite runs over plain HTTP and no certificate exists. +# +# Arguments are passed through to `playwright test`. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +CA_FILE="$CERTS_DIR/rootCA.pem" + +if [ -z "${NODE_EXTRA_CA_CERTS:-}" ] && [ -f "$CA_FILE" ]; then + export NODE_EXTRA_CA_CERTS="$CA_FILE" +fi + +exec npx playwright test --config tests/e2e/playwright.config.js "$@" diff --git a/README.md b/README.md index bb1db96f8..5c3a10b65 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,7 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), npm run env:start ``` - This spins up a WordPress instance at [https://cloudinary.local.wpenv.net](https://cloudinary.local.wpenv.net) with the plugin activated and `WP_DEBUG` enabled, plus a tests instance at [https://tests.cloudinary.local.wpenv.net](https://tests.cloudinary.local.wpenv.net). - - The site is served over HTTPS by an nginx proxy that `npm run env:start` brings up alongside wp-env. The environment runs over TLS by default because several code paths behave differently under HTTPS: `is_ssl()` decides the delivery URL scheme, auth cookies only get the `Secure` flag on HTTPS, and the admin enforces `FORCE_SSL_ADMIN`. Testing on plain HTTP hides those differences until production. - - Loopback REST API self-requests are configured automatically, and they verify the certificate rather than skipping the check, so they exercise the same code path as production. + This spins up a WordPress instance at [https://cloudinary.local.wpenv.net](https://cloudinary.local.wpenv.net) with the plugin activated and `WP_DEBUG` enabled, plus a tests instance at [https://tests.cloudinary.local.wpenv.net](https://tests.cloudinary.local.wpenv.net). An nginx proxy serves both over HTTPS, so local behaviour matches production for `is_ssl()`, `Secure` cookies and loopback requests. No `/etc/hosts` entry is needed. 5. **Trust the local certificate (first run only):** @@ -87,9 +83,7 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), npm run env:install-cert ``` - This adds the locally generated certificate authority to your OS trust store, so the browser accepts `*.cloudinary.local.wpenv.net` without a warning. It asks for your password, because changing the system trust store requires it. The certificate and its CA are created on first `npm run env:start` and live in `.wp-env/certs/`, which is gitignored. - - No `/etc/hosts` entry is needed: `local.wpenv.net` and all of its subdomains resolve to `127.0.0.1` over public DNS. + Adds the generated certificate authority to your OS trust store, so the browser accepts the site without a warning. It asks for your password. Certificates live in `.wp-env/certs/`, which is gitignored. 6. **Build front-end assets:** @@ -123,13 +117,13 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), ### Troubleshooting the local environment -**The browser warns that the certificate is not trusted.** Run `npm run env:install-cert`. If the warning persists, delete `.wp-env/certs/`, run `npm run env:start` to reissue the certificate, then trust it again. +| Symptom | Fix | +| ------- | --- | +| Browser warns the certificate is untrusted | `npm run env:install-cert`. If it persists, delete `.wp-env/certs/`, run `npm run env:start`, then trust it again. | +| Port 80 or 443 is in use | Another project holds it; the error names the container. Stop it, then `npm run env:proxy:up`. | +| `Call to undefined function Cloudinary\get_plugin_instance()` | The plugin is inactive after switching between `.wp-env.json` and `.wp-env.ci.json`. Destroy and start again under the same config. | -**`npm run env:start` reports that port 80 or 443 is in use.** Another local project holds the port. The message names the container or process. Stop it, then run `npm run env:proxy:up` to finish starting the proxy. - -**`wp-env stop` leaves the proxy running.** The proxy is a separate Docker Compose project, so wp-env does not manage it. `npm run env:stop` and `npm run env:destroy` stop it for you; `npm run env:proxy:down` does it on its own. - -**CI uses a different config.** GitHub runners have no certificate authority and no proxy, so the workflow passes `--config .wp-env.ci.json`, which is the same environment without the HTTPS URLs. Keep the shared values in both files in sync. +CI runs over plain HTTP via `--config .wp-env.ci.json`, because runners have no certificate authority. Keep the shared values in both config files in sync. ### Create a Plugin Release Package @@ -171,9 +165,7 @@ npm run env:install-cert npm run test:e2e ``` -The suite runs against the HTTPS tests site at `https://tests.cloudinary.local.wpenv.net`. Certificates are verified rather than ignored, so a broken certificate fails the run instead of passing silently. Always start the suite through the npm scripts: they set `NODE_EXTRA_CA_CERTS`, which Playwright's Node-side request client needs because it does not read the OS trust store. - -To run against a different site, set `WP_BASE_URL`. CI uses this to run over plain HTTP, because GitHub runners have no local certificate authority: +The suite runs against the HTTPS tests site. Start it through the npm scripts, which point Node at the local certificate authority. Override the target with `WP_BASE_URL`: ```bash WP_BASE_URL=http://localhost:8889 npm run test:e2e diff --git a/package.json b/package.json index 388033375..345c33435 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "readme": "composer readme", "prepare": "husky", "test:e2e": "npm-run-all --silent test:e2e:parallel test:e2e:serial", - "test:e2e:parallel": "NODE_EXTRA_CA_CERTS=./.wp-env/certs/rootCA.pem playwright test --config tests/e2e/playwright.config.js --grep-invert @serial", - "test:e2e:serial": "NODE_EXTRA_CA_CERTS=./.wp-env/certs/rootCA.pem playwright test --config tests/e2e/playwright.config.js --grep @serial --workers=1", - "test:e2e:debug": "NODE_EXTRA_CA_CERTS=./.wp-env/certs/rootCA.pem playwright test --config tests/e2e/playwright.config.js --ui", + "test:e2e:parallel": "./.wp-env/scripts/run-e2e.sh --grep-invert @serial", + "test:e2e:serial": "./.wp-env/scripts/run-e2e.sh --grep @serial --workers=1", + "test:e2e:debug": "./.wp-env/scripts/run-e2e.sh --ui", "test:unit": "wp-env run tests-cli --env-cwd=\"wp-content/plugins/$(basename \"$PWD\")\" vendor/bin/phpunit" }, "lint-staged": { diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index 135f140ed..8fd788edf 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -37,8 +37,9 @@ process.env.WP_BASE_URL = BASE_URL; // CA bundle and ignores the keychain. // // NODE_EXTRA_CA_CERTS is read once when Node starts, so it cannot be set from -// here; the test:e2e npm scripts export it instead. Fail loudly rather than let -// the run die later inside globalSetup with an opaque TLS error. +// here; .wp-env/scripts/run-e2e.sh exports it before launching Playwright. +// Fail loudly rather than let the run die later inside globalSetup with an +// opaque TLS error. const LOCAL_CA_PATH = path.join( process.cwd(), '.wp-env/certs/rootCA.pem' ); if ( From 1484b739a4be22eb7aa788d36f20d7f4ddef03f4 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 22 Sep 2026 13:43:08 +0530 Subject: [PATCH 4/4] fix(env): Respect custom wp-env ports in loopback and URL rewrite PR review found two places that still assumed the default ports after this branch made them configurable. The Apache listener was hard-coded to 8888, so with a custom port wp-env published one port while Apache opened another and loopback requests failed. It now uses the resolved port from config.sh. The URL filter matched a literal 8888 or 8889, so a custom port stayed in every generated URL. It now derives the public origin from WP_CONTENT_URL, which wp-env never rewrites, and replaces whatever port is present. It also leaves URLs on other hosts untouched, which the previous pattern did not guard against. --- .wp-env/mu-plugins/https-proxy.php | 36 +++++++++++++++++++++++++----- .wp-env/scripts/fix-loopback.sh | 15 +++++++------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.wp-env/mu-plugins/https-proxy.php b/.wp-env/mu-plugins/https-proxy.php index 0b7a4a375..83e3af333 100644 --- a/.wp-env/mu-plugins/https-proxy.php +++ b/.wp-env/mu-plugins/https-proxy.php @@ -54,8 +54,12 @@ * cookie path. Stripping it here is the last chance to correct the value, * because the constants are already defined by the time mu-plugins load. * - * Only ports wp-env itself publishes are removed, so a developer who - * deliberately runs on a custom port keeps it. + * The port to remove is not hard-coded, because wp-env lets developers change + * it through WP_ENV_PORT or a "port" key in the config files. The origin is + * taken from WP_CONTENT_URL instead, which .wp-env.json defines as the intended + * public URL and which wp-env never rewrites. Any port on the incoming URL is + * then replaced with whatever that constant says, so a custom wp-env port is + * handled without this file knowing about it. * * This filter cannot fix asset URLs. wp_plugin_directory_constants() defines * WP_CONTENT_URL and WP_PLUGIN_URL from get_option( 'siteurl' ) at @@ -65,14 +69,36 @@ * WP_HOME, WP_SITEURL and WP_TESTS_DOMAIN. * * @param string $url The home or site URL. - * @return string The URL without the wp-env port. + * @return string The URL with the wp-env port replaced by the public origin. */ function cld_strip_wp_env_port( $url ) { - if ( ! is_string( $url ) || 0 !== strpos( $url, 'https://' ) ) { + if ( ! is_string( $url ) || ! defined( 'WP_CONTENT_URL' ) ) { return $url; } - return preg_replace( '#^(https://[^/:]+):(?:8888|8889)#', '$1', $url ); + $origin = wp_parse_url( WP_CONTENT_URL ); + + if ( empty( $origin['scheme'] ) || empty( $origin['host'] ) ) { + return $url; + } + + $parts = wp_parse_url( $url ); + + // Only rewrite URLs that point at the same host, so an unrelated URL + // passing through these filters is left alone. + if ( empty( $parts['host'] ) || $parts['host'] !== $origin['host'] ) { + return $url; + } + + $public = $origin['scheme'] . '://' . $origin['host']; + + if ( ! empty( $origin['port'] ) ) { + $public .= ':' . $origin['port']; + } + + $path = isset( $parts['path'] ) ? $parts['path'] : ''; + + return $public . $path; } add_filter( 'option_home', 'cld_strip_wp_env_port', 20 ); diff --git a/.wp-env/scripts/fix-loopback.sh b/.wp-env/scripts/fix-loopback.sh index 1f7684440..abb5e35f8 100755 --- a/.wp-env/scripts/fix-loopback.sh +++ b/.wp-env/scripts/fix-loopback.sh @@ -1,10 +1,10 @@ #!/bin/bash # Fix plain-HTTP loopback requests in the wp-env Docker environment. # -# wp-env publishes WordPress on port 8888, but inside the container Apache -# only listens on port 80. Self-pinging REST API requests (used by -# Cloudinary's sync daemon) that target the published port therefore fail -# with cURL error 7. Adding port 8888 to Apache resolves this. +# wp-env publishes WordPress on a host port (8888 by default), but inside the +# container Apache only listens on port 80. Self-pinging REST API requests +# (used by Cloudinary's sync daemon) that target the published port therefore +# fail with cURL error 7. Adding that port to Apache resolves this. # # The environment now runs over HTTPS through the proxy in .wp-env/proxy/, # where loopback goes through the proxy instead. This fix stays because the @@ -23,10 +23,11 @@ if [ -z "$CONTAINER" ]; then exit 0 fi -# Add Listen 8888 if not already present, then graceful restart Apache. +# Add the listener if not already present, then gracefully restart Apache. +# The port comes from config.sh, which resolves any wp-env port override. if docker exec "$CONTAINER" bash -c \ - "grep -q 'Listen 8888' /etc/apache2/ports.conf || (echo 'Listen 8888' >> /etc/apache2/ports.conf && apache2ctl graceful)" 2>/dev/null; then - echo "Loopback fix applied: Apache now also listens on port 8888 inside the container." + "grep -q 'Listen $WP_ENV_PORT' /etc/apache2/ports.conf || (echo 'Listen $WP_ENV_PORT' >> /etc/apache2/ports.conf && apache2ctl graceful)" 2>/dev/null; then + echo "Loopback fix applied: Apache now also listens on port $WP_ENV_PORT inside the container." else echo "Warning: Failed to apply loopback fix." fi