generated from bisco/codex-bootstrap
Compare commits
4 Commits
482928a296
..
wp
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ec3c94dc | |||
| f75bce3dea | |||
| 472b8616b4 | |||
| 0762b98fc6 |
@@ -11,6 +11,7 @@ WP_TITLE=Azione!Lab
|
|||||||
WP_ADMIN_USER=azionelab-admin
|
WP_ADMIN_USER=azionelab-admin
|
||||||
WP_ADMIN_PASSWORD=replace-with-a-local-admin-password
|
WP_ADMIN_PASSWORD=replace-with-a-local-admin-password
|
||||||
WP_ADMIN_EMAIL=admin@example.org
|
WP_ADMIN_EMAIL=admin@example.org
|
||||||
|
WP_CRON_INTERVAL_SECONDS=300
|
||||||
NGINX_BIND_ADDRESS=127.0.0.1
|
NGINX_BIND_ADDRESS=127.0.0.1
|
||||||
NGINX_HTTP_PORT=8080
|
NGINX_HTTP_PORT=8080
|
||||||
NGINX_HTTPS_PORT=8443
|
NGINX_HTTPS_PORT=8443
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
A warm, editorial single-page website for the Azione!Lab contemporary theatre
|
A warm, editorial single-page website for the Azione!Lab contemporary theatre
|
||||||
workshop. WordPress manages the content, a custom theme owns the visual system, and
|
workshop. WordPress manages the content, a custom theme owns the visual system, and
|
||||||
Docker Compose provides MariaDB, NGINX, optional Let's Encrypt, WP-CLI, and isolated
|
Docker Compose provides MariaDB, NGINX, an internal WordPress cron runner, optional
|
||||||
browser tests.
|
Let's Encrypt, WP-CLI, and isolated browser tests.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ browser tests.
|
|||||||
- `db`: MariaDB 11.8 LTS, available only on the internal data network.
|
- `db`: MariaDB 11.8 LTS, available only on the internal data network.
|
||||||
- `proxy`: the only published service; routes `azionelab.org` to WordPress and applies
|
- `proxy`: the only published service; routes `azionelab.org` to WordPress and applies
|
||||||
edge security controls.
|
edge security controls.
|
||||||
|
- `wp-cron`: internal WP-CLI runner for due scheduled WordPress events.
|
||||||
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
||||||
- `wp-cli`: opt-in bootstrap and maintenance service.
|
- `wp-cli`: opt-in bootstrap and maintenance service.
|
||||||
- `tests/functional`: Playwright tests running only through the public virtual host.
|
- `tests/functional`: Playwright tests running only through the public virtual host.
|
||||||
@@ -61,6 +62,7 @@ Use this checklist for the first run of a new environment.
|
|||||||
|
|
||||||
- set strong `MARIADB_PASSWORD`, `MARIADB_ROOT_PASSWORD`, and `WP_ADMIN_PASSWORD`;
|
- set strong `MARIADB_PASSWORD`, `MARIADB_ROOT_PASSWORD`, and `WP_ADMIN_PASSWORD`;
|
||||||
- set `WP_URL` to the public URL, for example `https://lab.younerd.org`;
|
- set `WP_URL` to the public URL, for example `https://lab.younerd.org`;
|
||||||
|
- keep `WP_CRON_INTERVAL_SECONDS=300` unless scheduled jobs need a different cadence;
|
||||||
- set `LETSENCRYPT_DOMAIN` to the same hostname when direct Let's Encrypt is used;
|
- set `LETSENCRYPT_DOMAIN` to the same hostname when direct Let's Encrypt is used;
|
||||||
- keep `LETSENCRYPT_STAGING=1` for the first certificate test;
|
- keep `LETSENCRYPT_STAGING=1` for the first certificate test;
|
||||||
- configure `DB_DATA_PATH`, `WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and
|
- configure `DB_DATA_PATH`, `WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and
|
||||||
@@ -127,7 +129,7 @@ Use this checklist for the first run of a new environment.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps
|
docker compose ps
|
||||||
docker compose logs --tail=100 proxy wordpress db certbot
|
docker compose logs --tail=100 proxy wordpress wp-cron db certbot
|
||||||
```
|
```
|
||||||
|
|
||||||
Open the configured `WP_URL` and `/wp-admin/`.
|
Open the configured `WP_URL` and `/wp-admin/`.
|
||||||
@@ -212,12 +214,35 @@ docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
|||||||
The `docker-compose.test.yml` override intentionally uses disposable test volumes; do
|
The `docker-compose.test.yml` override intentionally uses disposable test volumes; do
|
||||||
not use it for production or staging bootstrap commands.
|
not use it for production or staging bootstrap commands.
|
||||||
|
|
||||||
|
`wp-cron` runs internally through WP-CLI every `WP_CRON_INTERVAL_SECONDS` seconds. It
|
||||||
|
does not publish ports and does not join the public `web` network. Public
|
||||||
|
`/wp-cron.php` requests remain blocked intentionally.
|
||||||
|
|
||||||
|
## Maintenance commands with internet access
|
||||||
|
|
||||||
|
The default `wp-cli` service joins only the internal database network, so it cannot
|
||||||
|
download packages from the internet. For controlled maintenance commands that need
|
||||||
|
egress, such as a WordPress core update, include the maintenance override:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update --version=7.0.2 --force'
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update-db'
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||||
|
docker compose restart wordpress proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this override only for maintenance operations that genuinely need outbound
|
||||||
|
network access. Normal bootstrap and inspection commands should keep using the base
|
||||||
|
Compose file.
|
||||||
|
|
||||||
## Useful commands
|
## Useful commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f proxy wordpress db certbot
|
docker compose logs -f proxy wordpress wp-cron db certbot
|
||||||
docker compose --profile tools run --rm wp-cli -c 'wp plugin list'
|
docker compose --profile tools run --rm wp-cli -c 'wp plugin list'
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp cron event list'
|
||||||
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update --version=7.0.2 --force'
|
||||||
|
|
||||||
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/functions.php
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/functions.php
|
||||||
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/front-page.php
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/front-page.php
|
||||||
@@ -228,6 +253,7 @@ docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools
|
|||||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --rm security-tests
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --rm security-tests
|
||||||
docker compose config --quiet
|
docker compose config --quiet
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml config --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
## Backup
|
## Backup
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Optional maintenance override.
|
||||||
|
#
|
||||||
|
# The default wp-cli service only joins the internal data network. Use this override
|
||||||
|
# when a controlled maintenance command must reach the internet, for example a
|
||||||
|
# WordPress core download from wordpress.org.
|
||||||
|
services:
|
||||||
|
wp-cli:
|
||||||
|
networks:
|
||||||
|
- data
|
||||||
|
- web
|
||||||
@@ -17,6 +17,13 @@ services:
|
|||||||
- test_wordpress_data:/var/www/html
|
- test_wordpress_data:/var/www/html
|
||||||
- ./wp-cli/bootstrap.sh:/scripts/bootstrap.sh:ro
|
- ./wp-cli/bootstrap.sh:/scripts/bootstrap.sh:ro
|
||||||
|
|
||||||
|
wp-cron:
|
||||||
|
environment:
|
||||||
|
WP_CRON_INTERVAL_SECONDS: 300
|
||||||
|
volumes:
|
||||||
|
- test_wordpress_data:/var/www/html
|
||||||
|
- ./wp-cli/cron.sh:/scripts/cron.sh:ro
|
||||||
|
|
||||||
proxy:
|
proxy:
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ services:
|
|||||||
$$_SERVER['HTTPS'] = 'on';
|
$$_SERVER['HTTPS'] = 'on';
|
||||||
}
|
}
|
||||||
define('DISALLOW_FILE_EDIT', true);
|
define('DISALLOW_FILE_EDIT', true);
|
||||||
|
define('DISABLE_WP_CRON', true);
|
||||||
define('WP_AUTO_UPDATE_CORE', 'minor');
|
define('WP_AUTO_UPDATE_CORE', 'minor');
|
||||||
define('WP_POST_REVISIONS', 10);
|
define('WP_POST_REVISIONS', 10);
|
||||||
if (getenv('WP_ENVIRONMENT_TYPE') === 'production') {
|
if (getenv('WP_ENVIRONMENT_TYPE') === 'production') {
|
||||||
@@ -80,6 +81,47 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
pids_limit: 300
|
pids_limit: 300
|
||||||
|
|
||||||
|
wp-cron:
|
||||||
|
image: wordpress:cli-2.12.0-php8.3
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
user: "33:33"
|
||||||
|
working_dir: /var/www/html
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: db:3306
|
||||||
|
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-azionelab}
|
||||||
|
WORDPRESS_DB_USER: ${MARIADB_USER:-azionelab}
|
||||||
|
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:-replace-with-a-local-password}
|
||||||
|
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-azl_}
|
||||||
|
WP_CRON_INTERVAL_SECONDS: ${WP_CRON_INTERVAL_SECONDS:-300}
|
||||||
|
WP_ENVIRONMENT_TYPE: ${WP_ENVIRONMENT_TYPE:-local}
|
||||||
|
WP_URL: ${WP_URL:-http://azionelab.org:8080}
|
||||||
|
HOME: /tmp
|
||||||
|
HTTP_HOST: ${LETSENCRYPT_DOMAIN:-azionelab.org}
|
||||||
|
entrypoint: ["/bin/sh"]
|
||||||
|
command: ["/scripts/cron.sh"]
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ${WORDPRESS_DATA_PATH:-./runtime/wordpress}
|
||||||
|
target: /var/www/html
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
- ./wp-cli/cron.sh:/scripts/cron.sh:ro
|
||||||
|
networks:
|
||||||
|
- data
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
wordpress:
|
||||||
|
condition: service_healthy
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
pids_limit: 100
|
||||||
|
|
||||||
proxy:
|
proxy:
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
|
|||||||
@@ -12,15 +12,17 @@ architecture and starts independently from `main`.
|
|||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
Use pinned WordPress 7.0/PHP 8.3 with MariaDB 11.8 LTS. Build a dependency-free custom
|
Use pinned WordPress 7.0.2/PHP 8.3 with MariaDB 11.8 LTS. Build a dependency-free custom
|
||||||
classic theme for exact semantic markup and responsive design. Store homepage fields as
|
classic theme for exact semantic markup and responsive design. Store homepage fields as
|
||||||
sanitized theme modifications, while a must-use plugin owns Shows and Gallery custom
|
sanitized theme modifications, while a must-use plugin owns Shows and Gallery custom
|
||||||
post types so structured content is not lost when changing themes.
|
post types so structured content is not lost when changing themes.
|
||||||
|
|
||||||
NGINX is the only public entry point. WP-CLI provides an idempotent opt-in bootstrap;
|
NGINX is the only public entry point. Public `wp-cron.php` requests stay blocked, while
|
||||||
Certbot provides opt-in direct TLS. Docker networks isolate the database. Runtime state
|
an internal WP-CLI based `wp-cron` service runs due scheduled events over the private
|
||||||
uses host-based bind mounts with a preparation script for ownership/mode, while
|
data network. WP-CLI provides an idempotent opt-in bootstrap; Certbot provides opt-in
|
||||||
functional tests use separate Docker volumes.
|
direct TLS. Docker networks isolate the database. Runtime state uses host-based bind
|
||||||
|
mounts with a preparation script for ownership/mode, while functional tests use
|
||||||
|
separate Docker volumes.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
@@ -30,6 +32,8 @@ functional tests use separate Docker volumes.
|
|||||||
- Core/theme/plugin patches require an image rebuild in production.
|
- Core/theme/plugin patches require an image rebuild in production.
|
||||||
- Fixed participation cards remain code until editorial requirements justify another
|
- Fixed participation cards remain code until editorial requirements justify another
|
||||||
structured content type.
|
structured content type.
|
||||||
|
- Scheduled WordPress jobs depend on the internal `wp-cron` service instead of visitor
|
||||||
|
traffic.
|
||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
|
|
||||||
@@ -43,8 +47,9 @@ functional tests use separate Docker volumes.
|
|||||||
|
|
||||||
The architecture inherits WordPress's public CMS attack surface. The implementation
|
The architecture inherits WordPress's public CMS attack surface. The implementation
|
||||||
reduces it with network isolation, no direct application port, disabled file editing
|
reduces it with network isolation, no direct application port, disabled file editing
|
||||||
and XML-RPC, production immutability, sanitization/escaping, rate limiting, security
|
and XML-RPC, production immutability, sanitized/escaped rendering, no bundled demo
|
||||||
headers, and fail-closed TLS. Admin MFA and allowlisting remain external controls.
|
plugins, internal-only scheduled jobs, rate limiting, security headers, and fail-closed
|
||||||
|
TLS. Admin MFA and allowlisting remain external controls.
|
||||||
|
|
||||||
## Operational impact
|
## Operational impact
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -1,20 +1,24 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
NGINX is the only public entry point for `azionelab.org`. It proxies HTTP to the
|
NGINX is the only public entry point for `azionelab.org`. It proxies HTTP to the
|
||||||
official WordPress 7.0/PHP 8.3 Apache image over the private `web` network. WordPress
|
official WordPress 7.0.2/PHP 8.3 Apache image over the private `web` network. WordPress
|
||||||
connects to MariaDB 11.8 LTS over a separate internal `data` network. Neither WordPress
|
connects to MariaDB 11.8 LTS over a separate internal `data` network. Neither WordPress
|
||||||
nor MariaDB publishes a host port; automated security checks guard this assumption.
|
nor MariaDB publishes a host port; automated security checks guard this assumption.
|
||||||
|
|
||||||
The custom `azionelab` classic theme renders the public single page. Theme modifications
|
The custom `azionelab` classic theme renders the public single page. Theme modifications
|
||||||
store the hero, manifesto, laboratory, teacher, lesson, and contact fields. The
|
store the hero, manifesto, laboratory, teacher, lesson, and contact fields. The
|
||||||
`azionelab-content` must-use plugin registers Shows and Gallery custom post types so
|
`azionelab-content` must-use plugin registers Shows and Gallery custom post types so
|
||||||
structured editorial content survives a theme change. Images use WordPress featured
|
structured editorial content survives a theme change. These custom post types are
|
||||||
images with local SVG fallbacks.
|
editorial data sources for the homepage, not standalone public routes or REST
|
||||||
|
collections. Images use WordPress featured images with local SVG fallbacks.
|
||||||
|
|
||||||
WP-CLI is an opt-in tools-profile service. Its idempotent bootstrap installs WordPress,
|
`wp-cron` is an internal WP-CLI runner on the `data` network. It executes due scheduled
|
||||||
activates the theme, configures the site, and creates realistic demo content. Certbot is
|
events with `wp cron event run --due-now` and keeps public `wp-cron.php` requests
|
||||||
another optional service, enabled only for direct deployments. It shares challenge and
|
blocked. WP-CLI is also available as an opt-in tools-profile service. Its idempotent
|
||||||
certificate volumes with NGINX but has no container-control access.
|
bootstrap installs WordPress, activates the theme, configures the site, and creates
|
||||||
|
realistic demo content. Certbot is another optional service, enabled only for direct
|
||||||
|
deployments. It shares challenge and certificate volumes with NGINX but has no
|
||||||
|
container-control access.
|
||||||
|
|
||||||
Apache includes a small defense-in-depth hardening file that denies uploaded PHP files,
|
Apache includes a small defense-in-depth hardening file that denies uploaded PHP files,
|
||||||
direct `wp-config.php` requests, and direct access to selected internal WordPress PHP
|
direct `wp-config.php` requests, and direct access to selected internal WordPress PHP
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
|||||||
|
|
||||||
NGINX binds to loopback ports 8080/8443. WordPress and MariaDB remain private. The
|
NGINX binds to loopback ports 8080/8443. WordPress and MariaDB remain private. The
|
||||||
bootstrap is safe to rerun and does not duplicate demo records.
|
bootstrap is safe to rerun and does not duplicate demo records.
|
||||||
|
The internal `wp-cron` service runs scheduled WordPress events after the bootstrap
|
||||||
|
completes.
|
||||||
|
|
||||||
The default persistent paths are host-based bind mounts under `./runtime`. Before the
|
The default persistent paths are host-based bind mounts under `./runtime`. Before the
|
||||||
first start, run:
|
first start, run:
|
||||||
@@ -37,6 +39,9 @@ Required controls:
|
|||||||
- either direct Let's Encrypt termination or a trusted external load balancer;
|
- either direct Let's Encrypt termination or a trusted external load balancer;
|
||||||
- off-host database/file backups and monitoring.
|
- off-host database/file backups and monitoring.
|
||||||
|
|
||||||
|
The default `wp-cron` service handles scheduled WordPress events internally and should
|
||||||
|
remain enabled unless another controlled cron runner replaces it.
|
||||||
|
|
||||||
When a load balancer terminates TLS, Certbot stays disabled. `TRUST_PROXY_HEADERS=1`
|
When a load balancer terminates TLS, Certbot stays disabled. `TRUST_PROXY_HEADERS=1`
|
||||||
is safe only when firewall/network policy makes the load balancer the sole NGINX
|
is safe only when firewall/network policy makes the load balancer the sole NGINX
|
||||||
caller and it overwrites `X-Forwarded-*` headers.
|
caller and it overwrites `X-Forwarded-*` headers.
|
||||||
|
|||||||
+24
-1
@@ -5,9 +5,10 @@
|
|||||||
```bash
|
```bash
|
||||||
docker compose up --build -d
|
docker compose up --build -d
|
||||||
docker compose ps
|
docker compose ps
|
||||||
docker compose logs -f proxy wordpress db certbot
|
docker compose logs -f proxy wordpress wp-cron db certbot
|
||||||
./scripts/prepare-host-volumes.sh
|
./scripts/prepare-host-volumes.sh
|
||||||
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp cron event list'
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -23,6 +24,26 @@ Review security releases routinely, update pins in a task branch, rebuild, run t
|
|||||||
test suite, and deploy. Production disables WordPress web-based file modifications, so
|
test suite, and deploy. Production disables WordPress web-based file modifications, so
|
||||||
image rebuilds are the update path.
|
image rebuilds are the update path.
|
||||||
|
|
||||||
|
The default `wp-cli` service is attached only to the internal data network. When a
|
||||||
|
maintenance command must download from the internet, use
|
||||||
|
`docker-compose.maintenance.yml` explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update --version=7.0.2 --force'
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update-db'
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||||
|
docker compose restart wordpress proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use the maintenance override for routine bootstrap or inspection commands that
|
||||||
|
do not need outbound network access.
|
||||||
|
|
||||||
|
External `wp-cron.php` requests are blocked to reduce public attack surface. The
|
||||||
|
default `wp-cron` service runs due scheduled events internally through WP-CLI every
|
||||||
|
`WP_CRON_INTERVAL_SECONDS` seconds, defaults to 300, and joins only the internal `data`
|
||||||
|
network. If a scheduled job is late, inspect `docker compose logs wp-cron wordpress db`
|
||||||
|
and run `docker compose --profile tools run --rm wp-cli -c 'wp cron event list'`.
|
||||||
|
|
||||||
## Backup and restore
|
## Backup and restore
|
||||||
|
|
||||||
Create database and WordPress file backups in one maintenance window. The default host
|
Create database and WordPress file backups in one maintenance window. The default host
|
||||||
@@ -42,6 +63,8 @@ the database and file volume together, restart, and verify the homepage, media,
|
|||||||
owner or mode; run `./scripts/prepare-host-volumes.sh` after changing paths or image
|
owner or mode; run `./scripts/prepare-host-volumes.sh` after changing paths or image
|
||||||
user IDs.
|
user IDs.
|
||||||
- SMTP is not configured; WordPress password-reset email needs an external mail service.
|
- SMTP is not configured; WordPress password-reset email needs an external mail service.
|
||||||
|
- WordPress cron is not publicly triggerable; scheduled jobs depend on the internal
|
||||||
|
`wp-cron` runner being healthy.
|
||||||
- Admin MFA and network allowlisting are deployment concerns and are not bundled.
|
- Admin MFA and network allowlisting are deployment concerns and are not bundled.
|
||||||
- WordPress plugins expand the attack surface; install only reviewed, maintained,
|
- WordPress plugins expand the attack surface; install only reviewed, maintained,
|
||||||
necessary plugins.
|
necessary plugins.
|
||||||
|
|||||||
@@ -35,6 +35,54 @@ HTTPS redirects during production startup.
|
|||||||
2. Run `docker compose --profile tools run --rm wp-cli -c 'wp theme status azionelab'`.
|
2. Run `docker compose --profile tools run --rm wp-cli -c 'wp theme status azionelab'`.
|
||||||
3. Verify file ownership before changing permissions; never make the tree world-writable.
|
3. Verify file ownership before changing permissions; never make the tree world-writable.
|
||||||
|
|
||||||
|
## WP-CLI cannot resolve wordpress.org
|
||||||
|
|
||||||
|
The default WP-CLI container intentionally runs only on the internal data network. Use
|
||||||
|
the maintenance override for commands that must download WordPress core files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update --version=7.0.2 --force'
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update-db'
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||||
|
docker compose restart wordpress proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
If DNS still fails with the maintenance override, inspect the host/container DNS
|
||||||
|
configuration and firewall rules before retrying the update.
|
||||||
|
|
||||||
|
## Scheduled WordPress events are late
|
||||||
|
|
||||||
|
The public `/wp-cron.php` trigger is intentionally blocked. Scheduled jobs are executed
|
||||||
|
by the internal `wp-cron` service.
|
||||||
|
|
||||||
|
1. Confirm the runner is present:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps wp-cron
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Inspect due events:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp cron event list'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Inspect runner logs without printing `.env` values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs --tail=100 wp-cron wordpress db
|
||||||
|
```
|
||||||
|
|
||||||
|
4. If needed, run due events manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp cron event run --due-now'
|
||||||
|
```
|
||||||
|
|
||||||
|
The Site Health loopback test may still report a blocked public loopback while the edge
|
||||||
|
continues to deny `/wp-cron.php`; that is an expected trade-off of the hardened public
|
||||||
|
configuration.
|
||||||
|
|
||||||
## Uploaded image does not appear on the public page
|
## Uploaded image does not appear on the public page
|
||||||
|
|
||||||
1. Confirm the image was selected in **Appearance > Customize**, not only uploaded in
|
1. Confirm the image was selected in **Appearance > Customize**, not only uploaded in
|
||||||
|
|||||||
+16
-3
@@ -6,12 +6,19 @@
|
|||||||
ignored and real secrets must come from the deployment secret manager.
|
ignored and real secrets must come from the deployment secret manager.
|
||||||
- WordPress uses its normal capability, nonce, authentication, cookie, and password
|
- WordPress uses its normal capability, nonce, authentication, cookie, and password
|
||||||
controls. Theme settings sanitize input and templates escape output.
|
controls. Theme settings sanitize input and templates escape output.
|
||||||
|
- Structured Shows and Gallery content is editable in WordPress admin but is consumed
|
||||||
|
by the homepage only; it is not exposed as standalone public routes or REST
|
||||||
|
collections.
|
||||||
- File editing is always disabled. Production also disables web-based core, theme, and
|
- File editing is always disabled. Production also disables web-based core, theme, and
|
||||||
plugin changes; patched images are rebuilt and redeployed instead.
|
plugin changes; patched images are rebuilt and redeployed instead.
|
||||||
|
- The WordPress image removes the bundled Akismet and Hello Dolly plugins, and the
|
||||||
|
entrypoint also removes them from existing persistent volumes on startup.
|
||||||
- XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
|
- XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
|
||||||
and direct `wp-config.php` requests, and rate-limits login/public requests. Apache
|
direct `wp-config.php` requests, the WordPress readme/license files, direct
|
||||||
also denies uploaded PHP files and direct access to sensitive WordPress internals as
|
installation entry points, public mu-plugin directory probing, and external
|
||||||
defense in depth. Public REST user enumeration and author archives are disabled.
|
`wp-cron.php` requests. It also rate-limits login/public requests. Apache also denies
|
||||||
|
uploaded PHP files and direct access to sensitive WordPress internals as defense in
|
||||||
|
depth. Public REST user enumeration and author archives are disabled.
|
||||||
- Security headers include CSP, same-origin framing, content-type protection, a strict
|
- Security headers include CSP, same-origin framing, content-type protection, a strict
|
||||||
referrer policy, and a restrictive Permissions Policy. WordPress compatibility still
|
referrer policy, and a restrictive Permissions Policy. WordPress compatibility still
|
||||||
requires inline style/script CSP allowances; do not treat this CSP as an XSS sanitizer.
|
requires inline style/script CSP allowances; do not treat this CSP as an XSS sanitizer.
|
||||||
@@ -23,6 +30,12 @@
|
|||||||
WordPress/Apache retains the capabilities needed by the official image internally,
|
WordPress/Apache retains the capabilities needed by the official image internally,
|
||||||
but no WordPress port is published. A containerized security test fails if the
|
but no WordPress port is published. A containerized security test fails if the
|
||||||
WordPress service is configured with host-published ports.
|
WordPress service is configured with host-published ports.
|
||||||
|
- `wp-cron` runs WordPress scheduled events through WP-CLI on the internal data network
|
||||||
|
only; public `wp-cron.php` remains blocked. WP-CLI normally joins only the internal
|
||||||
|
data network. The maintenance override
|
||||||
|
attaches WP-CLI to the web network for operator-triggered commands that require
|
||||||
|
outbound internet access, such as WordPress core downloads; do not use it for routine
|
||||||
|
bootstrap or inspection commands.
|
||||||
- Persistent state uses host-based bind mounts. Keep those paths outside the public web
|
- Persistent state uses host-based bind mounts. Keep those paths outside the public web
|
||||||
root, restrict host access, never make them world-writable, and run
|
root, restrict host access, never make them world-writable, and run
|
||||||
`./scripts/prepare-host-volumes.sh` when paths or image user IDs change.
|
`./scripts/prepare-host-volumes.sh` when paths or image user IDs change.
|
||||||
|
|||||||
+3
-1
@@ -12,6 +12,7 @@ docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools
|
|||||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||||
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --rm security-tests
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --rm security-tests
|
||||||
docker compose config --quiet
|
docker compose config --quiet
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml config --quiet
|
||||||
LETSENCRYPT_ENABLED=1 docker compose config --quiet
|
LETSENCRYPT_ENABLED=1 docker compose config --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ cover content and section order, contact actions, mobile overflow/navigation, se
|
|||||||
landmarks, image alternatives, admin routing, security headers, blocked sensitive
|
landmarks, image alternatives, admin routing, security headers, blocked sensitive
|
||||||
routes, blocked uploaded PHP requests, and unknown virtual hosts. Security checks also
|
routes, blocked uploaded PHP requests, and unknown virtual hosts. Security checks also
|
||||||
assert that WordPress does not publish host ports and that Apache hardening remains
|
assert that WordPress does not publish host ports and that Apache hardening remains
|
||||||
installed in the WordPress image.
|
installed in the WordPress image. They also assert that the default demo plugins are
|
||||||
|
removed and that the internal `wp-cron` service has no public network exposure.
|
||||||
|
|
||||||
Subjective visual review and a real-device accessibility audit remain manual release
|
Subjective visual review and a real-device accessibility audit remain manual release
|
||||||
checks.
|
checks.
|
||||||
|
|||||||
@@ -18,6 +18,26 @@ location = /xmlrpc.php {
|
|||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /wp-cron.php {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /readme.html {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /license.txt {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /wp-admin/install.php {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /wp-content/mu-plugins/ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
location = /wp-login.php {
|
location = /wp-login.php {
|
||||||
limit_req zone=login burst=5 nodelay;
|
limit_req zone=login burst=5 nodelay;
|
||||||
proxy_pass http://wordpress_backend;
|
proxy_pass http://wordpress_backend;
|
||||||
|
|||||||
@@ -54,15 +54,15 @@ test("provides usable contact actions", async ({ page }) => {
|
|||||||
await expect(page.locator(".social-link-youtube")).toHaveCount(1);
|
await expect(page.locator(".social-link-youtube")).toHaveCount(1);
|
||||||
await expect(page.getByRole("link", { name: "Instagram" })).toHaveAttribute(
|
await expect(page.getByRole("link", { name: "Instagram" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://instagram.com/",
|
"https://instagram.com/azionelab",
|
||||||
);
|
);
|
||||||
await expect(page.getByRole("link", { name: "Facebook" })).toHaveAttribute(
|
await expect(page.getByRole("link", { name: "Facebook" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://facebook.com/",
|
"https://facebook.com/azionelab",
|
||||||
);
|
);
|
||||||
await expect(page.getByRole("link", { name: "YouTube" })).toHaveAttribute(
|
await expect(page.getByRole("link", { name: "YouTube" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://youtube.com/",
|
"https://youtube.com/@azionelab",
|
||||||
);
|
);
|
||||||
await expect(page.locator(".social-links .social-icon")).toHaveCount(3);
|
await expect(page.locator(".social-links .social-icon")).toHaveCount(3);
|
||||||
await expect(page.locator(".contact-actions .contact-icon")).toHaveCount(3);
|
await expect(page.locator(".contact-actions .contact-icon")).toHaveCount(3);
|
||||||
@@ -121,8 +121,14 @@ test("protects the edge and exposes the WordPress admin", async ({ page, request
|
|||||||
expect((await request.get("/xmlrpc.php")).status()).toBe(403);
|
expect((await request.get("/xmlrpc.php")).status()).toBe(403);
|
||||||
expect((await request.get("/.env")).status()).toBe(404);
|
expect((await request.get("/.env")).status()).toBe(404);
|
||||||
expect((await request.get("/wp-config.php")).status()).toBe(404);
|
expect((await request.get("/wp-config.php")).status()).toBe(404);
|
||||||
|
expect((await request.get("/readme.html")).status()).toBe(404);
|
||||||
|
expect((await request.get("/license.txt")).status()).toBe(404);
|
||||||
|
expect((await request.get("/wp-admin/install.php")).status()).toBe(404);
|
||||||
|
expect((await request.get("/wp-content/mu-plugins/")).status()).toBe(404);
|
||||||
|
expect((await request.get("/wp-cron.php")).status()).toBe(403);
|
||||||
expect((await request.get("/wp-content/uploads/probe.php")).status()).toBe(403);
|
expect((await request.get("/wp-content/uploads/probe.php")).status()).toBe(403);
|
||||||
expect((await request.get("/wp-json/wp/v2/users")).status()).toBe(404);
|
expect((await request.get("/wp-json/wp/v2/users")).status()).toBe(404);
|
||||||
|
expect((await request.get("/?post_type=azl_show&name=le-cose-che-restano")).status()).toBe(404);
|
||||||
|
|
||||||
await page.goto("/wp-admin/");
|
await page.goto("/wp-admin/");
|
||||||
await expect(page).toHaveURL(/\/wp-login\.php/);
|
await expect(page).toHaveURL(/\/wp-login\.php/);
|
||||||
|
|||||||
@@ -22,7 +22,131 @@ grep -q 'azionelab-apache-hardening.conf' /workspace/wordpress/Dockerfile || {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grep -q '^FROM wordpress:7\.0\.2-php8\.3-apache$' /workspace/wordpress/Dockerfile || {
|
||||||
|
echo "The WordPress image must use the patched 7.0.2 PHP 8.3 Apache tag." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q 'wp-content/plugins/akismet' /workspace/wordpress/Dockerfile || {
|
||||||
|
echo "The WordPress image must remove the bundled Akismet plugin." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q 'wp-content/plugins/hello.php' /workspace/wordpress/Dockerfile || {
|
||||||
|
echo "The WordPress image must remove the bundled Hello Dolly plugin." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q 'wp-content/plugins/akismet' /workspace/wordpress/entrypoint-wrapper.sh || {
|
||||||
|
echo "The WordPress entrypoint must remove Akismet from existing persistent volumes." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q 'wp-content/plugins/hello.php' /workspace/wordpress/entrypoint-wrapper.sh || {
|
||||||
|
echo "The WordPress entrypoint must remove Hello Dolly from existing persistent volumes." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
grep -q 'wp-content/(uploads|files)' /workspace/wordpress/apache-hardening.conf || {
|
grep -q 'wp-content/(uploads|files)' /workspace/wordpress/apache-hardening.conf || {
|
||||||
echo "Apache hardening must block PHP execution below uploads/files." >&2
|
echo "Apache hardening must block PHP execution below uploads/files." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grep -q 'wp-content/mu-plugins' /workspace/wordpress/apache-hardening.conf || {
|
||||||
|
echo "Apache hardening must block direct mu-plugin probing." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "'publicly_queryable'[[:space:]]*=>[[:space:]]*false" /workspace/wordpress/mu-plugins/azionelab-content.php || {
|
||||||
|
echo "Structured show content must not be exposed as standalone public routes." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "location = /wp-cron.php" /workspace/nginx/proxy-routes.conf || {
|
||||||
|
echo "NGINX must block external wp-cron.php requests." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "location = /readme.html" /workspace/nginx/proxy-routes.conf || {
|
||||||
|
echo "NGINX must hide the WordPress readme.html file." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "location ^~ /wp-content/mu-plugins/" /workspace/nginx/proxy-routes.conf || {
|
||||||
|
echo "NGINX must hide the mu-plugins directory from public probing." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -f /workspace/docker-compose.maintenance.yml ] || {
|
||||||
|
echo "The maintenance Compose override must exist for controlled wp-cli egress." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "^[[:space:]]*wp-cli:" /workspace/docker-compose.maintenance.yml || {
|
||||||
|
echo "The maintenance override must target only the wp-cli service." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "^[[:space:]]*-[[:space:]]*data$" /workspace/docker-compose.maintenance.yml || {
|
||||||
|
echo "The maintenance wp-cli service must keep database network access." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "^[[:space:]]*-[[:space:]]*web$" /workspace/docker-compose.maintenance.yml || {
|
||||||
|
echo "The maintenance wp-cli service must explicitly opt into the web network." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -Eq "^[[:space:]]{2}wp-cron:" "$compose_file" || {
|
||||||
|
echo "Compose must define an internal wp-cron service." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "wp cron event run --due-now" /workspace/wp-cli/cron.sh || {
|
||||||
|
echo "The wp-cron runner must execute due WordPress cron events via WP-CLI." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
awk '
|
||||||
|
/^ wp-cron:/ {
|
||||||
|
in_wp_cron = 1
|
||||||
|
has_data_network = 0
|
||||||
|
has_db_host = 0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
in_wp_cron && /^ [A-Za-z0-9_-]+:/ {
|
||||||
|
if (!has_data_network) {
|
||||||
|
print "The wp-cron service must join the internal data network." > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if (!has_db_host) {
|
||||||
|
print "The wp-cron service must receive WordPress database environment variables." > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
in_wp_cron = 0
|
||||||
|
}
|
||||||
|
in_wp_cron && /^ ports:/ {
|
||||||
|
print "The wp-cron service must not publish host ports." > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
in_wp_cron && /^[[:space:]]+WORDPRESS_DB_HOST:/ {
|
||||||
|
has_db_host = 1
|
||||||
|
}
|
||||||
|
in_wp_cron && /^ - data$/ {
|
||||||
|
has_data_network = 1
|
||||||
|
}
|
||||||
|
in_wp_cron && /^ - web$/ {
|
||||||
|
print "The wp-cron service must not join the public web network." > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
if (in_wp_cron && !has_data_network) {
|
||||||
|
print "The wp-cron service must join the internal data network." > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if (in_wp_cron && !has_db_host) {
|
||||||
|
print "The wp-cron service must receive WordPress database environment variables." > "/dev/stderr"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$compose_file"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM wordpress:7.0.0-php8.3-apache
|
FROM wordpress:7.0.2-php8.3-apache
|
||||||
|
|
||||||
COPY php.ini /usr/local/etc/php/conf.d/azionelab.ini
|
COPY php.ini /usr/local/etc/php/conf.d/azionelab.ini
|
||||||
COPY .htaccess /opt/azionelab/.htaccess
|
COPY .htaccess /opt/azionelab/.htaccess
|
||||||
@@ -9,6 +9,7 @@ COPY theme/azionelab /opt/azionelab/theme
|
|||||||
COPY mu-plugins/azionelab-content.php /opt/azionelab/azionelab-content.php
|
COPY mu-plugins/azionelab-content.php /opt/azionelab/azionelab-content.php
|
||||||
|
|
||||||
RUN sed -ri 's!^[[:space:]]*CustomLog .*!CustomLog /dev/null combined!' /etc/apache2/sites-available/000-default.conf \
|
RUN sed -ri 's!^[[:space:]]*CustomLog .*!CustomLog /dev/null combined!' /etc/apache2/sites-available/000-default.conf \
|
||||||
|
&& rm -rf /usr/src/wordpress/wp-content/plugins/akismet /usr/src/wordpress/wp-content/plugins/hello.php \
|
||||||
&& a2enconf azionelab-apache-hardening \
|
&& a2enconf azionelab-apache-hardening \
|
||||||
&& chmod 755 /usr/local/bin/azionelab-entrypoint \
|
&& chmod 755 /usr/local/bin/azionelab-entrypoint \
|
||||||
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \
|
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ TraceEnable Off
|
|||||||
Require all denied
|
Require all denied
|
||||||
</LocationMatch>
|
</LocationMatch>
|
||||||
|
|
||||||
|
<LocationMatch "^/(readme\.html|license\.txt|wp-admin/install\.php|wp-cron\.php)$">
|
||||||
|
Require all denied
|
||||||
|
</LocationMatch>
|
||||||
|
|
||||||
|
<LocationMatch "^/wp-content/mu-plugins/">
|
||||||
|
Require all denied
|
||||||
|
</LocationMatch>
|
||||||
|
|
||||||
<LocationMatch "^/wp-admin/includes/">
|
<LocationMatch "^/wp-admin/includes/">
|
||||||
Require all denied
|
Require all denied
|
||||||
</LocationMatch>
|
</LocationMatch>
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ case "$environment" in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "${1:-}" = "apache2-foreground" ]; then
|
if [ "${1:-}" = "apache2-foreground" ]; then
|
||||||
mkdir -p /var/www/html/wp-content/themes /var/www/html/wp-content/mu-plugins
|
mkdir -p /var/www/html/wp-content/themes /var/www/html/wp-content/mu-plugins /var/www/html/wp-content/plugins
|
||||||
|
rm -rf /var/www/html/wp-content/plugins/akismet /var/www/html/wp-content/plugins/hello.php
|
||||||
rm -rf /var/www/html/wp-content/themes/azionelab
|
rm -rf /var/www/html/wp-content/themes/azionelab
|
||||||
cp -a /opt/azionelab/theme /var/www/html/wp-content/themes/azionelab
|
cp -a /opt/azionelab/theme /var/www/html/wp-content/themes/azionelab
|
||||||
cp /opt/azionelab/azionelab-content.php /var/www/html/wp-content/mu-plugins/azionelab-content.php
|
cp /opt/azionelab/azionelab-content.php /var/www/html/wp-content/mu-plugins/azionelab-content.php
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ function azionelab_register_content_types(): void {
|
|||||||
'add_new_item' => 'Aggiungi spettacolo',
|
'add_new_item' => 'Aggiungi spettacolo',
|
||||||
'edit_item' => 'Modifica spettacolo',
|
'edit_item' => 'Modifica spettacolo',
|
||||||
),
|
),
|
||||||
'public' => true,
|
'public' => false,
|
||||||
'show_in_rest' => false,
|
'show_ui' => true,
|
||||||
'menu_icon' => 'dashicons-tickets-alt',
|
'publicly_queryable' => false,
|
||||||
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes' ),
|
'exclude_from_search'=> true,
|
||||||
'has_archive' => false,
|
'show_in_rest' => false,
|
||||||
'rewrite' => false,
|
'menu_icon' => 'dashicons-tickets-alt',
|
||||||
|
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes' ),
|
||||||
|
'has_archive' => false,
|
||||||
|
'rewrite' => false,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ $social_links = array_values(
|
|||||||
static fn ( array $link ): bool => '' !== $link['url']
|
static fn ( array $link ): bool => '' !== $link['url']
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
$contact_email = sanitize_email( azionelab_mod( 'contact_email' ) );
|
||||||
|
$contact_phone = trim( azionelab_mod( 'contact_phone' ) );
|
||||||
|
$contact_phone_href = azionelab_phone_href( $contact_phone );
|
||||||
|
$contact_whatsapp = trim( azionelab_mod( 'contact_whatsapp' ) );
|
||||||
|
$contact_whatsapp_href = azionelab_whatsapp_href( $contact_whatsapp );
|
||||||
?>
|
?>
|
||||||
<main id="contenuto">
|
<main id="contenuto">
|
||||||
<section class="hero section" id="inizio" aria-labelledby="hero-title">
|
<section class="hero section" id="inizio" aria-labelledby="hero-title">
|
||||||
@@ -135,7 +140,7 @@ $social_links = array_values(
|
|||||||
?>
|
?>
|
||||||
<article class="show-card">
|
<article class="show-card">
|
||||||
<img src="<?php echo esc_url( $image ); ?>" alt="Locandina di <?php the_title_attribute(); ?>" width="700" height="880" loading="lazy">
|
<img src="<?php echo esc_url( $image ); ?>" alt="Locandina di <?php the_title_attribute(); ?>" width="700" height="880" loading="lazy">
|
||||||
<div class="show-copy"><?php if ( $show_meta ) : ?><p class="show-meta"><?php echo esc_html( $show_meta ); ?></p><?php endif; ?><h3><?php the_title(); ?></h3><div><?php the_excerpt(); ?></div></div>
|
<div class="show-copy"><?php if ( $show_meta ) : ?><p class="show-meta"><?php echo esc_html( $show_meta ); ?></p><?php endif; ?><h3><?php echo esc_html( get_the_title() ); ?></h3><div><?php echo wp_kses_post( wpautop( get_the_excerpt() ) ); ?></div></div>
|
||||||
</article>
|
</article>
|
||||||
<?php
|
<?php
|
||||||
++$show_index;
|
++$show_index;
|
||||||
@@ -207,9 +212,15 @@ $social_links = array_values(
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-actions">
|
<div class="contact-actions">
|
||||||
<a class="contact-action" href="mailto:<?php echo esc_attr( sanitize_email( azionelab_mod( 'contact_email' ) ) ); ?>"><?php echo azionelab_contact_icon( 'email' ); ?><span>Scrivi una mail</span><strong><?php echo esc_html( azionelab_mod( 'contact_email' ) ); ?></strong></a>
|
<?php if ( $contact_email ) : ?>
|
||||||
<a class="contact-action" href="tel:<?php echo esc_attr( azionelab_phone_href( azionelab_mod( 'contact_phone' ) ) ); ?>"><?php echo azionelab_contact_icon( 'phone' ); ?><span>Chiama</span><strong><?php echo esc_html( azionelab_mod( 'contact_phone' ) ); ?></strong></a>
|
<a class="contact-action" href="mailto:<?php echo esc_attr( $contact_email ); ?>"><?php echo azionelab_contact_icon( 'email' ); ?><span>Scrivi una mail</span><strong><?php echo esc_html( $contact_email ); ?></strong></a>
|
||||||
<a class="contact-action" href="https://wa.me/<?php echo esc_attr( azionelab_whatsapp_href( azionelab_mod( 'contact_whatsapp' ) ) ); ?>" rel="noopener noreferrer"><?php echo azionelab_contact_icon( 'whatsapp' ); ?><span>WhatsApp</span><strong><?php echo esc_html( azionelab_mod( 'contact_whatsapp' ) ); ?></strong></a>
|
<?php endif; ?>
|
||||||
|
<?php if ( $contact_phone && $contact_phone_href ) : ?>
|
||||||
|
<a class="contact-action" href="tel:<?php echo esc_attr( $contact_phone_href ); ?>"><?php echo azionelab_contact_icon( 'phone' ); ?><span>Chiama</span><strong><?php echo esc_html( $contact_phone ); ?></strong></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $contact_whatsapp && $contact_whatsapp_href ) : ?>
|
||||||
|
<a class="contact-action" href="https://wa.me/<?php echo esc_attr( $contact_whatsapp_href ); ?>" rel="noopener noreferrer"><?php echo azionelab_contact_icon( 'whatsapp' ); ?><span>WhatsApp</span><strong><?php echo esc_html( $contact_whatsapp ); ?></strong></a>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ function azionelab_defaults(): array {
|
|||||||
'contact_email' => 'ciao@azionelab.org',
|
'contact_email' => 'ciao@azionelab.org',
|
||||||
'contact_phone' => '+39 333 123 4567',
|
'contact_phone' => '+39 333 123 4567',
|
||||||
'contact_whatsapp' => '+39 333 123 4567',
|
'contact_whatsapp' => '+39 333 123 4567',
|
||||||
'contact_instagram' => 'https://instagram.com/',
|
'contact_instagram' => '',
|
||||||
'contact_facebook' => 'https://facebook.com/',
|
'contact_facebook' => '',
|
||||||
'contact_youtube' => 'https://youtube.com/',
|
'contact_youtube' => '',
|
||||||
'contact_address' => "Via dell'Epomeo 9999, Napoli",
|
'contact_address' => "Via dell'Epomeo 9999, Napoli",
|
||||||
'footer_text' => 'Uno spazio aperto a chi desidera incontrare il teatro, insieme.',
|
'footer_text' => 'Uno spazio aperto a chi desidera incontrare il teatro, insieme.',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ wp option update permalink_structure '/%postname%/'
|
|||||||
if ! wp theme is-active azionelab; then
|
if ! wp theme is-active azionelab; then
|
||||||
wp theme activate azionelab
|
wp theme activate azionelab
|
||||||
fi
|
fi
|
||||||
|
wp eval 'set_theme_mod( "contact_instagram", "https://instagram.com/azionelab" ); set_theme_mod( "contact_facebook", "https://facebook.com/azionelab" ); set_theme_mod( "contact_youtube", "https://youtube.com/@azionelab" );'
|
||||||
|
|
||||||
for sample_id in 1 2; do
|
for sample_id in 1 2; do
|
||||||
if ! wp post get "$sample_id" --field=ID >/dev/null 2>&1; then
|
if ! wp post get "$sample_id" --field=ID >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cd /var/www/html
|
||||||
|
|
||||||
|
trap 'exit 0' INT TERM
|
||||||
|
|
||||||
|
interval="${WP_CRON_INTERVAL_SECONDS:-300}"
|
||||||
|
case "$interval" in
|
||||||
|
"" | *[!0-9]*)
|
||||||
|
echo "WP_CRON_INTERVAL_SECONDS must be a positive integer." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$interval" -lt 60 ]; then
|
||||||
|
echo "WP_CRON_INTERVAL_SECONDS must be at least 60 seconds." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [ ! -f /var/www/html/wp-load.php ]; do
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
until wp core is-installed >/dev/null 2>&1; do
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
if ! wp cron event run --due-now >/dev/null 2>&1; then
|
||||||
|
echo "Warning: failed to run due WordPress cron events." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$interval" &
|
||||||
|
wait "$!"
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user