| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Which release channel the running build belongs to: a deliberately |
| 9 |
* published pre-release (`4.3.3-beta.2`, `4.4.0-rc.1`) or an ordinary stable |
| 10 |
* release (`4.3.2`). |
| 11 |
* |
| 12 |
* The distinction exists so that instrumentation which is legitimate on a |
| 13 |
* build handed to one consenting user, but NOT legitimate on the thousands of |
| 14 |
* ordinary installs a wp.org release reaches, can arm itself from a fact the |
| 15 |
* build already carries instead of from a switch somebody has to remember to |
| 16 |
* flip. That memory is not hypothetical: the detach A/B experiment |
| 17 |
* (ABJ_404_Solution_DetachAbExperiment::isEnabled(), Bruno |
| 18 |
* timeout cause matrix gap G9) shipped completely inert because its filter |
| 19 |
* defaulted false and nothing in the codebase or the packaging steps ever set |
| 20 |
* it true. A gate whose only "on" path is a manual step is a gate that ships |
| 21 |
* off. |
| 22 |
* |
| 23 |
* The version string is the single source of truth (the plugin header, read |
| 24 |
* into ABJ404_VERSION by Loader.php), because it is the one thing that cannot |
| 25 |
* be true of the source tree and false of the built artifact: packaging cannot |
| 26 |
* produce a beta zip whose header does not say beta. |
| 27 |
* |
| 28 |
* Parsing follows semver's rule, not a substring search for "beta": a version |
| 29 |
* is a pre-release exactly when a hyphen-introduced identifier follows the |
| 30 |
* numeric version core. Build metadata (`+20260722`) is NOT a pre-release |
| 31 |
* marker under that rule and is deliberately treated as stable. |
| 32 |
* |
| 33 |
* Fail-safe by construction: a version that does not parse at all reports |
| 34 |
* CHANNEL_UNKNOWN, and isPreRelease() collapses both 'stable' and 'unknown' to |
| 35 |
* false. An install this class cannot identify is never opted into anything. |
| 36 |
* The channel itself stays a three-value answer rather than a boolean so a |
| 37 |
* diagnostic record can say WHICH of "stable build" or "unparseable version" |
| 38 |
* kept an experiment inert, instead of leaving a reader to infer it. |
| 39 |
*/ |
| 40 |
final class ABJ_404_Solution_PluginReleaseChannel { |
| 41 |
|
| 42 |
/** An ordinary published release: `4.3.2`, `4.3.2+20260722`. */ |
| 43 |
const CHANNEL_STABLE = 'stable'; |
| 44 |
|
| 45 |
/** A deliberately published pre-release: `4.3.3-beta.2`, `4.4.0-rc.1`. */ |
| 46 |
const CHANNEL_PRERELEASE = 'prerelease'; |
| 47 |
|
| 48 |
/** The version string is absent or unparseable; treated as stable for gating. */ |
| 49 |
const CHANNEL_UNKNOWN = 'unknown'; |
| 50 |
|
| 51 |
/** Numeric version core: `4`, `4.3`, `4.3.3`. */ |
| 52 |
const VERSION_CORE = '\d+(?:\.\d+)*'; |
| 53 |
|
| 54 |
/** Semver identifier body, used for both the pre-release and build-metadata tails. */ |
| 55 |
const IDENTIFIER_TAIL = '[0-9A-Za-z.\-]+'; |
| 56 |
|
| 57 |
/** |
| 58 |
* Classify one version string. Pure: takes the version rather than |
| 59 |
* reading the constant, so every case (stable, pre-release, build |
| 60 |
* metadata, empty, garbage) is directly assertable without a process |
| 61 |
* whose ABJ404_VERSION happens to hold that value -- a constant cannot be |
| 62 |
* redefined mid-process, which would otherwise make most of these cases |
| 63 |
* untestable. |
| 64 |
* |
| 65 |
* @param mixed $version |
| 66 |
*/ |
| 67 |
public static function channelForVersion($version): string { |
| 68 |
$candidate = is_scalar($version) ? trim((string)$version) : ''; |
| 69 |
if ($candidate === '') { |
| 70 |
return self::CHANNEL_UNKNOWN; |
| 71 |
} |
| 72 |
$buildMetadata = '(?:\+' . self::IDENTIFIER_TAIL . ')?'; |
| 73 |
if (preg_match('/^' . self::VERSION_CORE . '-' . self::IDENTIFIER_TAIL . $buildMetadata . '$/', $candidate) === 1) { |
| 74 |
return self::CHANNEL_PRERELEASE; |
| 75 |
} |
| 76 |
if (preg_match('/^' . self::VERSION_CORE . $buildMetadata . '$/', $candidate) === 1) { |
| 77 |
return self::CHANNEL_STABLE; |
| 78 |
} |
| 79 |
return self::CHANNEL_UNKNOWN; |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* The running build's channel, reported honestly (including 'unknown') |
| 84 |
* so it can be journaled as evidence rather than only acted on. |
| 85 |
*/ |
| 86 |
public static function currentChannel(): string { |
| 87 |
return self::channelForVersion(defined('ABJ404_VERSION') ? ABJ404_VERSION : ''); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Whether this build may arm pre-release-only instrumentation. Only a |
| 92 |
* parsed pre-release qualifies: 'unknown' deliberately behaves like |
| 93 |
* 'stable' here, because the cost of guessing wrong is instrumentation |
| 94 |
* running on real installs that never opted into it. |
| 95 |
*/ |
| 96 |
public static function isPreRelease(): bool { |
| 97 |
return self::currentChannel() === self::CHANNEL_PRERELEASE; |
| 98 |
} |
| 99 |
} |
| 100 |
|