generated from bisco/codex-bootstrap
Compare commits
8 Commits
6c20a4ce1e
...
wp
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ec3c94dc | |||
| f75bce3dea | |||
| 472b8616b4 | |||
| 0762b98fc6 | |||
| 482928a296 | |||
| bf465edb8c | |||
| e5fde3e078 | |||
| 218c1ef9dd |
@@ -11,6 +11,7 @@ WP_TITLE=Azione!Lab
|
||||
WP_ADMIN_USER=azionelab-admin
|
||||
WP_ADMIN_PASSWORD=replace-with-a-local-admin-password
|
||||
WP_ADMIN_EMAIL=admin@example.org
|
||||
WP_CRON_INTERVAL_SECONDS=300
|
||||
NGINX_BIND_ADDRESS=127.0.0.1
|
||||
NGINX_HTTP_PORT=8080
|
||||
NGINX_HTTPS_PORT=8443
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
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
|
||||
Docker Compose provides MariaDB, NGINX, optional Let's Encrypt, WP-CLI, and isolated
|
||||
browser tests.
|
||||
Docker Compose provides MariaDB, NGINX, an internal WordPress cron runner, optional
|
||||
Let's Encrypt, WP-CLI, and isolated browser tests.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -12,6 +12,7 @@ browser tests.
|
||||
- `db`: MariaDB 11.8 LTS, available only on the internal data network.
|
||||
- `proxy`: the only published service; routes `azionelab.org` to WordPress and applies
|
||||
edge security controls.
|
||||
- `wp-cron`: internal WP-CLI runner for due scheduled WordPress events.
|
||||
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
||||
- `wp-cli`: opt-in bootstrap and maintenance service.
|
||||
- `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 `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;
|
||||
- keep `LETSENCRYPT_STAGING=1` for the first certificate test;
|
||||
- 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
|
||||
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/`.
|
||||
@@ -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
|
||||
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
|
||||
|
||||
```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 cron event list'
|
||||
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/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 --rm security-tests
|
||||
docker compose config --quiet
|
||||
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml config --quiet
|
||||
```
|
||||
|
||||
## 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
|
||||
- ./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:
|
||||
networks:
|
||||
web:
|
||||
|
||||
@@ -52,6 +52,7 @@ services:
|
||||
$$_SERVER['HTTPS'] = 'on';
|
||||
}
|
||||
define('DISALLOW_FILE_EDIT', true);
|
||||
define('DISABLE_WP_CRON', true);
|
||||
define('WP_AUTO_UPDATE_CORE', 'minor');
|
||||
define('WP_POST_REVISIONS', 10);
|
||||
if (getenv('WP_ENVIRONMENT_TYPE') === 'production') {
|
||||
@@ -80,6 +81,47 @@ services:
|
||||
- no-new-privileges:true
|
||||
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:
|
||||
build:
|
||||
context: ./nginx
|
||||
|
||||
@@ -12,15 +12,17 @@ architecture and starts independently from `main`.
|
||||
|
||||
## 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
|
||||
sanitized theme modifications, while a must-use plugin owns Shows and Gallery custom
|
||||
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;
|
||||
Certbot provides opt-in 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.
|
||||
NGINX is the only public entry point. Public `wp-cron.php` requests stay blocked, while
|
||||
an internal WP-CLI based `wp-cron` service runs due scheduled events over the private
|
||||
data network. WP-CLI provides an idempotent opt-in bootstrap; Certbot provides opt-in
|
||||
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
|
||||
|
||||
@@ -30,6 +32,8 @@ functional tests use separate Docker volumes.
|
||||
- Core/theme/plugin patches require an image rebuild in production.
|
||||
- Fixed participation cards remain code until editorial requirements justify another
|
||||
structured content type.
|
||||
- Scheduled WordPress jobs depend on the internal `wp-cron` service instead of visitor
|
||||
traffic.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -43,8 +47,9 @@ functional tests use separate Docker volumes.
|
||||
|
||||
The architecture inherits WordPress's public CMS attack surface. The implementation
|
||||
reduces it with network isolation, no direct application port, disabled file editing
|
||||
and XML-RPC, production immutability, sanitization/escaping, rate limiting, security
|
||||
headers, and fail-closed TLS. Admin MFA and allowlisting remain external controls.
|
||||
and XML-RPC, production immutability, sanitized/escaped rendering, no bundled demo
|
||||
plugins, internal-only scheduled jobs, rate limiting, security headers, and fail-closed
|
||||
TLS. Admin MFA and allowlisting remain external controls.
|
||||
|
||||
## Operational impact
|
||||
|
||||
|
||||
+11
-7
@@ -1,20 +1,24 @@
|
||||
# Architecture
|
||||
|
||||
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
|
||||
nor MariaDB publishes a host port; automated security checks guard this assumption.
|
||||
|
||||
The custom `azionelab` classic theme renders the public single page. Theme modifications
|
||||
store the hero, manifesto, laboratory, teacher, lesson, and contact fields. The
|
||||
`azionelab-content` must-use plugin registers Shows and Gallery custom post types so
|
||||
structured editorial content survives a theme change. Images use WordPress featured
|
||||
images with local SVG fallbacks.
|
||||
structured editorial content survives a theme change. These custom post types are
|
||||
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,
|
||||
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.
|
||||
`wp-cron` is an internal WP-CLI runner on the `data` network. It executes due scheduled
|
||||
events with `wp cron event run --due-now` and keeps public `wp-cron.php` requests
|
||||
blocked. WP-CLI is also available as an opt-in tools-profile service. Its idempotent
|
||||
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,
|
||||
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
|
||||
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
|
||||
first start, run:
|
||||
@@ -37,6 +39,9 @@ Required controls:
|
||||
- either direct Let's Encrypt termination or a trusted external load balancer;
|
||||
- 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`
|
||||
is safe only when firewall/network policy makes the load balancer the sole NGINX
|
||||
caller and it overwrites `X-Forwarded-*` headers.
|
||||
|
||||
+24
-1
@@ -5,9 +5,10 @@
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
user IDs.
|
||||
- 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.
|
||||
- WordPress plugins expand the attack surface; install only reviewed, maintained,
|
||||
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'`.
|
||||
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
|
||||
|
||||
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.
|
||||
- WordPress uses its normal capability, nonce, authentication, cookie, and password
|
||||
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
|
||||
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,
|
||||
and direct `wp-config.php` requests, and 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.
|
||||
direct `wp-config.php` requests, the WordPress readme/license files, direct
|
||||
installation entry points, public mu-plugin directory probing, and external
|
||||
`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
|
||||
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.
|
||||
@@ -23,6 +30,12 @@
|
||||
WordPress/Apache retains the capabilities needed by the official image internally,
|
||||
but no WordPress port is published. A containerized security test fails if the
|
||||
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
|
||||
root, restrict host access, never make them world-writable, and run
|
||||
`./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 --rm security-tests
|
||||
docker compose config --quiet
|
||||
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml 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
|
||||
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
|
||||
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
|
||||
checks.
|
||||
|
||||
@@ -18,6 +18,26 @@ location = /xmlrpc.php {
|
||||
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 {
|
||||
limit_req zone=login burst=5 nodelay;
|
||||
proxy_pass http://wordpress_backend;
|
||||
|
||||
@@ -48,18 +48,25 @@ test("provides usable contact actions", async ({ page }) => {
|
||||
"https://wa.me/393331234567",
|
||||
);
|
||||
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-facebook")).toHaveCount(1);
|
||||
await expect(page.locator(".social-link-youtube")).toHaveCount(1);
|
||||
await expect(page.getByRole("link", { name: "Instagram" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://instagram.com/",
|
||||
"https://instagram.com/azionelab",
|
||||
);
|
||||
await expect(page.getByRole("link", { name: "Facebook" })).toHaveAttribute(
|
||||
"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 }) => {
|
||||
@@ -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("/.env")).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-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 expect(page).toHaveURL(/\/wp-login\.php/);
|
||||
|
||||
@@ -22,7 +22,131 @@ grep -q 'azionelab-apache-hardening.conf' /workspace/wordpress/Dockerfile || {
|
||||
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 || {
|
||||
echo "Apache hardening must block PHP execution below uploads/files." >&2
|
||||
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 .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
|
||||
|
||||
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 \
|
||||
&& chmod 755 /usr/local/bin/azionelab-entrypoint \
|
||||
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \
|
||||
|
||||
@@ -10,6 +10,14 @@ TraceEnable Off
|
||||
Require all denied
|
||||
</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/">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
@@ -24,7 +24,8 @@ case "$environment" in
|
||||
esac
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -19,7 +19,10 @@ function azionelab_register_content_types(): void {
|
||||
'add_new_item' => 'Aggiungi spettacolo',
|
||||
'edit_item' => 'Modifica spettacolo',
|
||||
),
|
||||
'public' => true,
|
||||
'public' => false,
|
||||
'show_ui' => true,
|
||||
'publicly_queryable' => false,
|
||||
'exclude_from_search'=> true,
|
||||
'show_in_rest' => false,
|
||||
'menu_icon' => 'dashicons-tickets-alt',
|
||||
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes' ),
|
||||
|
||||
@@ -16,10 +16,16 @@ $social_links = array_values(
|
||||
array(
|
||||
array( 'label' => 'Instagram', 'url' => trim( azionelab_mod( 'contact_instagram' ) ), 'icon' => 'instagram' ),
|
||||
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']
|
||||
)
|
||||
);
|
||||
$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">
|
||||
<section class="hero section" id="inizio" aria-labelledby="hero-title">
|
||||
@@ -134,7 +140,7 @@ $social_links = array_values(
|
||||
?>
|
||||
<article class="show-card">
|
||||
<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>
|
||||
<?php
|
||||
++$show_index;
|
||||
@@ -187,7 +193,10 @@ $social_links = array_values(
|
||||
<p class="eyebrow">Parliamone</p>
|
||||
<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>
|
||||
<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 ) : ?>
|
||||
<div class="social-promo">
|
||||
<p class="social-promo-title">Seguici sui social</p>
|
||||
@@ -195,7 +204,7 @@ $social_links = array_values(
|
||||
<?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">
|
||||
<?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>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@@ -203,9 +212,15 @@ $social_links = array_values(
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<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>
|
||||
<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="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 if ( $contact_email ) : ?>
|
||||
<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>
|
||||
<?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>
|
||||
</section>
|
||||
|
||||
@@ -29,8 +29,9 @@ function azionelab_defaults(): array {
|
||||
'contact_email' => 'ciao@azionelab.org',
|
||||
'contact_phone' => '+39 333 123 4567',
|
||||
'contact_whatsapp' => '+39 333 123 4567',
|
||||
'contact_instagram' => 'https://instagram.com/',
|
||||
'contact_facebook' => 'https://facebook.com/',
|
||||
'contact_instagram' => '',
|
||||
'contact_facebook' => '',
|
||||
'contact_youtube' => '',
|
||||
'contact_address' => "Via dell'Epomeo 9999, Napoli",
|
||||
'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 {
|
||||
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 ) {
|
||||
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 '';
|
||||
@@ -149,6 +174,7 @@ function azionelab_customize_register( WP_Customize_Manager $customizer ): void
|
||||
'contact_whatsapp' => array( 'azionelab_contacts', 'WhatsApp', 'text' ),
|
||||
'contact_instagram' => array( 'azionelab_contacts', 'Instagram', 'url' ),
|
||||
'contact_facebook' => array( 'azionelab_contacts', 'Facebook', 'url' ),
|
||||
'contact_youtube' => array( 'azionelab_contacts', 'YouTube', 'url' ),
|
||||
'contact_address' => array( 'azionelab_contacts', 'Indirizzo', 'text' ),
|
||||
'footer_text' => array( 'azionelab_contacts', 'Testo footer', 'text' ),
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ Text Domain: azionelab
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
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 {
|
||||
margin: 0;
|
||||
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; }
|
||||
.contacts { color: var(--color-bg-light); background: var(--color-dark); }
|
||||
.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-actions { display: grid; gap: 0.8rem; }
|
||||
.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 { display: grid; position: relative; margin-top: 1.6rem; padding-left: 3rem; }
|
||||
.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-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-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 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-link-instagram { background: linear-gradient(135deg, #f58529 0%, #dd2a7b 48%, #8134af 100%); }
|
||||
.social-link-facebook { background: #1877f2; }
|
||||
.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; }
|
||||
.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-links a:hover { background: white; transform: translateY(-2px); }
|
||||
.social-icon { width: 2.45rem; height: 2.45rem; flex: 0 0 auto; }
|
||||
.site-footer { padding-block: 2rem; color: #f7efe4; background: var(--color-dark-text); }
|
||||
.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; }
|
||||
@@ -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); }
|
||||
.gallery-grid { grid-template-columns: repeat(4, 1fr); }
|
||||
.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) {
|
||||
|
||||
@@ -46,6 +46,7 @@ wp option update permalink_structure '/%postname%/'
|
||||
if ! wp theme is-active azionelab; then
|
||||
wp theme activate azionelab
|
||||
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
|
||||
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