Skip to main content

InstallIdentity

\Benecaster\Staging\InstallIdentity

Class Free

Answers one question: is this still the install it was when it first armed itself? The site records its own canonical home URL once, and from then on anything money-related is allowed only while that still matches. This is clone detection, and it is a deliberately different question from StagingManager::is_staging(), which is environment detection by hostname pattern.

Each catches what the other misses. A clone at an ordinary hostname matches no staging pattern, so the environment check allows it and this class refuses it. A fresh install that has always lived at a staging domain arms itself there and reads as production here, while the environment check refuses it. If you are writing an add-on that moves money, this is the signal you want — not the environment one.

It turns the clone’s own mechanism against it, and that is the property worth preserving. A staging copy is a database copy, so it inherits the stored value pointing at production while running at the staging domain. Nobody has to remember to configure the copy — the safe state is not something the podcaster can forget.

Three invariants hold it up, and each one breaks the guard if violated. The capture must never overwrite an existing value, or a clone’s first admin page load silently re-arms it as production and the protection is gone before anyone sees it. An uncaptured install is production, because deploying this to an existing live site must not stop its commerce before it has served a single admin page load — every uncertain state fails open. And key mode is not the test: a live key on a staging copy is exactly where mode says “live” and the answer must still be refuse.

The escape hatch is the BENECASTER_IS_PRODUCTION constant in wp-config.php. It can only live there: a database-stored override would be wiped by the next staging refresh, and would travel onto a copy if a staging database were ever restored the other way. Without an override at all, a legitimate domain change would lock a production site out of its own commerce with no route back that did not involve hand-editing the database.

Resolve the class from the service container rather than constructing it: benecaster()->container()->make( \Benecaster\Staging\InstallIdentity::class ).

Constructor Dependencies

Type Description
\Benecaster\Staging\EnvironmentResolver Reads BENECASTER_IS_PRODUCTION. Optional — it defaults to a fresh instance. This class no longer reads the constant itself.

Methods

Method Visibility Since Description
is_production(): bool Public Whether this install may move money. Returns true when the production override constant is set, when no URL has ever been captured, or when the captured URL still matches the current one. Fails open on every uncertain state, by design — a false positive here stops a real business taking real money, which is worse than the hazard being guarded against. Do not "tighten" it to return false on an unarmed install.
is_non_production_copy(): bool Public True only when this install is demonstrably a copy of another one. The inverse of is_production(), provided so calling code can read in the affirmative.
captured_url(): string Public The canonical URL this install armed itself at, or an empty string when it has never armed. The returned value is scheme-less — host, optional non-default port, and trimmed path — because two installs are compared by that form.
current_url(): string Public This install's canonical URL right now, reduced to the same comparable form as captured_url().
production_override_active(): bool Public Whether the operator has declared this install production outright via the BENECASTER_IS_PRODUCTION constant. Exposed separately because the override must beat every signal, not just this class's — a guard that also consults an environment check has to short-circuit on this first, or a production site on a host that merely looks like staging can never save live keys and has no way out. ⚠ It no longer reads the constant. It asks EnvironmentResolver::production_declared(), which is also what StagingManager::is_staging() now consults — so the two answers cannot disagree. See the EnvironmentResolver page for why that mattered.
maybe_capture(): void Public Records this install's canonical URL, once and only once, on the first admin page load it ever serves. The early return on an existing value is the whole guard — never "refresh" it, and never delete it on upgrade. Registered on admin_init; you should not need to call it yourself.
register(): void Public Hooks maybe_capture() onto admin_init. Called once during plugin bootstrap.

Constants

Name Value Description
OPTION benecaster_canonical_home_url The WordPress option holding the captured URL. Autoload is off. Read it through this class rather than directly — a stored value is scheme-less and needs canonicalising to compare correctly.

Notes

URL comparison ignores scheme deliberately. A staging copy served over http while production runs https is the same install by every measure that matters here, and treating the two as different would refuse commerce on a production site the day it gained a certificate. Host is lowercased, a leading www. is stripped, default ports are dropped, the trailing slash is trimmed, and query and fragment are discarded.

Canonicalisation is idempotent over its own output, and there is a regression test pinning exactly that. It matters more than it looks: a stored value is already scheme-less, which URL parsing reads as a path rather than a host — so a naive re-canonicalisation returns an empty string, which reads as "unarmed" and lets the install re-capture itself. That would defeat the never-overwrite invariant silently, on precisely the clone this exists to catch.