PluginProbe
wpForo Forum / 3.1.5
wpForo Forum v3.1.5
3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 1.4.2 All 137 releases
wpforo / admin / pages / license / src / LicenseModule.php

LicenseModule.php in wpForo Forum 3.1.5, at admin/pages/license/src/LicenseModule.php

243 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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