$hookExtra */ public static function onUpgradeComplete($upgrader, array $hookExtra): void { if (!self::isRelevantUpdate($hookExtra)) { return; } // Defensive: Contracts/VersionProfiles are standalone-loadable files that // the bootstrap requires, but this hook fires deep inside wp-admin's // upgrader flow. If a future bootstrap change ever drops them again, a // missing class here would fatal the UPGRADE SCREEN (seen live on // 2026-07-15), which is the worst possible place to fail. Require them // explicitly before use; require_once is a no-op when already loaded. require_once dirname(__DIR__) . '/Compatibility/Contracts.php'; require_once dirname(__DIR__) . '/Compatibility/VersionProfiles.php'; $results = Contracts::verify(); $profile = VersionProfiles::evaluate(); $summary = HealthPage::summarizeContracts($results); if (function_exists('update_option')) { update_option(self::LAST_RUN_OPTION, [ 'checked_at' => function_exists('time') ? time() : 0, 'trigger' => self::triggerLabel($hookExtra), 'fluentcart_version' => $profile['version'], 'profile_status' => $profile['status'], 'summary' => $summary, 'has_issue' => self::hasIssue($summary), ], false); } } /** * Whether an upgrader_process_complete `$hookExtra` payload represents a * plugin update (single or bulk) touching one of the watched plugins. * Pure/testable: no WordPress call, only array inspection. * * @param array $hookExtra */ public static function isRelevantUpdate(array $hookExtra): bool { if (($hookExtra['action'] ?? '') !== 'update' || ($hookExtra['type'] ?? '') !== 'plugin') { return false; } $plugins = []; if (!empty($hookExtra['plugins']) && is_array($hookExtra['plugins'])) { $plugins = $hookExtra['plugins']; } elseif (!empty($hookExtra['plugin']) && is_string($hookExtra['plugin'])) { $plugins = [$hookExtra['plugin']]; } foreach ($plugins as $plugin) { if (is_string($plugin) && in_array($plugin, self::WATCHED_PLUGIN_BASENAMES, true)) { return true; } } return false; } /** * @param array $hookExtra */ public static function triggerLabel(array $hookExtra): string { $plugins = []; if (!empty($hookExtra['plugins']) && is_array($hookExtra['plugins'])) { $plugins = $hookExtra['plugins']; } elseif (!empty($hookExtra['plugin']) && is_string($hookExtra['plugin'])) { $plugins = [$hookExtra['plugin']]; } $watched = array_values(array_filter($plugins, static function ($plugin): bool { return is_string($plugin) && in_array($plugin, self::WATCHED_PLUGIN_BASENAMES, true); })); return implode(', ', $watched); } /** * @param array{ok: int, warn: int, missing: int, uncheckable: int} $summary */ public static function hasIssue(array $summary): bool { return ($summary['missing'] ?? 0) > 0 || ($summary['warn'] ?? 0) > 0; } public static function maybeRenderNotice(): void { if (!function_exists('current_user_can') || !current_user_can('manage_options')) { return; } // Limited in scope: only where an administrator manages updates (the // Dashboard, Plugins and Updates screens) and on the Health page. if (!HealthPage::isNoticeScreen(self::NOTICE_SCREENS)) { return; } $last = function_exists('get_option') ? get_option(self::LAST_RUN_OPTION, null) : null; if (!is_array($last) || empty($last['has_issue'])) { return; } $checkedAt = (int) ($last['checked_at'] ?? 0); $dismissedAt = function_exists('get_option') ? (int) get_option(self::NOTICE_DISMISSED_OPTION, 0) : 0; if ($dismissedAt === $checkedAt) { return; } $summary = is_array($last['summary'] ?? null) ? $last['summary'] : ['ok' => 0, 'warn' => 0, 'missing' => 0, 'uncheckable' => 0]; $healthUrl = function_exists('admin_url') ? admin_url('options-general.php?page=' . HealthPage::PAGE_SLUG) : ''; $dismissUrl = self::dismissUrl(); echo '
'; echo '

' . esc_html(sprintf( /* translators: 1: trigger plugin label, 2: warn count, 3: missing count. */ __('SparkCore Commerce sentinel: after the update of %1$s, %2$d warning(s) and %3$d missing interface(s) were found.', 'sparkcore-commerce'), (string) ($last['trigger'] ?? ''), (int) $summary['warn'], (int) $summary['missing'] )) . ' '; if ($healthUrl !== '') { echo '' . esc_html__('View the health report', 'sparkcore-commerce') . ''; } echo '

'; if ($dismissUrl !== '') { echo '

' . esc_html__('Dismiss this notice', 'sparkcore-commerce') . '

'; } echo '
'; } private static function dismissUrl(): string { if (!function_exists('admin_url') || !function_exists('wp_nonce_url') || !function_exists('add_query_arg')) { return ''; } $base = admin_url('admin.php'); $url = add_query_arg(self::DISMISS_QUERY_VAR, '1', $base); return wp_nonce_url($url, self::DISMISS_NONCE_ACTION); } public static function maybeHandleDismiss(): void { if (!function_exists('current_user_can') || !current_user_can('manage_options')) { return; } if (empty($_GET[self::DISMISS_QUERY_VAR])) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This IS the nonce value; it is verified with wp_verify_nonce() on the next lines. $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash((string) $_GET['_wpnonce'])) : ''; if (!function_exists('wp_verify_nonce') || !wp_verify_nonce($nonce, self::DISMISS_NONCE_ACTION)) { return; } $last = function_exists('get_option') ? get_option(self::LAST_RUN_OPTION, null) : null; $checkedAt = is_array($last) ? (int) ($last['checked_at'] ?? 0) : 0; if (function_exists('update_option')) { update_option(self::NOTICE_DISMISSED_OPTION, $checkedAt, false); } if (function_exists('wp_safe_redirect') && function_exists('remove_query_arg')) { wp_safe_redirect(remove_query_arg([self::DISMISS_QUERY_VAR, '_wpnonce'])); exit; } } }