Compare commits

8 Commits

Author SHA1 Message Date
bisco e6ec3c94dc feat: add internal WordPress cron runner 2026-07-23 23:40:44 +02:00
bisco f75bce3dea chore: add wp-cli maintenance override 2026-07-23 21:22:21 +02:00
bisco 472b8616b4 fix: harden WordPress against WPScan findings 2026-07-23 19:10:11 +02:00
bisco 0762b98fc6 fix: harden WordPress content rendering 2026-06-26 15:54:57 +02:00
bisco 482928a296 fix: balance contact section socials 2026-06-26 09:17:29 +02:00
bisco bf465edb8c fix: add contact and social icons 2026-06-26 09:02:48 +02:00
bisco e5fde3e078 merge: social wordmark badges into wp 2026-06-26 08:42:01 +02:00
bisco 218c1ef9dd fix: use wordmark style social badges 2026-06-26 08:39:36 +02:00
24 changed files with 493 additions and 54 deletions
+1
View File
@@ -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
+30 -4
View File
@@ -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
+10
View File
@@ -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
+7
View File
@@ -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:
+42
View File
@@ -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 -7
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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.
+48
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+20
View File
@@ -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;
+17 -4
View File
@@ -48,18 +48,25 @@ test("provides usable contact actions", async ({ page }) => {
"https://wa.me/393331234567", "https://wa.me/393331234567",
); );
await expect(page.getByText("Seguici sui social")).toBeVisible(); await expect(page.getByText("Seguici sui social")).toBeVisible();
await expect(page.locator(".social-links a")).toHaveCount(2); await expect(page.locator(".social-links a")).toHaveCount(3);
await expect(page.locator(".social-link-instagram")).toHaveCount(1); await expect(page.locator(".social-link-instagram")).toHaveCount(1);
await expect(page.locator(".social-link-facebook")).toHaveCount(1); await expect(page.locator(".social-link-facebook")).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.locator(".social-links .social-icon")).toHaveCount(2); await expect(page.getByRole("link", { name: "YouTube" })).toHaveAttribute(
"href",
"https://youtube.com/@azionelab",
);
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-info .contact-icon")).toHaveCount(1);
}); });
test("supports mobile navigation without horizontal overflow", async ({ page }) => { test("supports mobile navigation without horizontal overflow", async ({ page }) => {
@@ -114,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/);
+124
View File
@@ -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"
+2 -1
View 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 \
+8
View File
@@ -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>
+2 -1
View File
@@ -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
+9 -6
View File
@@ -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
View File
@@ -16,10 +16,16 @@ $social_links = array_values(
array( array(
array( 'label' => 'Instagram', 'url' => trim( azionelab_mod( 'contact_instagram' ) ), 'icon' => 'instagram' ), array( 'label' => 'Instagram', 'url' => trim( azionelab_mod( 'contact_instagram' ) ), 'icon' => 'instagram' ),
array( 'label' => 'Facebook', 'url' => trim( azionelab_mod( 'contact_facebook' ) ), 'icon' => 'facebook' ), array( 'label' => 'Facebook', 'url' => trim( azionelab_mod( 'contact_facebook' ) ), 'icon' => 'facebook' ),
array( 'label' => 'YouTube', 'url' => trim( azionelab_mod( 'contact_youtube' ) ), 'icon' => 'youtube' ),
), ),
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">
@@ -134,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;
@@ -187,7 +193,10 @@ $social_links = array_values(
<p class="eyebrow">Parliamone</p> <p class="eyebrow">Parliamone</p>
<h2 id="contacts-title">Vieni a conoscerci</h2> <h2 id="contacts-title">Vieni a conoscerci</h2>
<p class="lead">Non serve esperienza: basta la curiosità di incontrare il teatro e il gruppo.</p> <p class="lead">Non serve esperienza: basta la curiosità di incontrare il teatro e il gruppo.</p>
<p><strong>Dove</strong><br><?php echo esc_html( azionelab_mod( 'contact_address' ) ); ?></p> <div class="contact-info">
<?php echo azionelab_contact_icon( 'address' ); ?>
<p><strong>Dove</strong><br><?php echo esc_html( azionelab_mod( 'contact_address' ) ); ?></p>
</div>
<?php if ( $social_links ) : ?> <?php if ( $social_links ) : ?>
<div class="social-promo"> <div class="social-promo">
<p class="social-promo-title">Seguici sui social</p> <p class="social-promo-title">Seguici sui social</p>
@@ -195,7 +204,7 @@ $social_links = array_values(
<?php foreach ( $social_links as $social_link ) : ?> <?php foreach ( $social_links as $social_link ) : ?>
<a class="social-link social-link-<?php echo esc_attr( $social_link['icon'] ); ?>" href="<?php echo esc_url( $social_link['url'] ); ?>" rel="noopener noreferrer"> <a class="social-link social-link-<?php echo esc_attr( $social_link['icon'] ); ?>" href="<?php echo esc_url( $social_link['url'] ); ?>" rel="noopener noreferrer">
<?php echo azionelab_social_icon( $social_link['icon'] ); ?> <?php echo azionelab_social_icon( $social_link['icon'] ); ?>
<span><?php echo esc_html( $social_link['label'] ); ?></span> <span class="visually-hidden"><?php echo esc_html( $social_link['label'] ); ?></span>
</a> </a>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
@@ -203,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' ) ) ); ?>"><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' ) ) ); ?>"><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"><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>
+30 -4
View File
@@ -29,8 +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' => '',
'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.',
); );
@@ -79,11 +80,35 @@ function azionelab_whatsapp_href( string $number ): string {
function azionelab_social_icon( string $network ): string { function azionelab_social_icon( string $network ): string {
if ( 'instagram' === $network ) { if ( 'instagram' === $network ) {
return '<svg class="social-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4.5" y="4.5" width="15" height="15" rx="4.2" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="3.5" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="16.8" cy="7.2" r="1.1" fill="currentColor"/></svg>'; return '<svg class="social-icon social-icon-instagram" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><defs><linearGradient id="azl-instagram-gradient" x1="3" y1="21" x2="21" y2="3" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#feda75"/><stop offset="0.28" stop-color="#fa7e1e"/><stop offset="0.55" stop-color="#d62976"/><stop offset="0.78" stop-color="#962fbf"/><stop offset="1" stop-color="#4f5bd5"/></linearGradient></defs><rect x="3.6" y="3.6" width="16.8" height="16.8" rx="5.1" fill="url(#azl-instagram-gradient)"/><circle cx="12" cy="12" r="4.1" fill="none" stroke="#fff" stroke-width="1.9"/><circle cx="16.8" cy="7.35" r="1.15" fill="#fff"/></svg>';
} }
if ( 'facebook' === $network ) { if ( 'facebook' === $network ) {
return '<svg class="social-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M14.2 8.4h2.3V5.1c-.4-.1-1.7-.2-3.1-.2-3.1 0-5.1 1.8-5.1 5.1v2.8H5v3.7h3.3V24h4.1v-7.5h3.3l.5-3.7h-3.8v-2.4c0-1.1.3-2 1.8-2Z"/></svg>'; return '<svg class="social-icon social-icon-facebook" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="2.5" y="2.5" width="19" height="19" rx="4.5" fill="#1877f2"/><path fill="#fff" d="M13.45 21.5v-7.35h2.48l.37-2.88h-2.85V9.43c0-.83.23-1.4 1.43-1.4h1.52V5.46c-.26-.04-1.17-.12-2.22-.12-2.2 0-3.7 1.34-3.7 3.8v2.13H8v2.88h2.48v7.35h2.97Z"/></svg>';
}
if ( 'youtube' === $network ) {
return '<svg class="social-icon social-icon-youtube" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="2" y="5" width="20" height="14" rx="4.2" fill="#ff0000"/><path d="m10.2 8.6 5.4 3.4-5.4 3.4V8.6Z" fill="#fff"/></svg>';
}
return '';
}
function azionelab_contact_icon( string $type ): string {
if ( 'email' === $type ) {
return '<svg class="contact-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="14" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="m4.5 7 7.5 6 7.5-6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/></svg>';
}
if ( 'phone' === $type ) {
return '<svg class="contact-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8.1 4.2 10 8.6l-2 1.5c1.2 2.4 3.1 4.3 5.6 5.7l1.6-2 4.5 1.9c.4.2.7.6.6 1.1l-.6 3c-.1.6-.7 1-1.3.9C10.5 19.9 4.2 13.6 3.4 5.7c-.1-.6.3-1.2.9-1.3l3-.6c.4-.1.8.1 1 .4Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/></svg>';
}
if ( 'whatsapp' === $type ) {
return '<svg class="contact-icon contact-icon-whatsapp" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4.5 20.2 5.7 16A8.1 8.1 0 1 1 9 19.2l-4.5 1Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="1.8"/><path d="M9.2 8.4c.2-.4.4-.4.7-.4h.5c.2 0 .4.1.5.4l.7 1.6c.1.3 0 .5-.2.7l-.4.5c.7 1.2 1.6 2.1 2.9 2.8l.6-.7c.2-.2.4-.3.7-.2l1.6.8c.3.1.4.3.4.6v.5c0 .4-.2.7-.6.9-.5.3-1.3.4-2.3.1-2.8-.8-5.3-3.2-6.1-6.1-.3-1-.1-1.8.4-2.1Z" fill="currentColor"/></svg>';
}
if ( 'address' === $type ) {
return '<svg class="contact-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 21s6.2-5.5 6.2-11.1A6.2 6.2 0 0 0 5.8 9.9C5.8 15.5 12 21 12 21Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="1.8"/><circle cx="12" cy="9.9" r="2.2" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>';
} }
return ''; return '';
@@ -149,6 +174,7 @@ function azionelab_customize_register( WP_Customize_Manager $customizer ): void
'contact_whatsapp' => array( 'azionelab_contacts', 'WhatsApp', 'text' ), 'contact_whatsapp' => array( 'azionelab_contacts', 'WhatsApp', 'text' ),
'contact_instagram' => array( 'azionelab_contacts', 'Instagram', 'url' ), 'contact_instagram' => array( 'azionelab_contacts', 'Instagram', 'url' ),
'contact_facebook' => array( 'azionelab_contacts', 'Facebook', 'url' ), 'contact_facebook' => array( 'azionelab_contacts', 'Facebook', 'url' ),
'contact_youtube' => array( 'azionelab_contacts', 'YouTube', 'url' ),
'contact_address' => array( 'azionelab_contacts', 'Indirizzo', 'text' ), 'contact_address' => array( 'azionelab_contacts', 'Indirizzo', 'text' ),
'footer_text' => array( 'azionelab_contacts', 'Testo footer', 'text' ), 'footer_text' => array( 'azionelab_contacts', 'Testo footer', 'text' ),
); );
+14 -9
View File
@@ -29,6 +29,7 @@ Text Domain: azionelab
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; } html { scroll-behavior: smooth; }
.visually-hidden { position: absolute; overflow: hidden; width: 1px; height: 1px; padding: 0; border: 0; margin: -1px; clip: rect(0 0 0 0); white-space: nowrap; }
body { body {
margin: 0; margin: 0;
color: var(--color-dark-text); color: var(--color-dark-text);
@@ -180,19 +181,22 @@ blockquote { margin: 2rem 0 0; padding-left: 1.5rem; border-left: 3px solid var(
.empty-state { padding: 2rem; border: 1px dashed var(--color-muted); border-radius: var(--radius); color: var(--color-muted); text-align: center; } .empty-state { padding: 2rem; border: 1px dashed var(--color-muted); border-radius: var(--radius); color: var(--color-muted); text-align: center; }
.contacts { color: var(--color-bg-light); background: var(--color-dark); } .contacts { color: var(--color-bg-light); background: var(--color-dark); }
.contacts .eyebrow { color: var(--color-primary-soft); } .contacts .eyebrow { color: var(--color-primary-soft); }
.contacts .lead { color: rgba(255, 248, 239, 0.8); } .contacts .lead { max-width: 38rem; color: rgba(255, 248, 239, 0.8); }
.contact-grid { display: grid; gap: 2.5rem; } .contact-grid { display: grid; gap: 2.5rem; }
.contact-actions { display: grid; gap: 0.8rem; } .contact-info { display: grid; position: relative; margin-top: 1.6rem; padding-left: 3rem; }
.contact-action { display: grid; padding: 1rem 1.1rem; border: 1px solid rgba(255,255,255,0.18); border-radius: 0.9rem; text-decoration: none; } .contact-info p { margin: 0; }
.contact-info .contact-icon { top: 0.1rem; left: 0; color: var(--color-sand); }
.contact-actions { display: grid; align-content: start; gap: 0.8rem; }
.contact-action { display: grid; position: relative; min-height: 5.75rem; padding: 1rem 1.1rem 1rem 4rem; border: 1px solid rgba(255,255,255,0.18); border-radius: 0.9rem; align-content: center; text-decoration: none; }
.contact-action span { color: var(--color-sand); font-size: 0.78rem; text-transform: uppercase; } .contact-action span { color: var(--color-sand); font-size: 0.78rem; text-transform: uppercase; }
.contact-icon { position: absolute; top: 1rem; left: 1.1rem; width: 2rem; height: 2rem; color: var(--color-primary-soft); }
.contact-icon-whatsapp { color: #25d366; }
.social-promo { margin-top: 1.6rem; padding-top: 1.25rem; border-top: 1px solid rgba(255, 248, 239, 0.22); } .social-promo { margin-top: 1.6rem; padding-top: 1.25rem; border-top: 1px solid rgba(255, 248, 239, 0.22); }
.social-promo-title { margin-bottom: 0.8rem; color: var(--color-sand); font-size: 0.78rem; font-weight: 750; letter-spacing: 0.13em; text-transform: uppercase; } .social-promo-title { margin-bottom: 0.8rem; color: var(--color-sand); font-size: 0.78rem; font-weight: 750; letter-spacing: 0.13em; text-transform: uppercase; }
.social-links { display: flex; flex-wrap: wrap; gap: 0.75rem; } .social-links { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.social-links a { display: inline-flex; min-height: 2.85rem; padding: 0.72rem 0.95rem; border: 1px solid transparent; border-radius: 0.5rem; align-items: center; gap: 0.55rem; color: white; font-weight: 750; text-decoration: none; box-shadow: 0 10px 24px rgba(29, 26, 23, 0.16); } .social-links a { display: inline-flex; width: 3.85rem; height: 3.85rem; border: 1px solid rgba(255, 248, 239, 0.7); border-radius: 0.75rem; align-items: center; justify-content: center; background: #fff8ef; color: var(--color-dark-text); line-height: 1; text-decoration: none; box-shadow: 0 14px 32px rgba(29, 26, 23, 0.18); }
.social-link-instagram { background: linear-gradient(135deg, #f58529 0%, #dd2a7b 48%, #8134af 100%); } .social-links a:hover { background: white; transform: translateY(-2px); }
.social-link-facebook { background: #1877f2; } .social-icon { width: 2.45rem; height: 2.45rem; flex: 0 0 auto; }
.social-links a:hover { filter: saturate(1.08) brightness(1.06); transform: translateY(-1px); }
.social-icon { width: 1.2rem; height: 1.2rem; flex: 0 0 auto; }
.site-footer { padding-block: 2rem; color: #f7efe4; background: var(--color-dark-text); } .site-footer { padding-block: 2rem; color: #f7efe4; background: var(--color-dark-text); }
.site-footer a { color: #fff8ef; } .site-footer a { color: #fff8ef; }
.footer-inner { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem 1.5rem; font-size: 0.85rem; } .footer-inner { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem 1.5rem; font-size: 0.85rem; }
@@ -231,7 +235,8 @@ blockquote { margin: 2rem 0 0; padding-left: 1.5rem; border-left: 3px solid var(
.shows-grid { grid-template-columns: repeat(2, 1fr); } .shows-grid { grid-template-columns: repeat(2, 1fr); }
.gallery-grid { grid-template-columns: repeat(4, 1fr); } .gallery-grid { grid-template-columns: repeat(4, 1fr); }
.gallery-item:nth-child(3n + 1) { grid-column: span 2; } .gallery-item:nth-child(3n + 1) { grid-column: span 2; }
.contact-grid { grid-template-columns: 1fr 0.9fr; } .contact-grid { grid-template-columns: minmax(0, 1fr) minmax(20rem, 0.78fr); align-items: center; }
.contact-actions { width: min(100%, 34rem); justify-self: end; }
} }
@media (min-width: 64rem) { @media (min-width: 64rem) {
+1
View File
@@ -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
+36
View File
@@ -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