PluginProbe
phpinfo() WP – Site Health, PHP Compatibility & Server Audit / 7.2.5
phpinfo() WP – Site Health, PHP Compatibility & Server Audit v7.2.5
7.2.7 7.2.6 7.2.5 7.2.4 7.2.3 7.2.0 7.2.1 7.2.2 7.1.0 7.0.3 7.0.4 7.0.5 trunk 6.0 7.0.0 7.0.1 7.0.2
phpinfo-wp / includes / class-license.php

class-license.php in phpinfo() WP – Site Health, PHP Compatibility & Server Audit 7.2.5, at includes/class-license.php

245 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined('ABSPATH') or die('Unauthorized Access');
3
4 class Phpinfo_WP_License {
5
6 const OPT_KEY = 'phpinfowp_license_key';
7 const OPT_CACHE = 'phpinfowp_lic_cache';
8 const OPT_FAILS = 'phpinfowp_lic_fails';
9 const OPT_LOCKED = 'phpinfowp_lic_locked';
10 const MAX_FAILS = 2;
11 const PING_URL = 'https://exeebit.com/api/license/validate';
12
13 // Secret assembled from fragments
14 private const _F1 = "\x50\x49\x57\x50";
15 private const _F2 = "\x5f\x70\x72\x6f";
16 private const _F3 = "\x5f\x73\x65\x63";
17
18 private static function _hmac_secret(): string {
19 static $s;
20 if ($s !== null) return $s;
21 $raw = self::_F1 . self::_F2 . self::_F3 . "\x72\x65\x74\x5f\x76\x31";
22 $s = hash('sha256', $raw, true);
23 return $s;
24 }
25
26 // Cache secret uses site's AUTH_KEY — unique per WP install.
27 // Injecting a fake "valid" row into wp_options fails because the MAC won't verify.
28 private static function _cache_secret(): string {
29 static $cs;
30 if ($cs !== null) return $cs;
31 $salt = defined('AUTH_KEY') ? AUTH_KEY : (defined('SECURE_AUTH_KEY') ? SECURE_AUTH_KEY : 'phpinfowp_fallback_salt');
32 $cs = hash('sha256', $salt . 'phpinfowp_cache_v1', true);
33 return $cs;
34 }
35
36 private static function _cache_write(bool $valid): void {
37 $data = json_encode(['v' => (int) $valid, 't' => time()]);
38 $mac = hash_hmac('sha256', $data, self::_cache_secret());
39 update_option(self::OPT_CACHE, $mac . '|' . base64_encode($data), false);
40 }
41
42 private static function _cache_read(): ?bool {
43 $stored = get_option(self::OPT_CACHE, '');
44 if (!$stored || strpos($stored, '|') === false) return null;
45
46 [$mac, $data_b64] = explode('|', $stored, 2);
47 $data = base64_decode($data_b64);
48 if (!$data) return null;
49
50 $expected = hash_hmac('sha256', $data, self::_cache_secret());
51 if (!hash_equals($expected, $mac)) return null; // Tampered
52
53 $parsed = json_decode($data, true);
54 if (!isset($parsed['v'], $parsed['t'])) return null;
55 if (time() - (int) $parsed['t'] > 6 * HOUR_IN_SECONDS) return null; // Stale
56
57 return (bool) $parsed['v'];
58 }
59
60 private static function _cache_clear(): void {
61 delete_option(self::OPT_CACHE);
62 }
63
64 // --- Public API ---
65
66 public static function get_key(): string {
67 return (string) get_option(self::OPT_KEY, '');
68 }
69
70 // Parse the active key's payload for UI display. Returns null when the
71 // key is missing or malformed. Does NOT re-verify the HMAC — call
72 // is_valid() for that. Returned keys: email, url, exp, iat (all from
73 // the payload), plus derived: days_left, expiry_human, is_lifetime.
74 public static function payload(): ?array {
75 $key = self::get_key();
76 if (!$key) return null;
77
78 $parts = explode('-', $key, 3);
79 if (count($parts) !== 3 || $parts[0] !== 'PIWP') return null;
80
81 $raw = base64_decode(strtr($parts[1], '-_', '+/'));
82 if (!$raw) return null;
83 $p = json_decode($raw, true);
84 if (!isset($p['exp'], $p['email'])) return null;
85
86 $exp = (int) $p['exp'];
87 $days_left = (int) floor(($exp - time()) / DAY_IN_SECONDS);
88 // Lifetime keys use a sentinel year-2099 timestamp — 50+ years out
89 // means we treat it as lifetime rather than print "26,000 days left".
90 $is_life = $days_left > (50 * 365);
91
92 return [
93 'email' => (string) $p['email'],
94 'url' => (string) ($p['url'] ?? ''),
95 'exp' => $exp,
96 'iat' => (int) ($p['iat'] ?? 0),
97 'days_left' => $days_left,
98 'expiry_human' => $is_life ? 'Lifetime' : date_i18n(get_option('date_format'), $exp),
99 'is_lifetime' => $is_life,
100 ];
101 }
102
103 public static function is_valid(): bool {
104 if (self::is_locked()) return false;
105
106 $cached = self::_cache_read();
107 if ($cached !== null) return $cached;
108
109 $valid = self::_validate_local(self::get_key());
110 self::_cache_write($valid);
111 return $valid;
112 }
113
114 public static function is_unlimited(): bool {
115 if (!self::is_valid()) return false;
116 $p = self::payload();
117 if (!$p) return false;
118 if (isset($p['iat']) && (int) $p['iat'] < 1781523600) {
119 return true;
120 }
121 return trim((string) ($p['url'] ?? '')) === '*';
122 }
123
124 public static function is_locked(): bool {
125 return (bool) get_option(self::OPT_LOCKED, false);
126 }
127
128 public static function activate(string $key): bool {
129 $key = sanitize_text_field(trim($key));
130 update_option(self::OPT_KEY, $key, false);
131 self::_cache_clear();
132 delete_option(self::OPT_FAILS);
133 delete_option(self::OPT_LOCKED);
134
135 // Always ping remote during manual activation to track it in real-time
136 // and enforce site limits immediately.
137 $valid = self::_ping_remote($key);
138 if (!$valid) {
139 // Fallback to local check if remote server is unreachable or offline
140 $valid = self::_validate_local($key);
141 }
142 self::_cache_write($valid);
143 return $valid;
144 }
145
146 public static function deactivate(): void {
147 delete_option(self::OPT_KEY);
148 self::_cache_clear();
149 delete_option(self::OPT_FAILS);
150 delete_option(self::OPT_LOCKED);
151 }
152
153 // Called by weekly wp_cron. After MAX_FAILS consecutive failures, locks Pro.
154 public static function cron_ping(): void {
155 $key = self::get_key();
156 if (!$key) return;
157
158 $ok = self::_ping_remote($key);
159 if ($ok) {
160 update_option(self::OPT_FAILS, 0, false);
161 delete_option(self::OPT_LOCKED);
162 self::_cache_clear();
163 return;
164 }
165
166 $fails = (int) get_option(self::OPT_FAILS, 0) + 1;
167 update_option(self::OPT_FAILS, $fails, false);
168 if ($fails >= self::MAX_FAILS) {
169 update_option(self::OPT_LOCKED, 1, false);
170 self::_cache_clear();
171 }
172 }
173
174 // --- Key validation ---
175
176 public static function _validate_local(string $key): bool {
177 if (empty($key)) return false;
178
179 // Format: PIWP-{base64url_payload}-{32_char_hmac}
180 $parts = explode('-', $key, 3);
181 if (count($parts) !== 3 || $parts[0] !== 'PIWP') return false;
182
183 $payload_b64 = $parts[1];
184 $sig = $parts[2];
185
186 $expected = substr(hash_hmac('sha256', $payload_b64, self::_hmac_secret()), 0, 32);
187 if (!hash_equals($expected, strtolower($sig))) return false;
188
189 $payload_raw = base64_decode(strtr($payload_b64, '-_', '+/'));
190 if (!$payload_raw) return false;
191
192 $p = json_decode($payload_raw, true);
193 if (!isset($p['url'], $p['exp'], $p['email'])) return false;
194
195 if ((int) $p['exp'] < time()) return false;
196
197 // Wildcard "*" = unlimited/lifetime tiers; server enforces site limits
198 // via activation tracking. Otherwise require exact site URL match.
199 $key_url = trim((string) $p['url']);
200 if ($key_url !== '*') {
201 $site = rtrim(strtolower(get_site_url()), '/');
202 if ($site !== rtrim(strtolower($key_url), '/')) return false;
203 }
204
205 return true;
206 }
207
208 private static function _ping_remote(string $key): bool {
209 $resp = wp_remote_post(self::PING_URL, [
210 'timeout' => 12,
211 'body' => [
212 'license_key' => $key,
213 'site_url' => get_site_url(),
214 'plugin_v' => PHPINFOWP_VERSION,
215 ],
216 ]);
217
218 if (is_wp_error($resp)) return false;
219 if (wp_remote_retrieve_response_code($resp) !== 200) return false;
220
221 $body = json_decode(wp_remote_retrieve_body($resp), true);
222 return !empty($body['valid']);
223 }
224
225 public static function schedule_remote_check_event(): void {
226 if (!wp_next_scheduled('phpinfowp_license_ping')) {
227 wp_schedule_event(time() + WEEK_IN_SECONDS, 'weekly', 'phpinfowp_license_ping');
228 }
229 }
230
231 // --- Key generator (server-side helper, called from licensing plugin) ---
232
233 public static function generate_key(string $email, string $site_url, int $expiry_ts): string {
234 $payload = json_encode([
235 'email' => $email,
236 'url' => rtrim(strtolower($site_url), '/'),
237 'exp' => $expiry_ts,
238 'iat' => time(),
239 ]);
240 $b64 = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
241 $sig = substr(hash_hmac('sha256', $b64, self::_hmac_secret()), 0, 32);
242 return "PIWP-{$b64}-{$sig}";
243 }
244 }
245