PluginProbe
Plugin Compatibility Checker / trunk
Plugin Compatibility Checker vtrunk
plugin-compatibility-checker / plugin-compatibility-checker.php

plugin-compatibility-checker.php in Plugin Compatibility Checker trunk, at plugin-compatibility-checker.php

1,282 lines 62.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Plugin Compatibility Checker (Portal-integrated)
4 * Description: Check which WordPress and PHP versions your plugins are compatible with before updating WordPress.adds Portal integration: license settings, request scan, fetch latest result, cron poller, admin email on new results.
5 * Version: 7.0.7
6 * Author: CompatShield
7 * Author URI: https://www.compatshield.com/
8 */
9
10
11 if ( ! defined('ABSPATH') ) exit;
12
13 if ( ! class_exists('PCC') ) :
14
15 class PCC {
16
17 const MENU_SLUG = 'PCC_Check';
18 const SUBMENU_SYSINFO_SLUG = 'websiteinfo';
19 const SUBMENU_SETTINGS_SLUG = 'pcc_settings';
20 const CAP_SINGLE = 'manage_options';
21 const CAP_NETWORK = 'manage_network';
22 const TRANSIENT_KEY = 'pcc_scan_results';
23 const TRANSIENT_TTL_DEFAULT = 6 * HOUR_IN_SECONDS;
24 const NONCE_ACTION = 'pcc_rescan';
25 const AJAX_ACTION = 'pcc_rescan';
26
27 const OPTION_LICENSE_KEY = 'pcc_license_key';
28 const OPTION_LICENSE_VALID = 'pcc_license_valid';
29 const OPTION_REMOTE_MAP = 'pcc_remote_php_compat';
30 const CRON_HOOK = 'pcc_cron_fetch_remote';
31 const PORTAL_API = 'https://www.compatshield.com/portal-api';
32 const SIGNUP_URL = 'https://www.compatshield.com/product'; // free plan signup
33 const PRODUCT_URL = 'https://www.compatshield.com/product/'; // pricing / pro
34
35 public function __construct() {
36 if ( is_multisite() ) {
37 add_action('network_admin_menu', [ $this, 'register_network_menus' ]);
38 } else {
39 add_action('admin_menu', [ $this, 'register_menus' ]);
40 }
41
42 add_action('admin_enqueue_scripts', [ $this, 'enqueue_assets' ]);
43 add_action('wp_ajax_' . self::AJAX_ACTION, [ $this, 'ajax_rescan' ]);
44 add_action('wp_ajax_pcc_request_scan', [ $this, 'ajax_request_scan' ]);
45 add_action('wp_ajax_pcc_fetch_remote', [ $this, 'ajax_fetch_remote' ]);
46 add_action('wp_ajax_pcc_validate_license', [ $this, 'ajax_validate_license' ]);
47
48 add_action('init', [ $this, 'maybe_schedule_cron' ]);
49 add_action(self::CRON_HOOK, [ $this, 'cron_fetch_remote' ]);
50
51 register_deactivation_hook(__FILE__, [ $this, 'on_deactivate' ]);
52 }
53
54 public function on_deactivate() {
55 $ts = wp_next_scheduled(self::CRON_HOOK);
56 if ($ts) wp_unschedule_event($ts, self::CRON_HOOK);
57 }
58
59 /* Menus */
60 public function register_menus() {
61 add_menu_page(
62 __('Plugin Compatibility Checker', 'pcc'),
63 __('Plugin Compatibility Checker', 'pcc'),
64 self::CAP_SINGLE,
65 self::MENU_SLUG,
66 [ $this, 'render_single_site' ],
67 'dashicons-screenoptions',
68 90
69 );
70
71 add_submenu_page(self::MENU_SLUG, __('System Info', 'pcc'), __('System Info', 'pcc'), self::CAP_SINGLE, self::SUBMENU_SYSINFO_SLUG, [ $this, 'render_sysinfo' ]);
72 add_submenu_page(self::MENU_SLUG, __('PCC Settings', 'pcc'), __('Settings', 'pcc'), self::CAP_SINGLE, self::SUBMENU_SETTINGS_SLUG, [ $this, 'render_settings' ]);
73 }
74
75 public function register_network_menus() {
76 add_menu_page(__('Plugin Compatibility Checker', 'pcc'), __('Plugin Compatibility Checker', 'pcc'), self::CAP_NETWORK, self::MENU_SLUG, [ $this, 'render_network' ], 'dashicons-screenoptions', 90);
77 add_submenu_page(self::MENU_SLUG, __('System Info', 'pcc'), __('System Info', 'pcc'), self::CAP_NETWORK, self::SUBMENU_SYSINFO_SLUG, [ $this, 'render_sysinfo' ]);
78 add_submenu_page(self::MENU_SLUG, __('PCC Settings', 'pcc'), __('Settings', 'pcc'), self::CAP_NETWORK, self::SUBMENU_SETTINGS_SLUG, [ $this, 'render_settings' ]);
79 }
80
81 /* Assets */
82 public function enqueue_assets($hook) {
83 $screen = get_current_screen();
84 if ( empty($screen->id) ) return;
85 if ( false === strpos($screen->id, self::MENU_SLUG) && false === strpos($screen->id, self::SUBMENU_SYSINFO_SLUG) && false === strpos($screen->id, self::SUBMENU_SETTINGS_SLUG) ) return;
86
87 wp_enqueue_style('pcc-custom', plugin_dir_url(__FILE__) . 'customcss/pcccustom.css', [], '1.0.0');
88 wp_enqueue_style('pcc-bootstrap', plugin_dir_url(__FILE__) . 'customcss/bootstrap.min.css', [], '1.0.1');
89
90 wp_enqueue_script('pcc-filter', plugin_dir_url(__FILE__) . 'customjs/filtertable.js', ['jquery'], '1.0.0', true);
91 wp_enqueue_script('pcc-export', plugin_dir_url(__FILE__) . 'customjs/export.js', ['jquery'], '1.0.0', true);
92
93 wp_register_script('pcc-rescan', plugin_dir_url(__FILE__) . 'customjs/pcc-rescan.js', ['jquery'], '1.0.0', true);
94 wp_localize_script('pcc-rescan', 'PCCVars', [
95 'ajaxUrl' => admin_url('admin-ajax.php'),
96 'nonce' => wp_create_nonce(self::NONCE_ACTION),
97 'action' => self::AJAX_ACTION,
98 ]);
99 wp_enqueue_script('pcc-rescan');
100
101 $settings_rel = 'customjs/pcc-settings.js';
102 $settings_path = plugin_dir_path(__FILE__) . $settings_rel;
103 $settings_url = plugin_dir_url(__FILE__) . $settings_rel;
104
105 $license = trim(get_option(self::OPTION_LICENSE_KEY, ''));
106 $license_valid = (bool) get_option(self::OPTION_LICENSE_VALID, false);
107
108 if ( file_exists($settings_path) ) {
109 wp_register_script('pcc-settings-js', $settings_url, ['jquery'], '1.0.2', true);
110 wp_localize_script('pcc-settings-js', 'PCCSettings', [
111 'ajaxUrl' => admin_url('admin-ajax.php'),
112 'validateAction' => 'pcc_validate_license',
113 'requestScanAction' => 'pcc_request_scan',
114 'fetchRemoteAction' => 'pcc_fetch_remote',
115 'nonce' => wp_create_nonce('pcc_settings_nonce'),
116 'siteUrl' => get_site_url(),
117 'licenseActive' => ($license !== '' && $license_valid),
118 ]);
119 wp_enqueue_script('pcc-settings-js');
120 } else {
121 wp_register_script('pcc-settings-fallback', false);
122 wp_enqueue_script('pcc-settings-fallback');
123 $localized = [
124 'ajaxUrl' => admin_url('admin-ajax.php'),
125 'validateAction' => 'pcc_validate_license',
126 'requestScanAction' => 'pcc_request_scan',
127 'fetchRemoteAction' => 'pcc_fetch_remote',
128 'nonce' => wp_create_nonce('pcc_settings_nonce'),
129 'siteUrl' => get_site_url(),
130 'licenseActive' => ($license !== '' && $license_valid),
131 ];
132 wp_localize_script('pcc-settings-fallback', 'PCCSettings', $localized);
133 $inline_handlers = <<<JS
134 (function($){
135 $(function(){});
136 })(jQuery);
137 JS;
138 wp_add_inline_script('pcc-settings-fallback', $inline_handlers);
139 }
140 }
141
142 /* Server-side helper: validate license against portal validate endpoint */
143 private function server_validate_license($license, $site_url = '') {
144 if ( empty($license) ) return new WP_Error('missing_license', 'Missing license');
145
146 $license = trim($license);
147
148 $site_raw = trim($site_url ?: get_site_url());
149 $host = parse_url($site_raw, PHP_URL_HOST);
150 if ( ! $host ) {
151 $tmp = @parse_url('https://' . ltrim($site_raw, '/'));
152 $host = $tmp ? ($tmp['host'] ?? '') : '';
153 }
154 $site_to_send = $host ? trim(strtolower($host)) : trim($site_raw);
155
156 $validate_url = rtrim(self::PORTAL_API, '/') . '/validate_license.php';
157
158 $payload = [
159 'site_url' => $site_to_send,
160 'license' => $license,
161 ];
162
163 $args = [
164 'headers' => [
165 'X-Portal-License' => $license,
166 'Content-Type' => 'application/json',
167 ],
168 'timeout' => 10,
169 'body' => wp_json_encode($payload),
170 ];
171
172 if ( defined('WP_DEBUG') && WP_DEBUG ) {
173 error_log('[PCC] validate_license -> POST ' . $validate_url . ' payload=' . wp_json_encode($payload));
174 }
175
176 $res = wp_remote_post($validate_url, $args);
177 if ( is_wp_error($res) ) return $res;
178
179 $code = wp_remote_retrieve_response_code($res);
180 $body = wp_remote_retrieve_body($res);
181 $json = json_decode($body, true);
182
183 if ( defined('WP_DEBUG') && WP_DEBUG ) {
184 error_log('[PCC] validate_license response_code=' . $code . ' body=' . $body);
185 }
186
187 if ( $code >= 200 && $code < 300 && is_array($json) && isset($json['valid']) && $json['valid'] === true ) {
188 return $json;
189 }
190
191 if ( is_array($json) && isset($json['valid']) && $json['valid'] === false ) {
192 return new WP_Error('license_invalid', 'License not valid', ['portal' => $json, 'http_code' => $code, 'body' => $body]);
193 }
194
195 return new WP_Error('invalid_response', 'License validation failed', ['http_code' => $code, 'body' => $body, 'json' => $json]);
196 }
197
198 /**
199 * Get full DB version string using SELECT VERSION().
200 * Returns the raw string e.g. "8.0.32" or "10.6.14-MariaDB".
201 * Use this instead of $wpdb->db_version() which strips the MariaDB suffix.
202 */
203 private function get_db_version() {
204 global $wpdb;
205 $full = $wpdb->get_var('SELECT VERSION()');
206 return $full ?: 'unknown';
207 }
208
209 /**
210 * Parse the full DB version string into type and clean version number.
211 * Returns array: ['type' => 'MySQL'|'MariaDB', 'clean' => '8.0.32', 'full' => '8.0.32-log']
212 */
213 private function parse_db_version($full_db = '') {
214 if ( empty($full_db) || $full_db === 'unknown' ) {
215 return [ 'type' => 'MySQL', 'clean' => 'unknown', 'full' => 'unknown' ];
216 }
217 $is_maria = stripos($full_db, 'mariadb') !== false;
218 $clean = preg_replace('/[^0-9.].*$/', '', $full_db);
219 return [
220 'type' => $is_maria ? 'MariaDB' : 'MySQL',
221 'clean' => $clean,
222 'full' => $full_db,
223 ];
224 }
225
226 /**
227 * Normalize a site URL to host-only (example.com or sub.example.com).
228 */
229 private function normalize_site_host($site_raw = '') {
230 $site_raw = trim($site_raw ?: get_site_url());
231 if ( function_exists('wp_parse_url') ) {
232 $host = wp_parse_url($site_raw, PHP_URL_HOST);
233 } else {
234 $parts = @parse_url($site_raw);
235 $host = $parts['host'] ?? null;
236 }
237 if ( ! $host ) {
238 $tmp = @parse_url('https://' . ltrim($site_raw, '/'));
239 $host = $tmp ? ($tmp['host'] ?? null) : null;
240 }
241 return $host ? strtolower(trim($host)) : trim($site_raw);
242 }
243
244 /* AJAX: Rescan */
245 public function ajax_rescan() {
246 check_ajax_referer(self::NONCE_ACTION, 'nonce');
247
248 if ( ! current_user_can(self::CAP_SINGLE) ) {
249 return wp_send_json_error(['message'=>'forbidden'], 403);
250 }
251
252 if ( is_multisite() ) delete_site_transient(self::TRANSIENT_KEY);
253 else delete_transient(self::TRANSIENT_KEY);
254
255 $license = trim(get_option(self::OPTION_LICENSE_KEY, ''));
256 $license_valid = (bool) get_option(self::OPTION_LICENSE_VALID, false);
257
258 if ( empty($license) ) {
259 delete_option(self::OPTION_REMOTE_MAP);
260 $this->get_scan_results(true);
261 return wp_send_json_success(['from_remote'=>false,'updated'=>0,'scan_pending'=>false,'message'=>'no_license_fallback']);
262 }
263
264 if ( ! $license_valid ) {
265 $v = $this->server_validate_license($license, get_site_url());
266 if ( is_wp_error($v) ) {
267 update_option(self::OPTION_LICENSE_VALID, false);
268 delete_option(self::OPTION_REMOTE_MAP);
269 $this->get_scan_results(true);
270 return wp_send_json_error(['message'=>'license_invalid_or_network','detail'=>$v->get_error_message()], 401);
271 } else {
272 update_option(self::OPTION_LICENSE_KEY, $license);
273 update_option(self::OPTION_LICENSE_VALID, true);
274 $license_valid = true;
275 }
276 }
277
278 $endpoint = rtrim(self::PORTAL_API, '/') . '/receive_client.php';
279 $site_host = $this->normalize_site_host(get_site_url());
280
281 // Build DB info cleanly before constructing payload
282 $db_full = $this->get_db_version();
283 $db_parsed = $this->parse_db_version($db_full);
284
285 $payload = [
286 'site_url' => $site_host,
287 'wp_version' => get_bloginfo('version'),
288 'php_version'=> phpversion(),
289 'server_info'=> [
290 'os' => PHP_OS,
291 'sapi' => php_sapi_name(),
292 'db_version' => $db_parsed['clean'],
293 'db_type' => $db_parsed['type'],
294 'db_version_full' => $db_parsed['full'],
295 ],
296 'components' => $this->build_components_payload(),
297 ];
298
299 $args = [
300 'headers' => [
301 'Content-Type' => 'application/json',
302 'X-Portal-License' => $license,
303 ],
304 'body' => wp_json_encode($payload),
305 'timeout' => 20,
306 ];
307
308 if ( defined('WP_DEBUG') && WP_DEBUG ) {
309 error_log('[PCC rescan] Payload sent: ' . wp_json_encode($payload));
310 }
311
312 $res = wp_remote_post($endpoint, $args);
313
314 if ( defined('WP_DEBUG') && WP_DEBUG ) {
315 error_log('[PCC rescan] HTTP code: ' . wp_remote_retrieve_response_code($res));
316 error_log('[PCC rescan] Body: ' . wp_remote_retrieve_body($res));
317 }
318
319 if ( is_wp_error($res) ) {
320 $this->get_scan_results(true);
321 return wp_send_json_success(['from_remote'=>false,'updated'=>0,'scan_pending'=>false,'remote_error'=>$res->get_error_message()]);
322 }
323
324 $code = wp_remote_retrieve_response_code($res);
325 $body = wp_remote_retrieve_body($res);
326 $json = json_decode($body, true);
327
328 if ( $code >= 200 && $code < 300 ) {
329 if ( is_array($json) && isset($json['status']) && $json['status'] === 'ok' && ! empty($json['scan_results']) ) {
330 $remote_map = get_option(self::OPTION_REMOTE_MAP, []);
331 if ( ! is_array($remote_map) ) $remote_map = [];
332 $acc = [];
333
334 foreach ( $json['scan_results'] as $sr ) {
335 $standards = $sr['standards'] ?? null;
336 if ( empty($standards) || stripos($standards,'PHPCompatibilityWP') === false ) continue;
337 if ( empty($sr['slug']) ) continue;
338
339 $is_compat = false;
340 if ( isset($sr['is_compatible']) ) {
341 $val = $sr['is_compatible'];
342 if ( is_bool($val) ) $is_compat = $val;
343 elseif ( is_numeric($val) ) $is_compat = ((int)$val === 1);
344 elseif ( is_string($val) ) $is_compat = in_array(strtolower($val), ['1','true','yes'], true);
345 }
346 if ( ! $is_compat ) continue;
347
348 $pv = $sr['php_version'] ?? '';
349 if ( ! $pv ) continue;
350 $parts = preg_split('/\s*,\s*/', trim($pv));
351 if ( ! isset($acc[$sr['slug']]) ) $acc[$sr['slug']] = [];
352 foreach ($parts as $p) { if ($p !== '') $acc[$sr['slug']][] = $p; }
353 }
354
355 $updated = 0;
356 foreach ($acc as $slug => $list) {
357 $list = array_values(array_unique($list));
358 usort($list, 'version_compare');
359 $norm = implode(', ', $list);
360 if ( ! isset($remote_map[$slug]) || $remote_map[$slug] !== $norm ) {
361 $remote_map[$slug] = $norm;
362 $updated++;
363 }
364 }
365
366 if ( $updated > 0 ) {
367 update_option(self::OPTION_REMOTE_MAP, $remote_map, false);
368 if ( is_multisite() ) delete_site_transient(self::TRANSIENT_KEY);
369 else delete_transient(self::TRANSIENT_KEY);
370 $this->get_scan_results(true);
371 }
372
373 return wp_send_json_success(['from_remote'=>true,'updated'=>$updated,'scan_pending'=>false,'portal_response'=>$json]);
374 }
375
376 $this->get_scan_results(true);
377 return wp_send_json_success(['from_remote'=>false,'updated'=>0,'scan_pending'=>true,'portal_response'=>$json]);
378 }
379
380 $this->get_scan_results(true);
381 return wp_send_json_error(['code'=>$code,'response'=>$json,'body'=>$body], 502);
382 }
383
384 /* AJAX: Request Scan (explicit) */
385 public function ajax_request_scan() {
386 if ( ! current_user_can(self::CAP_SINGLE) ) wp_send_json_error('forbidden', 403);
387 check_ajax_referer('pcc_settings_nonce', 'nonce');
388
389 $license = trim(get_option(self::OPTION_LICENSE_KEY, ''));
390 $license_valid = (bool) get_option(self::OPTION_LICENSE_VALID, false);
391 if ( empty($license) ) return wp_send_json_error(['error'=>'no_license_configured','message'=>'License not configured']);
392
393 if ( ! $license_valid ) {
394 $v = $this->server_validate_license($license, get_site_url());
395 if ( is_wp_error($v) ) {
396 update_option(self::OPTION_LICENSE_VALID, false);
397 return wp_send_json_error(['message'=>'license_invalid_or_network','detail'=>$v->get_error_message()], 401);
398 } else {
399 update_option(self::OPTION_LICENSE_VALID, true);
400 $license_valid = true;
401 }
402 }
403
404 $endpoint = rtrim(self::PORTAL_API, '/') . '/receive_client.php';
405 $site_host = $this->normalize_site_host(get_site_url());
406
407 // Build DB info cleanly before constructing payload
408 $db_full = $this->get_db_version();
409 $db_parsed = $this->parse_db_version($db_full);
410
411 $payload = [
412 'site_url' => $site_host,
413 'wp_version' => get_bloginfo('version'),
414 'php_version'=> phpversion(),
415 'server_info'=> [
416 'os' => PHP_OS,
417 'sapi' => php_sapi_name(),
418 'db_version' => $db_parsed['clean'],
419 'db_type' => $db_parsed['type'],
420 'db_version_full' => $db_parsed['full'],
421 ],
422 'components' => $this->build_components_payload(),
423 ];
424
425 $args = [
426 'headers' => [
427 'Content-Type' => 'application/json',
428 'X-Portal-License' => $license,
429 ],
430 'body' => wp_json_encode($payload),
431 'timeout' => 20,
432 ];
433
434 $res = wp_remote_post($endpoint, $args);
435 if ( is_wp_error($res) ) {
436 return wp_send_json_error(['error'=>'request_failed','message'=>$res->get_error_message()]);
437 }
438
439 $code = wp_remote_retrieve_response_code($res);
440 $body = wp_remote_retrieve_body($res);
441 $json = json_decode($body, true);
442
443 if ( $code >= 200 && $code < 300 ) {
444 if ( is_array($json) && isset($json['status']) && $json['status'] === 'ok' && ! empty($json['scan_results']) ) {
445 $remote_map = get_option(self::OPTION_REMOTE_MAP, []);
446 if ( ! is_array($remote_map) ) $remote_map = [];
447
448 $acc = [];
449 foreach ( $json['scan_results'] as $sr ) {
450 $standards = $sr['standards'] ?? null;
451 if ( empty($standards) || stripos($standards,'PHPCompatibilityWP') === false ) continue;
452 if ( empty($sr['slug']) ) continue;
453
454 $is_compat = false;
455 if ( isset($sr['is_compatible']) ) {
456 $val = $sr['is_compatible'];
457 if ( is_bool($val) ) $is_compat = $val;
458 elseif ( is_numeric($val) ) $is_compat = ((int)$val === 1);
459 elseif ( is_string($val) ) $is_compat = in_array(strtolower($val), ['1','true','yes'], true);
460 }
461 if ( ! $is_compat ) continue;
462
463 $pv = $sr['php_version'] ?? '';
464 if ( ! $pv ) continue;
465 $parts = preg_split('/\s*,\s*/', trim($pv));
466 if ( ! isset($acc[$sr['slug']]) ) $acc[$sr['slug']] = [];
467 foreach ($parts as $p) { if ($p !== '') $acc[$sr['slug']][] = $p; }
468 }
469
470 $updated = 0;
471 foreach ($acc as $slug => $list) {
472 $list = array_values(array_unique($list));
473 usort($list, 'version_compare');
474 $norm = implode(', ', $list);
475 if ( ! isset($remote_map[$slug]) || $remote_map[$slug] !== $norm ) {
476 $remote_map[$slug] = $norm;
477 $updated++;
478 }
479 }
480
481 if ( $updated > 0 ) {
482 update_option(self::OPTION_REMOTE_MAP, $remote_map, false);
483 if ( is_multisite() ) delete_site_transient(self::TRANSIENT_KEY);
484 else delete_transient(self::TRANSIENT_KEY);
485 $this->get_scan_results(true);
486 }
487
488 return wp_send_json_success(['response'=>$json,'message'=>'accepted_and_applied','applied_updated'=>$updated]);
489 }
490
491 return wp_send_json_success(['response'=>$json,'message'=>'accepted']);
492 } else {
493 return wp_send_json_error(['code'=>$code,'response'=>$json,'body'=>$body]);
494 }
495 }
496
497 /* AJAX: Fetch Remote */
498 public function ajax_fetch_remote() {
499 if ( ! current_user_can(self::CAP_SINGLE) ) wp_send_json_error('forbidden', 403);
500 check_ajax_referer('pcc_settings_nonce', 'nonce');
501
502 $result = $this->fetch_and_apply_remote_results();
503 if ( is_wp_error($result) ) {
504 $msg = $result->get_error_message();
505 return wp_send_json_error(['error'=>'fetch_failed','message'=>$msg], 502);
506 }
507
508 $updated = isset($result['updated']) ? (int)$result['updated'] : 0;
509 $scan_pending = ! empty($result['scan_pending']);
510
511 if ( $updated > 0 ) {
512 $this->get_scan_results(true);
513 }
514
515 return wp_send_json_success(['ok'=>true, 'updated'=>$updated, 'scan_pending'=>$scan_pending]);
516 }
517
518 /* AJAX: Validate license */
519 public function ajax_validate_license() {
520 if ( ! current_user_can(self::CAP_SINGLE) ) wp_send_json_error('forbidden', 403);
521 check_ajax_referer('pcc_settings_nonce', 'nonce');
522
523 $license = isset($_POST['license']) ? trim(sanitize_text_field($_POST['license'])) : get_option(self::OPTION_LICENSE_KEY, '');
524 $site_url = isset($_POST['site_url']) ? trim(sanitize_text_field($_POST['site_url'])) : get_site_url();
525
526 if ( empty($license) ) {
527 update_option(self::OPTION_LICENSE_VALID, false);
528 return wp_send_json_error(['message'=>'missing_license'], 400);
529 }
530
531 $v = $this->server_validate_license($license, $site_url);
532 if ( is_wp_error($v) ) {
533 update_option(self::OPTION_LICENSE_VALID, false);
534 $err_data = $v->get_error_data();
535 return wp_send_json_error(['message'=>'validate_failed','detail'=>$v->get_error_message(),'detail_data'=>$err_data], 502);
536 }
537
538 update_option(self::OPTION_LICENSE_KEY, $license);
539 update_option(self::OPTION_LICENSE_VALID, true);
540 return wp_send_json_success(['message'=>'license_valid','response'=>$v]);
541 }
542
543 /* Cron scheduling */
544 public function maybe_schedule_cron() {
545 add_filter('cron_schedules', function($s){
546 if (!isset($s['ten_minutes'])) $s['ten_minutes'] = ['interval'=>600, 'display'=>'Every Ten Minutes'];
547 return $s;
548 });
549
550 if ( ! wp_next_scheduled(self::CRON_HOOK) ) {
551 wp_schedule_event(time()+60, 'ten_minutes', self::CRON_HOOK);
552 }
553 }
554
555 public function cron_fetch_remote() {
556 $this->fetch_and_apply_remote_results(true);
557 }
558
559 /* Fetch & apply remote mapping */
560 protected function fetch_and_apply_remote_results($is_cron=false) {
561 $license = trim(get_option(self::OPTION_LICENSE_KEY, ''));
562 $license_valid = (bool) get_option(self::OPTION_LICENSE_VALID, false);
563
564 if ( empty(self::PORTAL_API) || empty($license) || ! $license_valid ) {
565 return new WP_Error('missing_config', 'Portal or license not configured or not validated');
566 }
567
568 $fetch_url = rtrim(self::PORTAL_API, '/') . '/fetch_data.php';
569 $payload = [ 'site_url' => $this->normalize_site_host(get_site_url()) ];
570
571 $args = [
572 'headers' => [
573 'Content-Type' => 'application/json',
574 'X-Portal-License' => $license,
575 ],
576 'body' => wp_json_encode($payload),
577 'timeout' => 20,
578 ];
579
580 $res = wp_remote_post($fetch_url, $args);
581 if ( is_wp_error($res) ) return $res;
582
583 $code = wp_remote_retrieve_response_code($res);
584 $body = wp_remote_retrieve_body($res);
585 $json = json_decode($body, true);
586
587 if ( ! is_array($json) || ! isset($json['status']) || $json['status'] !== 'ok' ) {
588 return [ 'updated' => 0, 'scan_pending' => true ];
589 }
590
591 $remote_map = get_option(self::OPTION_REMOTE_MAP, []);
592 if ( ! is_array($remote_map) ) $remote_map = [];
593
594 $updated = 0;
595 $new_map = $remote_map;
596
597 if ( isset($json['scan_results']) && is_array($json['scan_results']) && count($json['scan_results']) > 0 ) {
598 $acc = [];
599
600 foreach ( $json['scan_results'] as $sr ) {
601 $standards = $sr['standards'] ?? null;
602 if ( empty($standards) || stripos($standards,'PHPCompatibilityWP') === false ) continue;
603 if ( empty($sr['slug']) ) continue;
604 $slug = $sr['slug'];
605
606 $is_compat = false;
607 if ( isset($sr['is_compatible']) ) {
608 $val = $sr['is_compatible'];
609 if ( is_bool($val) ) $is_compat = $val;
610 elseif ( is_numeric($val) ) $is_compat = ((int)$val === 1);
611 elseif ( is_string($val) ) $is_compat = in_array(strtolower($val), ['1','true','yes'], true);
612 }
613 if ( ! $is_compat ) continue;
614
615 $pv = $sr['php_version'] ?? '';
616 if ( ! $pv ) continue;
617
618 $parts = preg_split('/\s*,\s*/', trim($pv));
619 if ( ! isset($acc[$slug]) ) $acc[$slug] = [];
620 foreach ($parts as $p) {
621 if ($p === '') continue;
622 $acc[$slug][] = $p;
623 }
624 }
625
626 foreach ($acc as $slug => $ver_list) {
627 $ver_list = array_values(array_unique($ver_list));
628 usort($ver_list, 'version_compare');
629 $norm = implode(', ', $ver_list);
630
631 if ( ! isset($new_map[$slug]) || $new_map[$slug] !== $norm ) {
632 $new_map[$slug] = $norm;
633 $updated++;
634 }
635 }
636 } else {
637 return [ 'updated' => 0, 'scan_pending' => true ];
638 }
639
640 if ( $updated > 0 ) {
641 update_option(self::OPTION_REMOTE_MAP, $new_map, false);
642 if ( is_multisite() ) delete_site_transient(self::TRANSIENT_KEY);
643 else delete_transient(self::TRANSIENT_KEY);
644
645 if ( $is_cron ) {
646 $admin_email = get_option('admin_email');
647 $subject = 'CompatShield: New remote scan results fetched';
648 $message = sprintf("Fetched %d new PHP compatibility entries from the portal for site %s\n\nVisit %s to view results.", $updated, get_site_url(), admin_url('admin.php?page=' . self::MENU_SLUG));
649 @wp_mail($admin_email, $subject, $message);
650 }
651 }
652
653 return [ 'updated' => $updated, 'scan_pending' => false ];
654 }
655
656 /* UI renderers */
657 private function stat_card($icon_url, $title, $value_html) {
658 echo '<div class="stat-card" style="display:inline-block;margin:8px;padding:18px;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.06);width:180px;text-align:center;vertical-align:top;background:#fff;">';
659 if ($icon_url) {
660 echo '<div class="stat-icon" style="height:64px;margin-bottom:8px;"><img src="'.esc_url($icon_url).'" alt="'.esc_attr($title).' Icon" style="max-height:64px;max-width:64px;"></div>';
661 }
662 echo '<div class="stat-content">';
663 echo '<h3 style="margin:6px 0 4px;font-size:16px;">'.esc_html($title).'</h3>';
664 echo '<p style="margin:0;font-size:18px;font-weight:600;">'.esc_html($value_html).'</p>';
665 echo '</div>';
666 echo '</div>';
667 }
668
669 /* Build payload for portal */
670 protected function build_components_payload() {
671 $plugins = get_plugins();
672 $out = [];
673
674 foreach ( $plugins as $file => $info ) {
675 $candidate = WP_PLUGIN_DIR . '/' . $file;
676 if ( ! file_exists($candidate) ) continue;
677
678 $slug = $this->resolve_plugin_slug($file, $info['TextDomain'] ?? '', $info['PluginURI'] ?? '');
679 $out[] = [
680 'type' => 'plugin',
681 'slug' => $slug ?: basename(dirname($file)),
682 'name' => $info['Name'] ?? $file,
683 'version' => $info['Version'] ?? '',
684 'source' => 'wporg',
685 ];
686 }
687
688 $theme = wp_get_theme();
689 $out[] = [
690 'type' => 'theme',
691 'slug' => $theme->get_stylesheet(),
692 'name' => $theme->get('Name'),
693 'version' => $theme->get('Version'),
694 'source' => 'wporg',
695 ];
696
697 return $out;
698 }
699
700 public function render_network() {
701 if ( ! current_user_can(self::CAP_NETWORK) ) {
702 wp_die(esc_html__('You do not have permission to access this page.', 'pcc'));
703 }
704 $this->render_dashboard(true);
705 }
706
707 public function render_single_site() {
708 if ( ! current_user_can(self::CAP_SINGLE) ) {
709 wp_die(esc_html__('You do not have permission to access this page.', 'pcc'));
710 }
711 $this->render_dashboard(false);
712 }
713
714 private function render_dashboard($is_network = false) {
715 $license_valid = (bool) get_option(self::OPTION_LICENSE_VALID, false);
716 $buy_link = esc_url(self::PRODUCT_URL);
717 $signup_link = esc_url(self::SIGNUP_URL);
718 $stats = $this->get_environment_stats($is_network);
719 $scan = $this->get_scan_results(false, $is_network);
720 $icons = [
721 'wp' => plugins_url('icons/wordpress.png', __FILE__),
722 'php' => plugins_url('icons/php.png', __FILE__),
723 'db' => plugins_url('icons/database-storage.png', __FILE__),
724 'plug' => plugins_url('icons/plug.png', __FILE__),
725 'active' => plugins_url('icons/check-mark.png', __FILE__),
726 'inactive' => plugins_url('icons/multiplication.png', __FILE__),
727 ];
728
729 echo '<h1 class="pluginheading">'.esc_html__('Check Your Plugin Compatibility', 'pcc').'</h1>';
730
731 if ( ! $license_valid ) {
732 echo '<div class="notice" style="margin:12px 0;padding:14px;border-left:4px solid #135e96;background:#f7fbff;">
733 <strong>Unlock PHP 8.1–8.5 checks — only $1/month (subscription).</strong>
734 Subscribe for $1/month to get a CompatShield license key and view newer PHP compatibility in this dashboard.
735 &nbsp; <a class="button button-primary" href="'.$signup_link.'" target="_blank" rel="noopener">Subscribe — $1/month </a>
736 &nbsp; <a class="button" href="'.$buy_link.'" target="_blank" rel="noopener">See Pro features</a>
737 </div>';
738 }
739
740 // Stat cards
741 echo '<div class="stats-grid">';
742 $this->stat_card($icons['wp'], 'WordPress', $stats['wp_version']);
743 $this->stat_card($icons['php'], 'PHP', $stats['php_version']);
744 $this->stat_card($icons['db'], $stats['db_type'], $stats['db_version']);
745 $this->stat_card($icons['plug'], 'Plugins Installed', (string) $stats['plugins_total']);
746 $this->stat_card($icons['active'], $is_network ? 'Plugins Active (Network)' : 'Plugins Active', (string) $stats['plugins_active']);
747 $this->stat_card($icons['inactive'], $is_network ? 'Plugins Inactive (Network)' : 'Plugins Inactive', (string) $stats['plugins_inactive']);
748 echo '</div>';
749
750 // WordPress version notice
751 if ( version_compare($stats['wp_version'], $stats['wp_latest'], '>=') ) {
752 echo '<br><b>'.esc_html__('You are already on the latest WordPress version.', 'pcc').'</b><br><br>';
753 } else {
754 echo '<br><b>'.sprintf(esc_html__('The latest stable WordPress version available is: %s', 'pcc'), esc_html($stats['wp_latest'])).'</b><br><br>';
755 }
756
757 // ── Database version warning banner ──────────────────────────────────────
758 $clean_db = $stats['db_version']; // e.g. "8.0.32" or "10.6.14"
759 $db_label = $stats['db_type']; // "MySQL" or "MariaDB"
760 $min_required = ( $db_label === 'MariaDB' ) ? '10.6' : '8.0';
761 $db_too_old = ( $clean_db !== 'unknown' ) && version_compare($clean_db, $min_required, '<');
762
763 if ( $db_too_old ) {
764 echo '<div style="margin:0 0 16px 0;padding:14px 18px;border-left:4px solid #b71c1c;background:#fff5f5;border-radius:4px;display:flex;align-items:flex-start;gap:12px;">
765 <span style="font-size:22px;line-height:1.3;">⚠️</span>
766 <div>
767 <strong style="color:#b71c1c;font-size:14px;">Database Compatibility Warning — WordPress 7.0</strong>
768 <p style="margin:6px 0 0;color:#333;font-size:13px;line-height:1.6;">
769 Your server is running <strong>' . esc_html($db_label . ' ' . $clean_db) . '</strong>,
770 but WordPress 7.0 requires a minimum of <strong>' . esc_html($db_label . ' ' . $min_required) . '</strong>.
771 Sites on older database versions will <strong>not be offered the WordPress 7.0 update</strong>
772 and will remain on 6.9 until the database is upgraded.
773 Contact your hosting provider to upgrade your database before updating WordPress.
774 </p>
775 </div>
776 </div>';
777 } else {
778 echo '<div style="margin:0 0 16px 0;padding:14px 18px;border-left:4px solid #155724;background:#f0fff4;border-radius:4px;display:flex;align-items:flex-start;gap:12px;">
779 <span style="font-size:22px;line-height:1.3;">�
780 </span>
781 <div>
782 <strong style="color:#155724;font-size:14px;">Database Compatible with WordPress 7.0</strong>
783 <p style="margin:6px 0 0;color:#333;font-size:13px;line-height:1.6;">
784 Your server is running <strong>' . esc_html($db_label . ' ' . $clean_db) . '</strong>,
785 which meets the WordPress 7.0 minimum database requirement
786 of <strong>' . esc_html($db_label . ' ' . $min_required) . '</strong>.
787 </p>
788 </div>
789 </div>';
790 }
791 // ── End database banner ──────────────────────────────────────────────────
792
793 $settings_url = admin_url('admin.php?page=' . self::SUBMENU_SETTINGS_SLUG);
794 $license = trim(get_option(self::OPTION_LICENSE_KEY, ''));
795 $license_valid = (bool) get_option(self::OPTION_LICENSE_VALID, false);
796 $remote_map = get_option(self::OPTION_REMOTE_MAP, []);
797 if ( ! is_array($remote_map) ) $remote_map = [];
798
799 echo '
800 <div class="tnip" style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
801 <div style="flex:1;min-width:260px;">
802 <b class="filter">'.esc_html__('Filter By Plugin Status', 'pcc').'</b>
803 <select class="form-control fltr" data-role="select-dropdown" id="plgstatus" style="display:inline-block;max-width:220px;margin-left:8px;">
804 <option value="all">'.esc_html__('Plugin Status', 'pcc').'</option>
805 <option value="Activated">'.esc_html__('Activated', 'pcc').'</option>
806 <option value="Deactivated">'.esc_html__('Deactivated', 'pcc').'</option>
807 </select>
808 </div>
809 <div style="flex:1;text-align:right;min-width:320px;">
810 <a href="' . esc_url($settings_url) . '" class="button button-secondary" style="margin-right:8px;">' . esc_html__('Settings', 'pcc') . '</a>
811 <button id="exportButton" class="button button-secondary" style="margin-right:8px;">'.esc_html__('Export to CSV', 'pcc').'</button>
812 <button id="pcc-fetch-latest" class="button button-secondary" style="margin-right:8px;">Fetch latest result</button>
813 <button id="pcc-rescan" class="button button-primary" style="margin-right:8px;">'.esc_html__('Rescan', 'pcc').'</button>
814 <div id="pcc-mode-notice" style="display:inline-block;vertical-align:middle;margin-left:12px;font-weight:600;"></div>
815 </div>
816 </div>';
817
818 echo '<hr />';
819 echo '<h2>'.esc_html__('Local Plugin Compatibility Summary', 'pcc').'</h2>';
820
821 if ( $license_valid ) {
822 echo '<p style="color:#155724;font-weight:500;">
823 The results you are viewing are fetched directly from the CompatShield Portal because your license is active.
824 These include advanced PHP compatibility checks (PHP 8.1–8.5).
825 </p>';
826 } else {
827 echo '<p>'.esc_html__(
828 'This table uses WPTide data until you validate a CompatShield license. Subscribe to the $1/month entry plan (recurring) to unlock Portal results (PHP 8.1–8.5). Use "Rescan" to request a fresh Portal scan (when licensed) or refresh WPTide (when not). Use "Fetch latest result" to pull Portal output when available.',
829 'pcc'
830 ).'</p>';
831 }
832
833 $this->render_dashboard_table_only();
834
835 $notice_text = $license_valid
836 ? 'Portal mode — license validated. Showing CompatShield results (includes PHP 8.1–8.5).'
837 : 'WPTide mode — showing community results (limited to PHP ≤ 8.0). <a href="'.$signup_link.'" target="_blank" rel="noopener">Subscribe — $1/month </a> to enable CompatShield results (PHP 8.1–8.5).';
838
839 $pcc_settings_nonce = wp_create_nonce('pcc_settings_nonce');
840 ?>
841 <script>
842 (function($){
843 $(document).ready(function(){
844 var noticeHtml = <?php echo json_encode($notice_text); ?>;
845 $('#pcc-mode-notice').html(noticeHtml).css('color','<?php echo $license_valid ? '#155724' : '#b71c1c'; ?>');
846
847 $('#pcc-rescan').on('click', function(){
848 var btn = $(this);
849 btn.prop('disabled',true).text('Requesting scan...');
850 $.post(ajaxurl, {
851 action: '<?php echo self::AJAX_ACTION; ?>',
852 nonce: '<?php echo wp_create_nonce(self::NONCE_ACTION); ?>'
853 }).done(function(resp){
854 if (resp.success) {
855 var data = resp.data || {};
856 if (data.scan_pending) {
857 $('#pcc-mode-notice').html('Portal mode — scan in progress; results will appear when ready.').css('color','#856404');
858 alert('Scan requested on Portal. Results will be available when the Portal completes scanning.');
859 } else {
860 alert('Local scan refreshed (fallback) or scan request failed to queue; check messages.');
861 }
862 setTimeout(function(){ location.reload(); }, 700);
863 } else {
864 alert('Rescan failed: ' + JSON.stringify(resp.data));
865 }
866 }).fail(function(){ alert('Rescan failed (network)'); })
867 .always(function(){ btn.prop('disabled',false).text('<?php echo esc_js(__('Rescan', 'pcc')); ?>'); });
868 });
869
870 <?php if ( $license_valid && empty($remote_map) ) : ?>
871 (function(){
872 $.post(ajaxurl, {
873 action: 'pcc_fetch_remote',
874 nonce: '<?php echo esc_js($pcc_settings_nonce); ?>'
875 }).done(function(resp){
876 if ( resp && resp.success && resp.data && resp.data.updated > 0 ) {
877 setTimeout(function(){ location.reload(); }, 700);
878 }
879 }).fail(function(){ /* ignore silently */ });
880 })();
881 <?php endif; ?>
882 });
883 })(jQuery);
884 </script>
885 <?php
886 }
887
888 public function render_sysinfo() {
889 $cap = is_multisite() ? self::CAP_SINGLE : self::CAP_SINGLE;
890 if ( ! current_user_can($cap) ) {
891 wp_die(esc_html__('You do not have permission to access this page.', 'pcc'));
892 }
893 echo '<h1>'.esc_html__('System Info', 'pcc').'</h1><br>';
894 $php = phpversion();
895 echo '<b style="font-size:18px;">'.sprintf(esc_html__('Your Current PHP Version is: %s', 'pcc'), esc_html($php)).'</b><br>';
896
897 $space_total_gb = (int) ( @disk_total_space(ABSPATH) / 1024 / 1024 / 1024 );
898 $space_free_gb = (int) ( @disk_free_space(ABSPATH) / 1024 / 1024 / 1024 );
899 $space_used_gb = max(0, $space_total_gb - $space_free_gb);
900
901 echo '<table class="sysinfo"><tr><td><b>'.esc_html__('Disk Total Space', 'pcc').'</b></td><td><b>'.intval($space_total_gb).' GB</b></td></tr>';
902 echo '<tr><td><b>'.esc_html__('Disk Space Used', 'pcc').'</b></td><td><b>'.intval($space_used_gb).' GB</b></td></tr>';
903 echo '<tr><td><b>'.esc_html__('Disk Space Free', 'pcc').'</b></td><td><b>'.intval($space_free_gb).' GB</b></td></tr></table>';
904
905 $ini = [
906 'Max Execution Time' => ini_get('max_execution_time'),
907 'Max File Uploads' => ini_get('max_file_uploads'),
908 'Max Input Vars' => ini_get('max_input_vars'),
909 'Post Max Size' => ini_get('post_max_size'),
910 'Memory Limit' => ini_get('memory_limit'),
911 'Upload Max Filesize' => ini_get('upload_max_filesize'),
912 ];
913
914 echo '<table class="sysinfo">';
915 foreach ($ini as $k => $v) {
916 echo '<tr><td><b>'.esc_html($k).'</b></td><td><b>'.esc_html($v).'</b></td></tr>';
917 }
918 echo '</table>';
919
920 $exts = get_loaded_extensions();
921 sort($exts, SORT_NATURAL | SORT_FLAG_CASE);
922 echo '<center><p class="pheading">'.esc_html__('List of Loaded Extensions', 'pcc').'</p><br>';
923 echo '<table class="extntable">';
924 foreach ( $exts as $ext ) {
925 echo '<tr><td class="tdstyle">'.esc_html($ext).'</td></tr>';
926 }
927 echo '</table></center>';
928 }
929
930 public function render_settings() {
931 if ( ! current_user_can(self::CAP_SINGLE) ) {
932 wp_die(esc_html__('You do not have permission to access this page.', 'pcc'));
933 }
934
935 if ( isset($_POST['pcc_save_settings']) && check_admin_referer('pcc_settings_save') ) {
936 $license = isset($_POST['pcc_license_key']) ? sanitize_text_field(trim($_POST['pcc_license_key'])) : '';
937 update_option(self::OPTION_LICENSE_KEY, $license);
938 update_option(self::OPTION_LICENSE_VALID, false);
939 echo '<div class="updated"><p>Settings saved. Please validate the license using the "Validate License" button.</p></div>';
940 }
941
942 $license = esc_attr(get_option(self::OPTION_LICENSE_KEY, ''));
943 $license_valid = (bool) get_option(self::OPTION_LICENSE_VALID, false);
944 $buy_link = esc_url(self::PRODUCT_URL);
945 $signup_link = esc_url(self::SIGNUP_URL);
946 ?>
947 <div class="wrap">
948 <h1><?php esc_html_e('Plugin Compatibility Checker', 'pcc'); ?></h1>
949
950 <form method="post">
951 <?php wp_nonce_field('pcc_settings_save'); ?>
952 <table class="form-table">
953 <tr>
954 <th scope="row"><label for="pcc_license_key">Portal License Key</label></th>
955 <td>
956 <input type="text" id="pcc_license_key" name="pcc_license_key" value="<?php echo $license; ?>" class="regular-text" />
957 <p class="description">
958 Enter your CompatShield license key to enable Portal mode.
959 Without a validated license, the plugin uses WPTide data limited to PHP 8.0.
960 With a validated license (entry plan $1/month or Pro), CompatShield shows newer PHP compatibility (8.1–8.5) right in your dashboard.
961 Don't have a key? <a href="<?php echo $signup_link; ?>" target="_blank" rel="noopener">Subscribe for $1/month</a>.
962 </p>
963 </td>
964 </tr>
965 </table>
966 <p class="submit"><input type="submit" name="pcc_save_settings" id="submit" class="button button-primary" value="Save Settings"></p>
967 </form>
968
969 <p>
970 <button id="pcc-request-scan" class="button button-secondary">Request Scan on Portal</button>
971 &nbsp;
972 <button id="pcc-fetch-latest" class="button">Fetch latest result</button>
973 &nbsp;
974 <button id="pcc-validate-license" class="button">Validate License</button>
975 <span id="pcc-settings-msg" style="margin-left:12px;"></span>
976 <span id="pcc-license-mode" style="margin-left:12px;font-weight:600;"></span>
977 </p>
978
979 <p style="margin-top:8px;">
980 <?php if (! $license_valid && $license) : ?>
981 <em style="color:#856404;">Saved license not validated yet. Click <strong>Validate License</strong> to enable Portal mode.</em>
982 <?php elseif (! $license) : ?>
983 <em style="color:#b71c1c;">No license configured — using WPTide results (PHP ≤ 8.0). <a href="<?php echo $signup_link; ?>" target="_blank" rel="noopener">Subscribe — $1/month</a> or <a href="<?php echo $buy_link; ?>" target="_blank" rel="noopener">upgrade to Pro</a>.</em>
984 <?php else: ?>
985 <em style="color:#155724;">License validated — Portal mode enabled (PHP 8.1–8.5 shown).</em>
986 <?php endif; ?>
987 </p>
988
989 <div style="margin-top:12px;padding:14px;border:1px solid #e5e5e5;border-radius:6px;background:#fafafa;">
990 <strong>No license yet?</strong>
991 <a class="button button-primary" href="<?php echo $signup_link; ?>" target="_blank" rel="noopener">Subscribe — $1/month</a>
992 &nbsp; or &nbsp;
993 <a class="button" href="<?php echo $buy_link; ?>" target="_blank" rel="noopener">View Pro</a>
994 <div style="margin-top:6px;color:#555;">The $1 entry plan unlocks PHP 8.1–8.5 compatibility visibility via the Portal. Pro adds deeper summaries and recommendations.</div>
995 </div>
996
997 <hr />
998
999 <p><em><?php esc_html_e('Use the main "Plugin Compatibility Checker" menu item to view the local compatibility table and dashboard. Settings page only manages portal/license and remote actions.', 'pcc'); ?></em></p>
1000 <p><em><?php esc_html_e('Use the User-Guide if you face any issue after license activation or you can raise a ticket or mail at support@compatshield.com.', 'pcc'); ?></em></p>
1001 <p><a href="https://www.compatshield.com/user-guide/" target="_blank"><strong><?php esc_html_e('Open Full User Guide', 'pcc'); ?></strong></a></p>
1002
1003 <div style="margin-top:15px;margin-bottom:25px;">
1004 <iframe width="100%" height="350" src="https://www.youtube.com/embed/PCxhJmO-Tb4" frameborder="0" allowfullscreen></iframe>
1005 </div>
1006
1007 <p><strong><?php esc_html_e('Quick Steps:', 'pcc'); ?></strong></p>
1008 <ol style="margin-left:20px;">
1009 <li><?php esc_html_e('Login to Portal Dashboard → Add your domain inside License tab', 'pcc'); ?></li>
1010 <li><?php esc_html_e('Copy your License Key from Portal', 'pcc'); ?></li>
1011 <li><?php esc_html_e('Paste License Key inside Plugin settings here', 'pcc'); ?></li>
1012 <li><?php esc_html_e('Click Validate License', 'pcc'); ?></li>
1013 <li><?php esc_html_e('Click Save Settings', 'pcc'); ?></li>
1014 <li><?php esc_html_e('Click Rescan on main scan page to fetch plugins', 'pcc'); ?></li>
1015 </ol>
1016 </div>
1017
1018 <script>
1019 (function($){
1020 $(document).ready(function(){
1021 if (typeof PCCSettings !== 'undefined' && typeof PCCSettings.licenseActive !== 'undefined') {
1022 var active = PCCSettings.licenseActive;
1023 $('#pcc-license-mode').text(active ? 'Portal mode — license validated' : 'Fallback mode — no validated license').css('color', active ? '#155724' : '#b71c1c');
1024 } else {
1025 var srvActive = <?php echo json_encode(($license !== '' && $license_valid)); ?>;
1026 $('#pcc-license-mode').text(srvActive ? 'Portal mode — license validated' : 'Fallback mode — no validated license').css('color', srvActive ? '#155724' : '#b71c1c');
1027 }
1028 });
1029 })(jQuery);
1030 </script>
1031 <?php
1032 }
1033
1034 private function render_dashboard_table_only() {
1035 $is_network = is_multisite();
1036 $scan = $this->get_scan_results(false, $is_network);
1037
1038 echo '<div class="table-responsive table-hover"><table class="table table-bordered" id="pcctable">
1039 <thead class="thead-dark">
1040 <tr>
1041 <th scope="col">'.esc_html__('Plugin Name', 'pcc').'</th>
1042 <th scope="col">'.esc_html__('Current Plugin Version', 'pcc').'</th>
1043 <th scope="col">'.esc_html__('Latest Plugin Version', 'pcc').'</th>
1044 <th scope="col">'.esc_html__('Compatible With WordPress Version', 'pcc').'</th>
1045 <th scope="col">'.esc_html__('PHP Supported (WPTide: ≤8.0 • Portal: 8.1–8.5)', 'pcc').'</th>
1046 <th scope="col">'.esc_html($is_network ? 'Plugin Network Status' : 'Plugin Status').'</th>
1047 <th scope="col">'.esc_html__('Updateable With Latest Version of WordPress', 'pcc').'</th>
1048 <th scope="col">'.esc_html__('Issues Resolved in Last Two Months', 'pcc').'</th>
1049 </tr>
1050 </thead>
1051 <tbody class="tbdy">';
1052
1053 $export_rows = [];
1054 foreach ( $scan['rows'] as $row ) {
1055 $bg = ($row['current_version'] === $row['latest_version'] && $row['latest_version'] !== 'No Data') ? '#135e96' : '#f64855';
1056
1057 echo '<tr style="background-color:'.esc_attr($bg).'">';
1058 echo '<th scope="row">'.esc_html($row['name']).'</th>';
1059 echo '<td>'.esc_html($row['current_version']).'</td>';
1060 echo '<td>'.esc_html($row['latest_version']).'</td>';
1061 echo '<td>'.esc_html($row['tested_wp']).'</td>';
1062 echo '<td>'.esc_html($row['php_supported']).'</td>';
1063 echo '<td>'.esc_html($row['status']).'</td>';
1064 echo '<td>'.esc_html($row['upgradeable']).'</td>';
1065 echo '<td>'.esc_html($row['issues_ratio']).'</td>';
1066 echo '</tr>';
1067
1068 $export_rows[] = [
1069 'Plugin Name' => str_replace(',', ' ', $row['name']),
1070 'Current Plugin Version' => $row['current_version'],
1071 'Latest Plugin Version' => $row['latest_version'],
1072 'Compatible With WordPress Version' => $row['tested_wp'],
1073 'Supported PHP Version' => str_replace(',', ' ', $row['php_supported']),
1074 $is_network ? 'Plugin Network Status' : 'Plugin Status' => $row['status'],
1075 'Updateable With Latest Version of WordPress' => $row['upgradeable'],
1076 'Issues Resolved in Last Two Months' => "'" . str_replace(':','', $row['issues_ratio']),
1077 ];
1078 }
1079
1080 echo '</tbody></table></div>';
1081
1082 echo '<div class="pcc-table-note" style="margin-top:12px;padding:10px;border-left:4px solid #135e96;background:#f7fbff;">';
1083 echo '<strong>' . esc_html__('Note:', 'pcc') . '</strong> ' . esc_html__('Plugins showing No Data are likely custom/premium — check with the plugin author or the plugin version you are using have been removed by the author from wporg.', 'pcc');
1084 echo '</div>';
1085
1086 printf('<script>window.PCCExportData=%s;</script>', wp_json_encode($export_rows));
1087 }
1088
1089 /* Helpers */
1090
1091 private function resolve_plugin_slug($plugin_file, $text_domain, $plugin_uri) {
1092 $dir = dirname($plugin_file);
1093 if ( $dir && $dir !== '.' ) {
1094 return $dir;
1095 }
1096 if ( ! empty($text_domain) ) {
1097 return sanitize_title($text_domain);
1098 }
1099 if ( $plugin_uri ) {
1100 $parts = explode('/', $plugin_uri, 5);
1101 if ( isset($parts[4]) && ! empty($parts[4]) ) {
1102 return rtrim($parts[4], '/');
1103 }
1104 }
1105 return '';
1106 }
1107
1108 private function fetch_wp_latest_version() {
1109 $url = 'https://api.wordpress.org/core/version-check/1.7/';
1110 $res = wp_remote_get($url, [ 'timeout' => 15 ]);
1111 if ( is_wp_error($res) ) {
1112 return get_bloginfo('version');
1113 }
1114 $body = wp_remote_retrieve_body($res);
1115 $obj = json_decode($body);
1116 return isset($obj->offers[0]->version) ? $obj->offers[0]->version : get_bloginfo('version');
1117 }
1118
1119 private function fetch_wporg_plugin_info($slug) {
1120 if ( empty($slug) ) return [];
1121 $url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=' . rawurlencode($slug);
1122 $res = wp_remote_get($url, [ 'timeout' => 20 ]);
1123 if ( is_wp_error($res) ) return [];
1124 $body = wp_remote_retrieve_body($res);
1125 $data = json_decode($body, true);
1126 if ( ! is_array($data) || isset($data['error']) ) return [];
1127 return [
1128 'tested' => $data['tested'] ?? null,
1129 'version' => $data['version'] ?? null,
1130 'support_threads' => $data['support_threads'] ?? null,
1131 'support_resolved' => $data['support_threads_resolved'] ?? null,
1132 'requires_php' => $data['requires_php'] ?? null,
1133 ];
1134 }
1135
1136 private function fetch_wptide_php_compat_list($slug, $latest_version) {
1137 $url = sprintf('https://wptide.org/api/v1/audit/wporg/plugin/%s/%s?reports=all', rawurlencode($slug), rawurlencode($latest_version));
1138 $res = wp_remote_get($url, [ 'timeout' => 20 ]);
1139 if ( is_wp_error($res) ) return null;
1140 $body = wp_remote_retrieve_body($res);
1141 $data = json_decode($body, true);
1142 if ( ! is_array($data) || isset($data['error']) ) return null;
1143
1144 $compat_arr = $data['reports']['phpcs_phpcompatibilitywp']['report']['compatible'] ?? null;
1145 if ( ! is_array($compat_arr) || empty($compat_arr) ) return null;
1146
1147 return implode(', ', $compat_arr);
1148 }
1149
1150 private function get_environment_stats($is_network) {
1151 $wp_version = get_bloginfo('version');
1152 $php_ver = phpversion();
1153 $plugins = get_plugins();
1154 $total = count($plugins);
1155
1156 if ( $is_network ) {
1157 $active_sitewide = (array) get_site_option('active_sitewide_plugins', []);
1158 $active = 0;
1159 foreach ($plugins as $file => $info) {
1160 if ( isset($active_sitewide[$file]) ) $active++;
1161 }
1162 } else {
1163 $active_plugins = (array) get_option('active_plugins', []);
1164 $active = 0;
1165 foreach ($plugins as $file => $info) {
1166 if ( in_array($file, $active_plugins, true) ) $active++;
1167 }
1168 }
1169 $inactive = max(0, $total - $active);
1170
1171 $wp_latest = $this->fetch_wp_latest_version();
1172
1173 // Get full DB version string via SELECT VERSION() for accurate MariaDB detection
1174 $full_db = $this->get_db_version();
1175 $db_parsed = $this->parse_db_version($full_db);
1176
1177 return [
1178 'wp_version' => $wp_version,
1179 'php_version' => $php_ver,
1180 'db_version' => $db_parsed['clean'], // e.g. "8.0.32" or "10.6.14"
1181 'db_type' => $db_parsed['type'], // "MySQL" or "MariaDB"
1182 'db_version_full' => $db_parsed['full'], // raw string e.g. "10.6.14-MariaDB"
1183 'plugins_total' => $total,
1184 'plugins_active' => $active,
1185 'plugins_inactive'=> $inactive,
1186 'wp_latest' => $wp_latest,
1187 ];
1188 }
1189
1190 private function get_scan_results($force_rebuild = false, $is_network = null) {
1191 if ( is_null($is_network) ) $is_network = is_multisite();
1192 $key = self::TRANSIENT_KEY;
1193 $ttl = apply_filters('pcc_cache_ttl', self::TRANSIENT_TTL_DEFAULT);
1194
1195 $data = $is_network ? get_site_transient($key) : get_transient($key);
1196 if ( $force_rebuild || empty($data) || ! is_array($data) ) {
1197 $data = $this->build_scan_results($is_network);
1198 if ( $is_network ) {
1199 set_site_transient($key, $data, $ttl);
1200 } else {
1201 set_transient($key, $data, $ttl);
1202 }
1203 }
1204 return $data;
1205 }
1206
1207 private function build_scan_results($is_network) {
1208 $plugins = get_plugins();
1209 $wp_latest = $this->fetch_wp_latest_version();
1210
1211 if ( $is_network ) {
1212 $active_map = (array) get_site_option('active_sitewide_plugins', []);
1213 } else {
1214 $active_map = array_fill_keys( (array) get_option('active_plugins', []), true );
1215 }
1216
1217 $rows = [];
1218 $remote_map = get_option(self::OPTION_REMOTE_MAP, []);
1219 if ( ! is_array($remote_map) ) $remote_map = [];
1220
1221 foreach ( $plugins as $file => $plug ) {
1222 $candidate = WP_PLUGIN_DIR . '/' . $file;
1223 if ( ! file_exists($candidate) ) continue;
1224
1225 $name = isset($plug['Name']) ? $plug['Name'] : $file;
1226 $current_ver = isset($plug['Version']) ? $plug['Version'] : '';
1227 $plugin_uri = isset($plug['PluginURI']) ? $plug['PluginURI'] : '';
1228 $text_domain = isset($plug['TextDomain']) ? $plug['TextDomain'] : '';
1229 $slug = $this->resolve_plugin_slug($file, $text_domain, $plugin_uri);
1230
1231 $info = $slug ? $this->fetch_wporg_plugin_info($slug) : [];
1232
1233 $tested_wp = $info['tested'] ?? 'No Data';
1234 $latest_ver = $info['version'] ?? 'No Data';
1235 $support_total = $info['support_threads'] ?? 'No Data';
1236 $support_res = $info['support_resolved'] ?? 'No Data';
1237 $requires_php = $info['requires_php'] ?? 'No Data';
1238
1239 $status = ( isset($active_map[$file]) ? 'Activated' : 'Deactivated' );
1240
1241 $upgradeable = 'No Data';
1242 if ( 'No Data' !== $tested_wp ) {
1243 if ( version_compare($tested_wp, $wp_latest, '>=') ) $upgradeable = 'Yes';
1244 else $upgradeable = ( $tested_wp === '6.2.0' ) ? 'Yes' : 'No';
1245 }
1246
1247 $issues_ratio = 'No Data';
1248 if ($support_total === ':0' && $support_res === ':0') {
1249 $issues_ratio = 'There Are No Issues';
1250 } elseif ($support_total !== 'No Data' && $support_res !== 'No Data') {
1251 $issues_ratio = str_replace(':','', $support_res) . '/' . str_replace(':','', $support_total);
1252 }
1253
1254 $php_supported = 'No Data';
1255 if ( $slug && isset($remote_map[$slug]) && ! empty($remote_map[$slug]) ) {
1256 $php_supported = $remote_map[$slug];
1257 } else {
1258 if ( $slug && 'No Data' !== $latest_ver ) {
1259 $compat = $this->fetch_wptide_php_compat_list($slug, $latest_ver);
1260 $php_supported = $compat ? $compat : 'No Data';
1261 }
1262 }
1263
1264 $rows[] = [
1265 'name' => $name,
1266 'current_version' => $current_ver ?: 'No Data',
1267 'latest_version' => $latest_ver,
1268 'tested_wp' => $tested_wp,
1269 'php_supported' => $php_supported,
1270 'status' => $status,
1271 'upgradeable' => $upgradeable,
1272 'issues_ratio' => $issues_ratio,
1273 ];
1274 }
1275
1276 return [ 'rows' => $rows ];
1277 }
1278 }
1279
1280 new PCC();
1281
1282 endif;