$screenIds */ public static function isNoticeScreen(array $screenIds): bool { if (!function_exists('get_current_screen')) { return false; } $screen = get_current_screen(); if (!$screen || !isset($screen->id)) { return false; } $id = (string) $screen->id; if (self::$hookSuffix !== '' && $id === self::$hookSuffix) { return true; } // Network admin screens carry a "-network" suffix (plugins-network). $baseId = substr($id, -8) === '-network' ? substr($id, 0, -8) : $id; return in_array($baseId, $screenIds, true); } public static function hook(): void { add_action('admin_menu', [self::class, 'registerMenu']); add_action('admin_enqueue_scripts', [self::class, 'maybeEnqueueAssets']); } public static function registerMenu(): void { if (!function_exists('add_submenu_page')) { return; } $hook = add_submenu_page( 'options-general.php', __('FluentCart Health', 'sparkcore-commerce'), __('FluentCart Health', 'sparkcore-commerce'), 'manage_options', self::PAGE_SLUG, [self::class, 'render'] ); if (is_string($hook)) { self::$hookSuffix = $hook; } } public static function maybeEnqueueAssets(string $hookSuffix): void { if ($hookSuffix === '' || $hookSuffix !== self::$hookSuffix) { return; } if (!function_exists('wp_enqueue_style')) { return; } wp_enqueue_style( 'fcbd-admin-health', defined('FCBD_URL') ? FCBD_URL . 'assets/admin-health.css' : '', [], defined('FCBD_VERSION') ? FCBD_VERSION : null ); } public static function render(): void { if (!function_exists('current_user_can') || !current_user_can('manage_options')) { if (function_exists('wp_die')) { wp_die(esc_html__('You do not have permission to view this page.', 'sparkcore-commerce')); } return; } $contractResults = Contracts::verify(); $versionProfile = VersionProfiles::evaluate(); $environment = self::gatherEnvironment(); $cacheAdvice = self::detectCachePlugins(); $reportText = self::buildReportText($contractResults, $versionProfile, $environment, $cacheAdvice); echo '
'; echo '

' . esc_html__('FluentCart Health', 'sparkcore-commerce') . '

'; self::renderVersionProfileSection($versionProfile); self::renderContractSection($contractResults); self::renderEnvironmentSection($environment); self::renderCacheAdviceSection($cacheAdvice); self::renderReportSection($reportText); echo '
'; } // ===================================================================== // (a) contract-status + version-profile section // ===================================================================== /** * @param array{version: string|null, status: string, message: string} $versionProfile */ private static function renderVersionProfileSection(array $versionProfile): void { echo '

' . esc_html__('Version profile', 'sparkcore-commerce') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
' . esc_html__('Audited FluentCart profiles', 'sparkcore-commerce') . '' . esc_html(self::auditedFluentCartProfilesLabel()) . '
' . esc_html__('Verified Breakdance baseline', 'sparkcore-commerce') . '' . esc_html(Contracts::VERIFIED_BREAKDANCE_VERSION) . '
' . esc_html__('Active FluentCart version', 'sparkcore-commerce') . '' . esc_html($versionProfile['version'] ?? __('unknown', 'sparkcore-commerce')) . '
' . esc_html__('Verdict', 'sparkcore-commerce') . '' . esc_html(self::profileStatusLabel($versionProfile['status'])) . '

' . esc_html($versionProfile['message']) . '

'; } private static function profileBadgeClass(string $status): string { switch ($status) { case VersionProfiles::STATUS_KNOWN_GOOD: return 'ok'; case VersionProfiles::STATUS_UNKNOWN_NEWER: case VersionProfiles::STATUS_OLDER_UNSUPPORTED: return 'warn'; default: return 'unknown'; } } private static function profileStatusLabel(string $status): string { switch ($status) { case VersionProfiles::STATUS_KNOWN_GOOD: return __('Known good', 'sparkcore-commerce'); case VersionProfiles::STATUS_UNKNOWN_NEWER: return __('Unknown, newer', 'sparkcore-commerce'); case VersionProfiles::STATUS_OLDER_UNSUPPORTED: return __('Older than supported', 'sparkcore-commerce'); default: return __('Not detected', 'sparkcore-commerce'); } } /** * @param array $contractResults */ private static function renderContractSection(array $contractResults): void { $summary = self::summarizeContracts($contractResults); echo '

' . esc_html__('Contract status', 'sparkcore-commerce') . '

'; echo '

'; echo esc_html(sprintf( /* translators: 1: ok count, 2: warn count, 3: uncheckable count, 4: missing count. */ __('%1$d ok, %2$d warning(s), %3$d not checkable, %4$d missing.', 'sparkcore-commerce'), $summary['ok'], $summary['warn'], $summary['uncheckable'], $summary['missing'] )); echo '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($contractResults as $entry) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Interface', 'sparkcore-commerce') . '' . esc_html__('Type', 'sparkcore-commerce') . '' . esc_html__('Status', 'sparkcore-commerce') . '' . esc_html__('Detail', 'sparkcore-commerce') . '
' . esc_html((string) $entry['id']) . '' . esc_html((string) $entry['type']) . '' . esc_html(self::contractStatusLabel((string) $entry['status'])) . '' . esc_html((string) $entry['detail']) . '
'; } private static function contractBadgeClass(string $status): string { switch ($status) { case 'ok': return 'ok'; case 'warn': return 'warn'; case 'missing': return 'missing'; default: return 'unknown'; } } private static function contractStatusLabel(string $status): string { switch ($status) { case 'ok': return __('Ok', 'sparkcore-commerce'); case 'warn': return __('Warning', 'sparkcore-commerce'); case 'missing': return __('Missing', 'sparkcore-commerce'); case 'uncheckable': return __('Not checkable (E2E)', 'sparkcore-commerce'); default: return __('Unknown', 'sparkcore-commerce'); } } /** * @param array $contractResults * @return array{ok: int, warn: int, missing: int, uncheckable: int} */ public static function summarizeContracts(array $contractResults): array { $summary = ['ok' => 0, 'warn' => 0, 'missing' => 0, 'uncheckable' => 0]; foreach ($contractResults as $entry) { $status = (string) ($entry['status'] ?? ''); if (array_key_exists($status, $summary)) { $summary[$status]++; } } return $summary; } // ===================================================================== // (b) environment status section // ===================================================================== /** * @return array{ * wp_version: string, * php_version: string, * fluentcart_version: string, * fluentcart_pro_version: string, * breakdance_version: string, * takeover_enabled: bool, * atomized_checkout_enabled: bool, * preview_product_id: int * } */ public static function gatherEnvironment(): array { $unknown = __('not active', 'sparkcore-commerce'); return [ 'wp_version' => function_exists('get_bloginfo') ? (string) get_bloginfo('version') : $unknown, 'php_version' => PHP_VERSION, 'fluentcart_version' => defined('FLUENTCART_VERSION') ? (string) FLUENTCART_VERSION : $unknown, 'fluentcart_pro_version' => defined('FLUENTCART_PRO_PLUGIN_VERSION') ? (string) FLUENTCART_PRO_PLUGIN_VERSION : $unknown, 'breakdance_version' => defined('__BREAKDANCE_VERSION') ? (string) __BREAKDANCE_VERSION : $unknown, 'takeover_enabled' => function_exists('get_option') && (bool) get_option(TemplateRegistry::TAKEOVER_OPTION, false), 'atomized_checkout_enabled' => function_exists('get_option') && (bool) get_option(self::ATOMIZED_CHECKOUT_OPTION, false), 'preview_product_id' => class_exists(EditorSupport::class) ? EditorSupport::previewProductId() : 0, ]; } /** * @param array{ * wp_version: string, * php_version: string, * fluentcart_version: string, * fluentcart_pro_version: string, * breakdance_version: string, * takeover_enabled: bool, * atomized_checkout_enabled: bool, * preview_product_id: int * } $environment */ private static function renderEnvironmentSection(array $environment): void { echo '

' . esc_html__('Environment status', 'sparkcore-commerce') . '

'; echo ''; echo ''; $rows = [ __('WordPress', 'sparkcore-commerce') => $environment['wp_version'], __('PHP', 'sparkcore-commerce') => $environment['php_version'], __('FluentCart', 'sparkcore-commerce') => $environment['fluentcart_version'], __('FluentCart Pro', 'sparkcore-commerce') => $environment['fluentcart_pro_version'], __('Breakdance', 'sparkcore-commerce') => $environment['breakdance_version'], ]; foreach ($rows as $label => $value) { echo ''; } echo ''; echo ''; echo ''; echo '
' . esc_html($label) . '' . esc_html($value) . '
' . esc_html__('Template takeover (fcbd_takeover)', 'sparkcore-commerce') . '' . esc_html(self::flagLabel($environment['takeover_enabled'])) . '
' . esc_html__('Atomized checkout (fcbd_atomized_checkout)', 'sparkcore-commerce') . '' . esc_html(self::flagLabel($environment['atomized_checkout_enabled'])) . ' ' . esc_html__('The checkout family is built (WP9); this flag is deliberately off until the real purchase gate (wave 3).', 'sparkcore-commerce') . '' . '
' . esc_html__('Editor preview product', 'sparkcore-commerce') . '' . ($environment['preview_product_id'] > 0 ? esc_html(sprintf( /* translators: %d: WordPress post ID. */ __('Set to product ID %d.', 'sparkcore-commerce'), $environment['preview_product_id'] )) : esc_html__('Not set.', 'sparkcore-commerce')) . '
'; } private static function flagLabel(bool $enabled): string { return $enabled ? __('on', 'sparkcore-commerce') : __('off', 'sparkcore-commerce'); } // ===================================================================== // (c) cache advice section // ===================================================================== /** * Detects known full-page-cache plugins via class/constant checks (never * by naming assumption alone; every constant below is verified against * the real plugin source, see this fork's report for file:line evidence) * and pairs each with the exact exclusion advice already documented in * readme.txt's cache FAQ. * * @return array */ public static function detectCachePlugins(): array { return [ [ 'id' => 'flyingpress', 'name' => 'FlyingPress', 'active' => defined('FLYING_PRESS_VERSION'), 'evidence' => 'FLYING_PRESS_VERSION', 'advice' => __('Settings > Cache > Excluded URLs: add the cart, checkout, account and receipt page paths (or the FluentCart page setting slugs if you changed them).', 'sparkcore-commerce'), ], [ 'id' => 'litespeed', 'name' => 'LiteSpeed Cache', 'active' => defined('LSCWP_V'), 'evidence' => 'LSCWP_V', 'advice' => __('Cache > Excludes > Do Not Cache URIs: add the same page paths.', 'sparkcore-commerce'), ], [ 'id' => 'wp_rocket', 'name' => 'WP Rocket', 'active' => defined('WP_ROCKET_VERSION'), 'evidence' => 'WP_ROCKET_VERSION', 'advice' => __('Cache > Never Cache URL(s): add the same page paths.', 'sparkcore-commerce'), ], ]; } /** * @param array $cacheAdvice */ private static function renderCacheAdviceSection(array $cacheAdvice): void { echo '

' . esc_html__('Cache advice', 'sparkcore-commerce') . '

'; echo '

' . esc_html__('This plugin already marks the cart, checkout, account and receipt routes as no-cache. As a second line of defense, also add the same paths to your cache plugin\'s exclusion list:', 'sparkcore-commerce') . '

'; $anyActive = false; echo '
    '; foreach ($cacheAdvice as $entry) { if (!$entry['active']) { continue; } $anyActive = true; echo '
  • ' . esc_html($entry['name']) . ': ' . esc_html($entry['advice']) . '
  • '; } echo '
'; if (!$anyActive) { echo '

' . esc_html__('No supported full-page cache plugin detected (FlyingPress, LiteSpeed Cache, WP Rocket).', 'sparkcore-commerce') . '

'; } } // ===================================================================== // (d) copyable diagnostic report // ===================================================================== private static function renderReportSection(string $reportText): void { echo '

' . esc_html__('Copyable diagnostics report', 'sparkcore-commerce') . '

'; echo '

' . esc_html__('Contains no email addresses, order data or keys. Safe to share with a support request.', 'sparkcore-commerce') . '

'; echo ''; } /** * Pure text-assembly, deliberately free of any WordPress data lookup * beyond the arguments it is given, so it can be exercised directly by * tests/admin/run-admin.php with a stubbed contract/version/environment * fixture. Never includes an email address, order record, or secret: only * plugin/theme version strings, boolean feature flags, and Contracts:: * verify()'s own capability-level detail strings. * * @param array $contractResults * @param array{version: string|null, status: string, message: string} $versionProfile * @param array{ * wp_version: string, * php_version: string, * fluentcart_version: string, * fluentcart_pro_version: string, * breakdance_version: string, * takeover_enabled: bool, * atomized_checkout_enabled: bool, * preview_product_id: int * } $environment * @param array $cacheAdvice */ public static function buildReportText( array $contractResults, array $versionProfile, array $environment, array $cacheAdvice ): string { $summary = self::summarizeContracts($contractResults); $lines = []; $lines[] = __('SparkCore Commerce diagnostics report', 'sparkcore-commerce'); $lines[] = str_repeat('=', 48); $lines[] = ''; $lines[] = __('Environment', 'sparkcore-commerce'); $lines[] = sprintf('WordPress: %s', $environment['wp_version']); $lines[] = sprintf('PHP: %s', $environment['php_version']); $lines[] = sprintf('FluentCart: %s', $environment['fluentcart_version']); $lines[] = sprintf('FluentCart Pro: %s', $environment['fluentcart_pro_version']); $lines[] = sprintf('Breakdance: %s', $environment['breakdance_version']); $lines[] = sprintf('fcbd_takeover: %s', self::flagLabel($environment['takeover_enabled'])); $lines[] = sprintf('fcbd_atomized_checkout: %s', self::flagLabel($environment['atomized_checkout_enabled'])); $lines[] = ''; $lines[] = __('Verified baselines', 'sparkcore-commerce'); $lines[] = sprintf('FluentCart: %s', self::auditedFluentCartProfilesLabel()); $lines[] = sprintf('Breakdance: %s', Contracts::VERIFIED_BREAKDANCE_VERSION); $lines[] = ''; $lines[] = __('Version profile', 'sparkcore-commerce'); $lines[] = sprintf('Status: %s', self::profileStatusLabel($versionProfile['status'])); $lines[] = (string) $versionProfile['message']; $lines[] = ''; $lines[] = __('Contract status', 'sparkcore-commerce'); $lines[] = sprintf( '%d ok, %d warn, %d uncheckable, %d missing', $summary['ok'], $summary['warn'], $summary['uncheckable'], $summary['missing'] ); foreach ($contractResults as $entry) { $lines[] = sprintf( '- [%s] %s (%s): %s', strtoupper((string) $entry['status']), (string) $entry['id'], (string) $entry['type'], (string) $entry['detail'] ); } $lines[] = ''; $lines[] = __('Cache advice', 'sparkcore-commerce'); $activeCache = false; foreach ($cacheAdvice as $entry) { if (!$entry['active']) { continue; } $activeCache = true; $lines[] = sprintf('- %s: %s', $entry['name'], $entry['advice']); } if (!$activeCache) { $lines[] = __('No supported full-page cache plugin detected.', 'sparkcore-commerce'); } return implode("\n", $lines); } /** * Every audited (known-good) FluentCart profile, oldest first, so the * Health page shows the full audited range instead of only the oldest * baseline constant (which deliberately stays 1.3.27 as the minimum). */ private static function auditedFluentCartProfilesLabel(): string { $known = []; foreach (VersionProfiles::profiles() as $version => $profile) { if (($profile['status'] ?? '') === VersionProfiles::STATUS_KNOWN_GOOD) { $known[] = (string) $version; } } if ($known === []) { return Contracts::VERIFIED_VERSION; } usort($known, 'version_compare'); return implode(', ', $known); } }