| 1 |
<?php |
| 2 |
|
| 3 |
namespace gVectors\License; |
| 4 |
|
| 5 |
// Exit if accessed directly |
| 6 |
use gVectors\License\Services\ActionsService; |
| 7 |
use gVectors\License\Services\AddonsService; |
| 8 |
use gVectors\License\Services\ApiService; |
| 9 |
use gVectors\License\Services\LicenseService; |
| 10 |
|
| 11 |
if( ! defined( 'ABSPATH' ) ) exit; |
| 12 |
|
| 13 |
class LicenseModule { |
| 14 |
/** |
| 15 |
* @deprecated Use LicenseModule::getActionsService($slug) instead. |
| 16 |
* Kept for backward compatibility — always points to the last-instantiated plugin's service. |
| 17 |
*/ |
| 18 |
public static $actionsService; |
| 19 |
|
| 20 |
/** |
| 21 |
* Keyed registry: slug → ActionsService instance. |
| 22 |
* Allows multiple plugins to coexist without overwriting each other. |
| 23 |
*/ |
| 24 |
private static $instances = []; |
| 25 |
|
| 26 |
public function __construct( Config $config ) { |
| 27 |
$actionsService = new ActionsService( $config, new AddonsService( $config, new LicenseService( $config, new ApiService( $config ) ) ) ); |
| 28 |
self::$actionsService = $actionsService; // backward compat |
| 29 |
self::$instances[ $config->get_core_plugin_slug() ] = $actionsService; |
| 30 |
new AdminPage( $config ); |
| 31 |
} |
| 32 |
|
| 33 |
/** |
| 34 |
* Get the ActionsService for a specific core plugin slug. |
| 35 |
*/ |
| 36 |
public static function getActionsService( string $slug ): ?ActionsService { |
| 37 |
return self::$instances[ $slug ] ?? null; |
| 38 |
} |
| 39 |
|
| 40 |
/** |
| 41 |
* Generate a unique site token for authenticating with the proxy server. |
| 42 |
* Based on raw domain + WordPress auth salts - unique per installation, not guessable. |
| 43 |
* Uses AUTH_SALT + SECURE_AUTH_SALT for maximum entropy. |
| 44 |
* Falls back to NONCE_SALT or LOGGED_IN_SALT if the primary salts are missing. |
| 45 |
* All standard WordPress installations define these in wp-config.php. |
| 46 |
*/ |
| 47 |
public static function get_site_token(): string { |
| 48 |
$domain = self::get_site_domain(); |
| 49 |
$salt = self::get_auth_salt(); |
| 50 |
return hash_hmac( 'sha256', $domain, $salt ); |
| 51 |
} |
| 52 |
|
| 53 |
/** |
| 54 |
* Get a strong, unpredictable salt for HMAC token generation. |
| 55 |
* Combines multiple WordPress salts for maximum entropy. |
| 56 |
* Refuses to use a hardcoded fallback — the site must have proper salts configured. |
| 57 |
*/ |
| 58 |
private static function get_auth_salt(): string { |
| 59 |
$parts = []; |
| 60 |
if( defined( 'AUTH_SALT' ) && AUTH_SALT !== '' ) $parts[] = AUTH_SALT; |
| 61 |
if( defined( 'SECURE_AUTH_SALT' ) && SECURE_AUTH_SALT !== '' ) $parts[] = SECURE_AUTH_SALT; |
| 62 |
if( defined( 'LOGGED_IN_SALT' ) && LOGGED_IN_SALT !== '' ) $parts[] = LOGGED_IN_SALT; |
| 63 |
if( defined( 'NONCE_SALT' ) && NONCE_SALT !== '' ) $parts[] = NONCE_SALT; |
| 64 |
|
| 65 |
if( ! empty( $parts ) ) { |
| 66 |
return implode( '|', $parts ); |
| 67 |
} |
| 68 |
|
| 69 |
// Absolute last resort: use DB-based unique key (wp_options: siteurl + DB password hash) |
| 70 |
// This is still unique per installation, unlike a hardcoded string |
| 71 |
return hash( 'sha256', DB_NAME . ':' . DB_USER . ':' . self::get_site_domain() ); |
| 72 |
} |
| 73 |
|
| 74 |
/** |
| 75 |
* Get the raw site domain (no protocol, no www, no trailing slash). |
| 76 |
* e.g. "example.com" or "sub.example.com" |
| 77 |
*/ |
| 78 |
public static function get_site_domain(): string { |
| 79 |
return self::normalize_domain( get_site_url() ); |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Normalize a site domain for comparison: lowercase, strip protocol and www, trim slashes. |
| 84 |
* Must match the server-side LicenseService::normalizeDomain() logic. |
| 85 |
*/ |
| 86 |
public static function normalize_domain( string $url ): string { |
| 87 |
$url = rtrim( strtolower( trim( $url ) ), '/' ); |
| 88 |
$url = preg_replace( '#^https?://#', '', $url ); |
| 89 |
$url = preg_replace( '#^www\.#', '', $url ); |
| 90 |
// Strip path — keep only host(:port), e.g. localhost/subpath → localhost |
| 91 |
return explode( '/', $url )[0]; |
| 92 |
} |
| 93 |
|
| 94 |
/** |
| 95 |
* Detect if the current WordPress installation is running on a development, local, or staging environment. |
| 96 |
* Development sites are exempt from signature verification and tamper detection. |
| 97 |
* |
| 98 |
* Detects: |
| 99 |
* - localhost / 127.0.0.1 / ::1 / 0.0.0.0 |
| 100 |
* - IP addresses (private ranges: 10.x, 172.16-31.x, 192.168.x, and any raw IP) |
| 101 |
* - Local TLDs: .local, .loc, .test, .localhost, .example, .invalid, .internal, .home |
| 102 |
* - Virtual host dev TLDs: .ddev.site, .lndo.site, .nip.io, .sslip.io, .xip.io |
| 103 |
* - Known staging/dev subdomains: dev.*, staging.*, stage.*, test.*, local.* |
| 104 |
* - Known temporary site patterns: *.instawp.xyz, *.tastewp.com |
| 105 |
* - WordPress environment type set to 'local', 'development', or 'staging' |
| 106 |
* - WP_LOCAL_DEV or WP_DEBUG constants |
| 107 |
* - Domains with port numbers (e.g., site.com:8080) |
| 108 |
* |
| 109 |
* Results are cached per request via a static variable. |
| 110 |
* |
| 111 |
* @return bool True if this is a development/local/staging environment. |
| 112 |
*/ |
| 113 |
public static function is_development_site(): bool { |
| 114 |
static $is_dev = null; |
| 115 |
if( $is_dev !== null ) return $is_dev; |
| 116 |
|
| 117 |
$site_domain = strtolower( self::get_site_domain() ); |
| 118 |
|
| 119 |
// Strip protocol |
| 120 |
$host = preg_replace( '#^https?://#', '', $site_domain ); |
| 121 |
// Strip path |
| 122 |
$host = explode( '/', $host )[0]; |
| 123 |
// Separate port if present |
| 124 |
$port = ''; |
| 125 |
if( preg_match( '/^(\[.*]):(\d+)$/', $host, $m ) ) { |
| 126 |
// IPv6 with port: [::1]:8080 |
| 127 |
$host = $m[1]; |
| 128 |
$port = $m[2]; |
| 129 |
} elseif( preg_match( '/^([^:]+):(\d+)$/', $host, $m ) ) { |
| 130 |
$host = $m[1]; |
| 131 |
$port = $m[2]; |
| 132 |
} |
| 133 |
|
| 134 |
// Strip brackets from IPv6 |
| 135 |
$host = trim( $host, '[]' ); |
| 136 |
|
| 137 |
// 1) Localhost / loopback |
| 138 |
$loopbacks = [ 'localhost', '127.0.0.1', '::1', '0.0.0.0' ]; |
| 139 |
if( in_array( $host, $loopbacks, true ) ) { |
| 140 |
$is_dev = true; |
| 141 |
return true; |
| 142 |
} |
| 143 |
|
| 144 |
// 2) Raw IP address (no real domain) |
| 145 |
if( filter_var( $host, FILTER_VALIDATE_IP ) ) { |
| 146 |
$is_dev = true; |
| 147 |
return true; |
| 148 |
} |
| 149 |
|
| 150 |
// 3) Non-standard port (real production sites don't use ports in URLs) |
| 151 |
if( $port && ! in_array( $port, [ '80', '443' ], true ) ) { |
| 152 |
$is_dev = true; |
| 153 |
return true; |
| 154 |
} |
| 155 |
|
| 156 |
// 4) Dev / staging domain suffixes |
| 157 |
// IMPORTANT: Keep in sync with Auth::BLOCKED_SUFFIXES on the server side. |
| 158 |
$dev_suffixes = [ |
| 159 |
// RFC 2606 / IANA reserved TLDs |
| 160 |
'.local', '.loc', '.test', '.localhost', '.example', '.invalid', |
| 161 |
|
| 162 |
// Common local / dev TLDs |
| 163 |
'.internal', '.home', '.lan', '.dev', '.dev.cc', |
| 164 |
'.staging', '.stg', '.qa', '.preprod', '.preview', |
| 165 |
|
| 166 |
// Virtual host / tunnel services |
| 167 |
'.ddev.site', '.lndo.site', |
| 168 |
'.nip.io', '.sslip.io', '.xip.io', |
| 169 |
'.ngrok.io', '.ngrok-free.app', |
| 170 |
'.serveo.net', '.localtunnel.me', |
| 171 |
'.trycloudflare.com', |
| 172 |
'.loca.lt', |
| 173 |
|
| 174 |
// Temporary / throwaway WordPress hosting |
| 175 |
'.instawp.xyz', '.tastewp.com', '.tempurl.host', |
| 176 |
|
| 177 |
// Managed WordPress staging environments |
| 178 |
'.myftpupload.com', |
| 179 |
'.cloudwaysapps.com', |
| 180 |
'.wpengine.com', '.wpengine.net', |
| 181 |
'.flywheelstaging.com', |
| 182 |
'.kinsta.cloud', |
| 183 |
'.platformsh.site', |
| 184 |
'.bigscoots-staging.com', |
| 185 |
'.wpmudev.host', |
| 186 |
'.closte.com', |
| 187 |
'.pressdns.com', |
| 188 |
'.accessdomain.com', |
| 189 |
]; |
| 190 |
foreach( $dev_suffixes as $suffix ) { |
| 191 |
if( substr( $host, -strlen( $suffix ) ) === $suffix ) { |
| 192 |
$is_dev = true; |
| 193 |
return true; |
| 194 |
} |
| 195 |
} |
| 196 |
|
| 197 |
// 5) Dev/staging subdomains |
| 198 |
$dev_prefixes = [ |
| 199 |
'localhost.', 'local.', |
| 200 |
'dev.', 'develop.', |
| 201 |
'staging.', 'stage.', 'stg.', |
| 202 |
'test.', 'testing.', |
| 203 |
'demo.', 'sandbox.', |
| 204 |
'preprod.', 'pre-prod.', 'preview.', |
| 205 |
'uat.', 'acceptance.', 'acc.', 'qa.', |
| 206 |
]; |
| 207 |
foreach( $dev_prefixes as $prefix ) { |
| 208 |
if( strpos( $host, $prefix ) === 0 ) { |
| 209 |
$is_dev = true; |
| 210 |
return true; |
| 211 |
} |
| 212 |
} |
| 213 |
|
| 214 |
// 6) Regex-based staging patterns (numbered staging, hosting-specific) |
| 215 |
$dev_regex_patterns = [ |
| 216 |
'#^staging\d+\.#i', |
| 217 |
'#^stg\d+\.#i', |
| 218 |
'#^dev\d+\.#i', |
| 219 |
'#^(dev|test)-[^.]+\.pantheonsite\.io$#i', |
| 220 |
'#^[^.]*staging[^.]*\.kinsta\.(com|cloud)$#i', |
| 221 |
]; |
| 222 |
foreach( $dev_regex_patterns as $pattern ) { |
| 223 |
if( preg_match( $pattern, $host ) ) { |
| 224 |
$is_dev = true; |
| 225 |
return true; |
| 226 |
} |
| 227 |
} |
| 228 |
|
| 229 |
// 7) WordPress environment type (WP 5.5+) |
| 230 |
if( function_exists( 'wp_get_environment_type' ) ) { |
| 231 |
$env = wp_get_environment_type(); |
| 232 |
if( in_array( $env, [ 'local', 'development', 'staging' ], true ) ) { |
| 233 |
$is_dev = true; |
| 234 |
return true; |
| 235 |
} |
| 236 |
} |
| 237 |
|
| 238 |
$is_dev = false; |
| 239 |
return false; |
| 240 |
} |
| 241 |
|
| 242 |
} |
| 243 |
|