PluginProbe
wpForo Forum / 3.2.1
wpForo Forum v3.2.1
3.2.1 3.2.0 3.1.7 3.1.6 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 All 141 releases
wpforo / admin / pages / license / src / LicenseModule.php

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

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