). */ public const AJAX_PREFIX = 'prweb_'; /** Prefix for the plugin's custom tables, appended to $wpdb->prefix. */ public const TABLE_PREFIX = 'prweb_'; /** Prefix for the cookies this plugin sets on a visitor's browser. */ public const COOKIE_PREFIX = 'prweb_'; /** * Prefix for wp_enqueue_script / wp_enqueue_style handles. * The handle registry is flat and install-wide: first registration of a * name wins and a later one is dropped silently, so a generic handle is a * collision, not a cosmetic issue. WP.org's prefix guideline names * wp_register_script() and wp_localize_script() explicitly. */ public const ASSET_PREFIX = 'prweb-'; /** * Root name of the JS globals this plugin puts on `window`. * wp_localize_script's second argument is a global variable name, which is * why WP.org's prefix guideline names that function alongside * wp_register_script(). The JS side cannot read this constant — it runs in * the browser — so the bundles under assets/ repeat the literal and * scripts/check-wp-asset-handles.mjs fails when the two disagree. */ public const JS_GLOBAL = 'prweb'; /** Prefix for the plugin's own PHP constants and the env vars backing them. */ public const ENV_PREFIX = 'PRWEB'; /** * Persisted-state prefixes this plugin has shipped under previously, * newest first. An install upgrading in place still holds option rows, * meta rows, transients and tables under these — the storage-prefix * migration in includes/migrations/ * renames them forward and the uninstaller still sweeps them. Never * prune an entry: doing so orphans data on the installs that need it * most (ORL-213). */ public const LEGACY_OPTION_PREFIXES = ['webapp_', 'pds_']; /** Historical post/user/term meta prefixes — see LEGACY_OPTION_PREFIXES. */ public const LEGACY_META_PREFIXES = ['_webapp_', '_pds_']; /** Historical custom-table prefixes — see LEGACY_OPTION_PREFIXES. */ public const LEGACY_TABLE_PREFIXES = ['webapp_', 'pds_']; /** * Historical cookie prefixes — see LEGACY_OPTION_PREFIXES. These matter * for longer than the others: an option row or a table can be renamed * forward by a migration, but a cookie lives in the visitor's browser, * out of reach of any server-side step. Accepting the old spelling on * read is the only thing that keeps an already-issued session alive * across a prefix move (ORL-213). */ public const LEGACY_COOKIE_PREFIXES = ['webapp_', 'pds_']; /** Historical PHP-constant / env-var prefixes, newest first. */ public const LEGACY_ENV_PREFIXES = ['WEBAPP']; /** * Header spellings the validator signed with before the current one. * Accepted on receive for one release so a rolling deploy cannot drop a * delivery signed by a sender that has not restarted yet (ORL-213). */ public const LEGACY_WEBHOOK_HEADERS = ['X-Webapp-Signature']; /** Wallet-side counterpart of LEGACY_WEBHOOK_HEADERS. */ public const LEGACY_WALLET_WEBHOOK_HEADERS = ['X-Webapp-Wallet-Signature']; /** HMAC header the validator stamps onto webhook deliveries. */ public const WEBHOOK_HEADER = 'X-Prweb-Signature'; /** HMAC header the wallet stamps onto webhook deliveries. */ public const WALLET_WEBHOOK_HEADER = 'X-Prweb-Wallet-Signature'; /** Build a child admin page slug, e.g. page('catalog') == 'property-rights-web-catalog'. */ public static function page( string $name ): string { return self::MENU_SLUG . '-' . $name; } /** Build an option key, e.g. option('oauth_token') == 'prweb_oauth_token'. */ public static function option( string $name ): string { return self::OPTION_PREFIX . $name; } /** Build a post-meta key, e.g. meta('summary') == '_prweb_summary'. */ public static function meta( string $name ): string { return self::META_PREFIX . $name; } /** Build a hook name, e.g. hook('asset_registered') == 'prweb_asset_registered'. */ public static function hook( string $name ): string { return self::HOOK_PREFIX . $name; } /** Build an AJAX action, e.g. ajax('get_balance') == 'prweb_get_balance'. */ public static function ajax( string $name ): string { return self::AJAX_PREFIX . $name; } /** * Build an asset handle, e.g. asset('admin-shared') == 'prweb-admin-shared'. * Always call this rather than interpolating a literal: handles are global * across every plugin on the install, and the last hardcoded set survived * a whole prefix migration untouched (ORL-229). */ public static function asset( string $name ): string { return self::ASSET_PREFIX . $name; } /** * Build a JS global name, e.g. js_global() == 'prweb', * js_global('Wizard') == 'prwebWizard'. Pass the same value to * wp_localize_script() that the bundle reads off window. */ public static function js_global( string $suffix = '' ): string { return self::JS_GLOBAL . $suffix; } /** * Build a custom-table name, e.g. table('events') == 'prweb_events'. * Always call this rather than interpolating a literal: the prefix has * moved once already and the migration keys off this single definition. */ public static function table( string $name ): string { global $wpdb; return $wpdb->prefix . self::TABLE_PREFIX . $name; } /** Every spelling of a table, current first then historical. */ public static function table_candidates( string $name ): array { global $wpdb; $names = [ $wpdb->prefix . self::TABLE_PREFIX . $name ]; foreach ( self::LEGACY_TABLE_PREFIXES as $legacy ) { $names[] = $wpdb->prefix . $legacy . $name; } return $names; } /** Build a cookie name, e.g. cookie('reader') == 'prweb_reader'. */ public static function cookie( string $name ): string { return self::COOKIE_PREFIX . $name; } /** * Every spelling of a cookie, current first then historical. * Read paths MUST walk this list rather than the single current name: * see LEGACY_COOKIE_PREFIXES for why a migration cannot help here. * * @return string[] */ public static function cookie_candidates( string $name ): array { $names = [ self::cookie( $name ) ]; foreach ( self::LEGACY_COOKIE_PREFIXES as $legacy ) { $names[] = $legacy . $name; } return $names; } /** * Read an environment value by its unprefixed name, trying the current * env prefix first and then every historical one. * * The prefix rename (ORL-213) is scoped to the plugin's own PHP globals — * the names WordPress.org's PrefixAllGlobals sniff actually inspects. The * surrounding repo still exports the older spelling from .env, docker * compose, the k8s ExternalSecrets and Secret Manager, so the fallback is * what keeps an unchanged deployment working. Drop a legacy entry only * once every one of those emitters has moved. */ public static function env( string $name ): ?string { foreach ( self::env_names( $name ) as $candidate ) { $value = getenv( $candidate ); if ( is_string( $value ) && trim( $value ) !== '' ) { return $value; } } return null; } /** * Read a configured value from a PHP constant, given the name WITHOUT its * prefix — current prefix first, then every historical one. * * wp-config.php is the other half of the story env() tells. An operator's * wp-config, and the VM bootstrap that writes one, define constants rather * than exporting environment variables, so a constant left under the old * prefix has to keep resolving or a provisioned host silently loses its * funder credentials on upgrade (ORL-213). * * @return mixed The constant's value, or null when none is defined. */ public static function defined_value( string $name ) { foreach ( self::env_names( $name ) as $candidate ) { if ( defined( $candidate ) ) { return constant( $candidate ); } } return null; } /** * Resolve one of this plugin's configured values from wherever it is set: * a wp-config constant first (the operator's explicit override), then the * environment. Both halves try the current prefix before the historical * ones. * * An empty-string constant does not count as "set" here, so a stray * `define('PRWEB_X', '')` (e.g. inherited from a template) is skipped in * favour of the next candidate — the historical-prefix constant, then the * environment — instead of masking a real value behind it. This mirrors * config_origin()'s own scan rather than delegating to defined_value(): * that method returns on the first *defined* candidate regardless of * emptiness, which would end the search too early. defined_value() * itself stays raw on purpose — other callers (secrets, boolean flags) * read it directly and handle emptiness themselves, same as redirect_uri() * already does below. * * @return mixed */ public static function config( string $name ) { foreach ( self::env_names( $name ) as $candidate ) { if ( defined( $candidate ) ) { $value = constant( $candidate ); if ( ! is_string( $value ) || trim( $value ) !== '' ) { return $value; } } } return self::env( $name ); } /** * Like config(), but also reports WHERE the value came from — the concrete * spelling that resolved and whether it was a wp-config constant or an * environment variable. Prweb_Config uses this to tell an operator which * `define()` is doing the work and whether they are still on a legacy * (`WEBAPP_*`) spelling. * * Must be called BEFORE the plugin bootstrap define()s the canonical * `PRWEB_` constant for this key, or that define() masks the real * origin. * * @return array{value: ?string, source: ?string, name: ?string} * `source` is 'wp-config', 'env', or null when nothing is set. */ public static function config_origin( string $name ): array { foreach ( self::env_names( $name ) as $candidate ) { if ( defined( $candidate ) ) { $value = constant( $candidate ); if ( is_scalar( $value ) && trim( (string) $value ) !== '' ) { return [ 'value' => (string) $value, 'source' => 'wp-config', 'name' => $candidate ]; } } } foreach ( self::env_names( $name ) as $candidate ) { $value = getenv( $candidate ); if ( is_string( $value ) && trim( $value ) !== '' ) { return [ 'value' => $value, 'source' => 'env', 'name' => $candidate ]; } } return [ 'value' => null, 'source' => null, 'name' => null ]; } /** The canonical (current-prefix) constant name for one configured value. */ public static function env_name( string $name ): string { return self::ENV_PREFIX . '_' . $name; } /** * Every spelling of one of this plugin's configured names, current first. * * Public so Prweb_Config::report() can tell a legacy spelling of this * plugin's own variable (a member of this list) apart from a * cross-component alias like `AUTH_PUBLIC_URL` (never a member — see * property-rights-web.php's alias handling) — the two look identical as * a bare `found_as` string, but only the first has a canonical spelling * to move to. * * @return string[] */ public static function env_names( string $name ): array { $names = []; foreach ( array_merge( [ self::ENV_PREFIX ], self::LEGACY_ENV_PREFIXES ) as $prefix ) { $names[] = $prefix . '_' . $name; } return $names; } /** * WP.org-visible Plugin Name header value (the ™ form). * Authoritative copy for the wp.org listing — see ADR-003. */ public static function plugin_header_name(): string { return self::PLUGIN_HEADER_NAME; } /** * WP REST normalizes header names to lower-snake (`X-Prweb-Signature` * → `x_prweb_signature`) when looked up via $req->get_header(). */ public static function webhook_header_normalized(): string { return strtolower( str_replace( '-', '_', self::WEBHOOK_HEADER ) ); } /** * WP REST normalizes header names to lower-snake (`X-Prweb-Wallet-Signature` * → `x_prweb_wallet_signature`) when looked up via $req->get_header(). */ public static function wallet_webhook_header_normalized(): string { return strtolower( str_replace( '-', '_', self::WALLET_WEBHOOK_HEADER ) ); } }