PHPackages                             artetecha/upsun-wp - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. artetecha/upsun-wp

ActiveWordpress-muplugin

artetecha/upsun-wp
==================

Must-use plugin integrating WordPress with the Upsun platform: environment awareness, router-cache friendliness, safe preview clones, deploy migrations, Site Health checks, and a wp upsun CLI command.

1.1.0(2w ago)0404—6.7%1MITPHPPHP &gt;=8.1CI passing

Since Jul 13Pushed 2w agoCompare

[ Source](https://github.com/artetecha/upsun-wp)[ Packagist](https://packagist.org/packages/artetecha/upsun-wp)[ Docs](https://upsun.artetecha.com/)[ RSS](/packages/artetecha-upsun-wp/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (3)Versions (32)Used By (1)

upsun-wp — the Upsun mu-plugin for WordPress
============================================

[](#upsun-wp--the-upsun-mu-plugin-for-wordpress)

Platform integration for WordPress running on [Upsun](https://upsun.com): environment awareness, router-cache friendliness, safe preview clones, deploy migrations, Cloudflare front-end support, Upsun-specific Site Health checks, and a `wp upsun` CLI command.

**Site &amp; docs: [upsun.artetecha.com](https://upsun.artetecha.com/)**

The plugin detects Upsun at runtime (`PLATFORM_APPLICATION_NAME` + `PLATFORM_ENVIRONMENT`) and **fully no-ops anywhere else** — local development and CI need no special-casing. It reads platform variables directly and never defines WordPress configuration constants: your `wp-config.php` stays the single owner of database credentials, URLs, salts, and `WP_ENVIRONMENT_TYPE`.

This is a generic plugin for any WordPress project on Upsun; site-specific behavior belongs in the consuming project via the filters below — never in this package. It is used in production by two unrelated sites — an LMS/commerce site and a second migrated to the starter model at 1.0 — both consuming it exclusively through the public filter/constant API, which is what validates that the generic-vs-site-specific boundary holds. A companion [starter repository](https://github.com/artetecha/wordpress-upsun-starter) — a deploy-ready Composer WordPress on Upsun, pre-wired for this plugin — is live.

Installation (Composer-managed WordPress)
-----------------------------------------

[](#installation-composer-managed-wordpress)

Three steps: require the package, route the install path (and copy the loader shim), and wire the post\_deploy hook.

**1. Require the package**

```
// composer.json
{
  "require": {
    "artetecha/upsun-wp": "^1.0"
  }
}
```

**2. Route the install path and copy the loader shim.** WordPress does not scan mu-plugin subdirectories, so a shim always has to reach the mu-plugins root; where the package itself may install depends on your layout.

*Content directory OUTSIDE the core install dir* (Bedrock-style): the standard route works — the package lands in `mu-plugins/upsun/` (via its `installer-name`) and only the shim needs copying:

```
"extra": {
  "installer-paths": {
    "web/app/mu-plugins/{$name}": ["type:wordpress-muplugin"]
  }
},
"scripts": {
  "post-install-cmd": [
    "cp web/app/mu-plugins/upsun/upsun-loader.php web/app/mu-plugins/upsun-loader.php"
  ]
}
```

*Content directory INSIDE the core install dir* (johnpbloch-style `wordpress/wp-content/...`): **do not route this package into `wordpress/`.** Composer installs independent packages in alphabetical order; `artetecha/*` sorts before `johnpbloch/*`, and the WordPress core extraction replaces the entire install dir — silently deleting anything placed there earlier. Route the package to a staging directory and copy it in with the shim:

```
"extra": {
  "installer-paths": {
    "composer-mu-plugins/{$name}": ["artetecha/upsun-wp"],
    "wordpress/wp-content/mu-plugins/{$name}": ["type:wordpress-muplugin"]
  }
},
"scripts": {
  "postbuild": [
    "mkdir -p wordpress/wp-content/mu-plugins",
    "rm -rf wordpress/wp-content/mu-plugins/upsun",
    "cp -R composer-mu-plugins/upsun wordpress/wp-content/mu-plugins/upsun",
    "cp composer-mu-plugins/upsun/upsun-loader.php wordpress/wp-content/mu-plugins/upsun-loader.php"
  ],
  "post-install-cmd": "@postbuild",
  "post-update-cmd": "@postbuild"
}
```

(Add `/composer-mu-plugins/` and the copied files to `.gitignore`; scripts run after every install, so the copy is always fresh.)

**3. Wire preview sanitize into the post\_deploy hook.** Data syncs redeploy an environment **without a code change, so only the `post_deploy` hook runs** — `deploy` does not, which makes `post_deploy` the only hook that can catch every clone and resync. Add one line to `.upsun/config.yaml` that is safe on every environment (production refreshes the stamp that makes its clones detectable; already-sanitized previews no-op):

```
hooks:
  post_deploy: |
    wp upsun sanitize --if-needed
```

This line is also where your **sanitization policy** lives: `--enable` forces the opt-in DB-writing sanitizers for the run, so the whole policy is declared at project level in versioned config and applied identically to every child environment (or vary it per environment type with a small script):

```
hooks:
  post_deploy: |
    wp upsun sanitize --if-needed --enable="anonymize-user-emails,anonymize-user-passwords:password-{ID}"
```

Skipping this step does **not** weaken the runtime preview protections (mail interception, payment test mode, webhook pausing are active on every preview request from boot) — it only means the one-time `upsun_preview_sanitize` consumer actions never fire. The "Preview safety" health check (Site Health, the Upsun dashboard, `wp upsun doctor`) warns on every environment until the wiring is in place. If you cannot edit your hooks, enable the per-boot fallback via the `upsun_safe_previews_boot_check` filter.

Modules
-------

[](#modules)

ModuleWhat it does`cloudflare`For sites proxied by Cloudflare in front of the Upsun router. **The Upsun router already resolves the real client IP into `REMOTE_ADDR`** (verified: `REMOTE_ADDR` == `CF-Connecting-IP` == `X-Client-IP`, and Cloudflare's edge never appears in `REMOTE_ADDR`/`X-Forwarded-For`), so this module does **not** rewrite it — that would be redundant and, on a direct origin hit, spoofable. It detects Cloudflare via the `CF-Ray`/`CF-Connecting-IP` headers and adds a health check + dashboard panel that confirm fronting and that `REMOTE_ADDR` agrees with `CF-Connecting-IP`. Adds `wp upsun cloudflare purge` — the edge invalidation the Upsun router cache never had — and registers the backend behind `Upsun\purge_paths()` so consumer code can invalidate without knowing which CDN is in front, with optional auto-purge of a post's URL on change, and an optional shared-secret origin guard (off by default) that rejects production requests bypassing Cloudflare. Inert where Cloudflare isn't fronting, so it's safe to leave enabled everywhere.`security-headers`Emits baseline security response headers on the front end — `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `X-Frame-Options: SAMEORIGIN`. These protect the **HTML document**, which on Upsun can't be covered from `config.yaml` (its `web.locations` `headers` only decorate static files; dynamic passthru responses get headers from the app). HSTS is handled deliberately: when the `cloudflare` module detects the request is proxied, the edge owns HSTS and this module **defers** (no duplicate header) — otherwise, on a direct-Upsun production site over HTTPS, it emits HSTS itself. Either way there's exactly one source, and Site Health + the dashboard say which. CSP is intentionally left to consumers (it's inherently per-site). Header set is filterable via `upsun_security_headers`.`environment-indicator`Color-coded admin-bar badge (branch · environment type) with an Upsun Console link, a dashboard widget with environment metadata, and a matching banner on the login screen.`page-cache`Emits `Cache-Control: public, max-age=0, s-maxage={ttl}` on anonymous, session-free page views so the Upsun router can cache them; optionally strips configured Set-Cookie headers (e.g. LMS guest sessions) to keep responses cacheable. Built-in bypass patterns cover core session cookies; commerce patterns come from the Integrations layer. `wp upsun cache-check ` (also a form in the dashboard Caching panel) explains any page's verdict: effective TTL, Set-Cookie spoilers, bypass-pattern matches, the route cookie allowlist (declared via `upsun_cache_check_route_cache` — Upsun does not expose it at runtime), and whether the fetch was a router HIT/MISS/BYPASS.`updates-policy`Disables the in-app auto-update machinery (the filesystem is read-only; Composer is the update path), replaces the auto-update toggles with a note, and removes the core Site Health tests that would fail by design.`site-health`Upsun-specific Site Health checks: object cache round-trip, cron configuration, writable mounts, preview search visibility, deploy migrations, live relationship health (MySQL ping, Redis INFO, HTTP/cluster status), disk usage, pending vendored/premium updates, and the active vendored-update fetchers; plus an "Upsun" section in the Info tab.`preview-protection`Sends `X-Robots-Tag: noindex, nofollow` and robots meta on non-production environments, without touching the `blog_public` option (the database is a production clone).`smtp`Points PHPMailer at the on-platform relay (`PLATFORM_SMTP_HOST`, port 25) unless a mailer plugin already configured SMTP.`dashboard`A top-level "Upsun" page in wp-admin (`manage_options`) styled like the WP Dashboard: panels are real meta boxes in the core dashboard grid — collapsible, draggable between columns, layout persisted per user. Panels: environment, services (credentials never rendered), health checks, resolved caching config, module status; plus operational actions (flush object cache). Extensible via `upsun_dashboard_panels`; deliberately actions-not-settings — configuration stays in code.`cron-heartbeat`Proves cron *executes*, not just that it is configured: schedules a recurring event that stamps a timestamp option, and reports staleness (plus overdue-event counts) through Site Health, the dashboard, and `wp upsun doctor`.`mount-usage`Disk and mount visibility: live disk total/free from the mount filesystem (warn at 80% used, fail at 95% — full mounts are a rude way to discover a quota), plus a per-mount size breakdown computed daily via WP-Cron (walking uploads is expensive) and shown with its age in a "Disk &amp; mounts" dashboard panel and the shared checks.`writable-paths`Advises on the writable-path needs of known plugins: Integrations declare where plugins write, the check compares that against the mounts declared in `PLATFORM_APPLICATION`, and `wp upsun mounts` prints ready-to-paste mount YAML for anything missing. Advisory-only by design — on Upsun the fix is a mount, not a runtime path redirection.`safe-previews`Neuters live outbound integrations on preview clones, runtime-only (never DB writes): intercepts `wp_mail` (or redirects it) built-in; the WooCommerce integrations contribute Stripe test-mode forcing and webhook pausing through the same registry. Fresh clones and data syncs are detected via an environment stamp and sanitized by `wp upsun sanitize --if-needed` in the post\_deploy hook (installation step 3), which runs the opt-in DB-writing sanitizers (anonymize user emails/passwords, deactivate listed plugins, scrub listed options — all disabled by default, enabled via filters) and fires `upsun_preview_sanitize` so consumers can scrub their own integrations; registries extensible via `upsun_safe_previews_actions` and `upsun_preview_sanitizers`. Adds a "Preview safety" health check and dashboard panel that warn when the hook wiring is missing.Integrations
------------

[](#integrations)

Everything the plugin knows about one specific third-party plugin lives in a dedicated class under `src/Integrations/` — the single place to answer "what does this plugin do about X?". Integrations contribute **exclusively through the same public filters consumers use** (never privileged internal calls), so every built-in integration doubles as proof the public API is sufficient. They register at `muplugins_loaded` before regular plugins load; every contribution is a dormant no-op when its target plugin is absent, and the dashboard's Modules panel reports each integration's boot state plus whether the target was detected.

IntegrationTargetContributions`woocommerce`WooCommerceSession/cart cookies as page-cache bypass patterns; cart/checkout/account pages as page-cache skips; webhook-delivery pause as a SafePreviews protection.`woocommerce-stripe`WooCommerce Stripe gatewayTest mode forced at option-read time on previews as a SafePreviews protection (cloned live keys stay untouched and unused). Plus a "Stripe keys" dashboard panel: in test mode, both test keys are probed against Stripe (cached 12h, keyed on the key value) and reported per key — valid / INVALID / missing / unverified — with a warning that checkout will fail when a key is dead or absent; test keys that Stripe *rejects* leave the gateway available and silently broken, which the admin otherwise never shows. Live mode is reported, never probed.`wordfence`WordfenceAdvisory: declares `wp-content/wflogs` as a writable-path requirement.`updraftplus`UpdraftPlusAdvisory: declares `wp-content/updraft` as a writable-path requirement.`wp-rocket`WP RocketAdvisory: declares `wp-content/cache` and `wp-content/wp-rocket-config`; notes the `advanced-cache.php` root drop-in (not mountable — copy at build time).Toggles mirror modules: the `upsun_integrations` filter, or `UPSUN_DISABLE_INTEGRATION_{ID}` constants (e.g. `UPSUN_DISABLE_INTEGRATION_WOOCOMMERCE`, `UPSUN_DISABLE_INTEGRATION_WP_ROCKET`). To support a plugin the package doesn't know, use the public filters directly from your own mu-plugin — that is exactly what the built-in integrations do.

Configuration
-------------

[](#configuration)

### Constants (wp-config friendly)

[](#constants-wp-config-friendly)

- `UPSUN_MU_DISABLE` — kill switch for the whole plugin.
- `UPSUN_DISABLE_CLOUDFLARE`, `UPSUN_DISABLE_SECURITY_HEADERS`, `UPSUN_DISABLE_ENVIRONMENT_INDICATOR`, `UPSUN_DISABLE_PAGE_CACHE`, `UPSUN_DISABLE_UPDATES_POLICY`, `UPSUN_DISABLE_SITE_HEALTH`, `UPSUN_DISABLE_PREVIEW_PROTECTION`, `UPSUN_DISABLE_SMTP`, `UPSUN_DISABLE_DASHBOARD`, `UPSUN_DISABLE_CRON_HEARTBEAT`, `UPSUN_DISABLE_SAFE_PREVIEWS`, `UPSUN_DISABLE_WRITABLE_PATHS`, `UPSUN_DISABLE_MOUNT_USAGE` — per-module switches.
- `UPSUN_DISABLE_INTEGRATION_WOOCOMMERCE`, `UPSUN_DISABLE_INTEGRATION_WOOCOMMERCE_STRIPE`, `UPSUN_DISABLE_INTEGRATION_WORDFENCE`, `UPSUN_DISABLE_INTEGRATION_UPDRAFTPLUS`, `UPSUN_DISABLE_INTEGRATION_WP_ROCKET` — per-integration switches.
- `UPSUN_DISABLE_FETCHER_THIMPRESS` — turns off the built-in ThimPress vendored-update fetcher (it is already inert without thim-core).
- `UPSUN_DISABLE_FETCHER_TRANSIENT` — turns off the universal fallback fetcher. Note this disables generic vendored-update resolution entirely: with no fallback, only packages claimed by a specific fetcher can be resolved.
- `UPSUN_MIGRATIONS_DIR` — directory of deploy migrations (see below); unset = feature idle.
- `UPSUN_MU_FORCE` — boot modules and integrations off-platform (testing against faked `PLATFORM_*` variables).

Defined by the plugin, readable by consumers: `UPSUN_MU_PLUGIN_DIR` (the plugin's own directory) and `UPSUN_MU_PLUGIN_VERSION` (also returned by `Upsun\version()`).

### Filters

[](#filters)

Module boot is deferred to `muplugins_loaded` priority 0, so **any mu-plugin** can register these regardless of load order.

> **Frozen at 1.0.** These names change only through the [deprecation policy](docs/api-reference.md#deprecation-policy). The seven renames and eight toggle replacements introduced in 0.7 had their shims removed at 1.0 — see [Upgrading from 0.x](#upgrading-from-0x) if you are coming from 0.6 or earlier.

FilterTypeDefaultPurpose`upsun_modules``array`all modulesAdd/remove/replace modules.`upsun_module_enabled``bool`, `string $id``true`Toggle a single module by id (`page-cache`, `smtp`, …). The conditional counterpart to `UPSUN_DISABLE_{MODULE}`, which is read first and wins.`upsun_integration_enabled``bool`, `string $id``true`Toggle a single integration by id (`woocommerce`, `wp-rocket`, …). Counterpart to `UPSUN_DISABLE_INTEGRATION_{ID}`.`upsun_fetcher_enabled``bool`, `string $id``true`Toggle a built-in vendoring fetcher by id (`thimpress`, `transient`). Counterpart to `UPSUN_DISABLE_FETCHER_{ID}`.`upsun_integrations``array`all integrationsAdd/remove/replace third-party plugin integrations.`upsun_page_cache_ttl``int``600`Shared-cache TTL in seconds; `
