Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
Vulnerability
:
VulnerabilityController.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php declare(strict_types=1); namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability; use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\PluginController; use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\ThemeController; use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityDataController; use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityNoticeController; use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityNotificationController; use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\VulnerabilitySyncService; use ReallySimplePlugins\RSS\Core\Interfaces\FeatureInterface; use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig; use ReallySimplePlugins\RSS\Core\Traits\HasScheduler; use ReallySimplePlugins\RSS\Core\Traits\HasViews; /** * Runs vulnerability sync hooks. * * Daily cron does full sync. * Update and activation hooks queue small syncs. */ final class VulnerabilityController implements FeatureInterface { use HasViews; use HasScheduler; /** * Stores queued sync work until cron runs. */ private const PENDING_SYNC_OPTION = 'rsssl_vulnerability_pending_sync'; /** * Cron hook that runs the queued sync. */ private const SYNC_EVENT = 'rsssl_vulnerability_run_scheduled_sync'; /** * Fired after sync is done. */ public const SYNC_COMPLETED_ACTION = 'rsssl_vulnerability_sync_completed'; /** * Wait time before queued sync runs. */ private const SYNC_DEBOUNCE = (5 * MINUTE_IN_SECONDS); private VulnerabilitySyncService $vulnerabilitySyncService; private EnvironmentConfig $env; public function __construct( VulnerabilitySyncService $vulnerabilitySyncService, EnvironmentConfig $environmentConfig ) { $this->vulnerabilitySyncService = $vulnerabilitySyncService; $this->env = $environmentConfig; } /** * Wire all hooks for this feature. */ public function register(): void { add_action('admin_enqueue_scripts', [$this, 'enqueueStyles']); add_action(self::SYNC_EVENT, [$this, 'runSync'], 10, 0); add_filter('rss_core_controller_classes', [$this, 'registerControllers']); add_action('rss_core_activation', [$this, 'scheduleFullSync'], 10, 1); add_action('upgrader_process_complete', [$this, 'scheduleUpgradeSync'], 10, 2); add_action('activate_plugin', [$this, 'schedulePluginActivationSync'], 10, 2); add_action('after_switch_theme', [$this, 'scheduleThemeSwitchSync'], 10, 0); add_action('_core_updated_successfully', [$this, 'scheduleCoreUpdateSync'], 10, 1); add_action('rsssl_daily_cron', [$this, 'scheduleFullSync']); } /** * Registers the controllers related to vulnerability management. This * method is hooked into the 'rss_core_controller_classes' filter, that * is applied here {@see Plugin::registerControllers}, to make sure the * {@see ControllerManager} can register them in the plugin lifecycle. */ public function registerControllers(array $existingControllers): array { $enablePluginAndThemeDisplay = rsssl_get_option('enable_feedback_in_plugin', false); $availableControllers = [ VulnerabilityDataController::class, VulnerabilityNoticeController::class, VulnerabilityNotificationController::class, ]; if ($enablePluginAndThemeDisplay) { $availableControllers[] = PluginController::class; $availableControllers[] = ThemeController::class; } return array_merge($existingControllers, $availableControllers); } /** * Enqueues shared vulnerability styling on plugins and themes overview pages. * we register this here because both the PluginController and ThemeController * needs the same styles. * * @param null|string $hook Current admin page hook suffix. Can be null in * non-default admin pages. For example when using a page builder plugin. */ public function enqueueStyles(?string $hook = null): void { if (empty($hook) || !$this->isComponentOverviewScreen($hook)) { return; } $rtl = is_rtl() ? 'rtl/' : ''; $assetsUrl = trailingslashit($this->env->getString('plugin.assets_url')); $assetsPath = trailingslashit($this->env->getString('plugin.assets_path')); $url = $assetsUrl . "css/{$rtl}rsssl-plugin.min.css"; $path = $assetsPath . "css/{$rtl}rsssl-plugin.min.css"; $version = $this->env->get('plugin.version'); if (file_exists($path)) { wp_enqueue_style('rsssl-plugin', $url, [], $version); } } /** * Determine whether the current admin page is a plugin or theme overview screen. * * Supports both regular admin and multisite network-admin pages. */ private function isComponentOverviewScreen(string $hook): bool { return in_array($hook, ['plugins.php', 'plugins-network.php', 'themes.php', 'themes-network.php'], true); } /** * Queue full sync. Full beats scoped. * * Daily cron and product activation use this. */ public function scheduleFullSync(...$args): void { $pendingSync = $this->getPendingSync(); $pendingSync['full'] = true; $pendingSync['components'] = []; $this->storePendingSync($pendingSync); $this->scheduleDebounced(self::SYNC_EVENT, self::SYNC_DEBOUNCE, []); } /** * Queue one plugin sync from activation. * * Network-wide activation uses the same scoped sync. * Vulnerability data is per component, not per site. */ public function schedulePluginActivationSync(string $pluginFile = '', bool $networkWide = false): void { $slug = $this->getPluginSlugFromPath($pluginFile); if ($slug === '') { return; } $this->queueScopedSync('plugin', $slug); } /** * Queue scoped syncs from upgrader events. * * If the upgrader payload is unclear, skip the event. */ public function scheduleUpgradeSync($upgrader = null, array $hookExtra = []): void { $action = isset($hookExtra['action']) && is_string($hookExtra['action']) ? $hookExtra['action'] : ''; if ($action !== 'update') { return; } $type = isset($hookExtra['type']) && is_string($hookExtra['type']) ? $hookExtra['type'] : ''; if ($type === 'plugin') { $this->schedulePluginUpgradeSync($hookExtra['plugins'] ?? []); return; } if ($type === 'theme') { $this->scheduleThemeUpgradeSync($hookExtra['themes'] ?? []); return; } if ($type === 'core') { $this->scheduleCoreUpgradeSync(); return; } } /** * Queue scoped plugin syncs from an upgrader payload. * * @param mixed $plugins Upgrader payload plugin files. */ private function schedulePluginUpgradeSync($plugins): void { foreach ($this->normalizeComponentSlugsFromUpgradePayload($plugins, 'plugin') as $slug) { $this->queueScopedSync('plugin', $slug); } } /** * Queue scoped theme syncs from an upgrader payload. * * @param mixed $themes Upgrader payload theme slugs. */ private function scheduleThemeUpgradeSync($themes): void { foreach ($this->normalizeComponentSlugsFromUpgradePayload($themes, 'theme') as $slug) { $this->queueScopedSync('theme', $slug); } } /** * Queue scoped core sync from an upgrader payload. */ private function scheduleCoreUpgradeSync(): void { $this->queueScopedSync('core', 'wordpress'); } /** * Queue one theme sync after theme switch. */ public function scheduleThemeSwitchSync(): void { $theme = wp_get_theme(); $slug = sanitize_key((string) $theme->get_stylesheet()); if ($slug === '') { return; } $this->queueScopedSync('theme', $slug); } /** * Queue core sync after core update. */ public function scheduleCoreUpdateSync(...$args): void { $this->queueScopedSync('core', 'wordpress'); } /** * Run queued sync. * * Full sync only runs when full=true. * Components sync only runs when components exist. * Empty queue stops here, no full sync fallback. */ public function runSync(): void { $pendingSync = $this->getPendingSync(); try { if ($pendingSync['full'] ?? false) { $this->vulnerabilitySyncService->syncAllComponents(); } elseif (!empty($pendingSync['components'])) { $this->vulnerabilitySyncService->syncSelectedComponents(array_values($pendingSync['components'])); } else { delete_option(self::PENDING_SYNC_OPTION); return; } delete_option(self::PENDING_SYNC_OPTION); } finally { // Release the debounce lock so future triggers can schedule again. $this->releaseDebounceLock(self::SYNC_EVENT); } // Notify that the sync has completed. do_action(self::SYNC_COMPLETED_ACTION); } /** * Add one component to the queued scoped sync. * * Full sync stays stronger than any queued scoped work. */ private function queueScopedSync(string $type, string $slug): void { $type = strtolower(trim($type)); $slug = strtolower(trim($slug)); if ($type === '' || $slug === '') { return; } $pendingSync = $this->getPendingSync(); if (!($pendingSync['full'] ?? false)) { if (!isset($pendingSync['components']) || !is_array($pendingSync['components'])) { $pendingSync['components'] = []; } $componentKey = $type . ':' . $slug; $pendingSync['components'][$componentKey] = [ 'type' => $type, 'slug' => $slug, ]; } $this->storePendingSync($pendingSync); $this->scheduleDebounced(self::SYNC_EVENT, self::SYNC_DEBOUNCE, []); } /** * Read queued sync state from the option. * * Bad rows are ignored so one broken value does not break the queue. */ private function getPendingSync(): array { $pendingSync = get_option(self::PENDING_SYNC_OPTION, []); if (!is_array($pendingSync)) { return [ 'full' => false, 'components' => [], ]; } if (!isset($pendingSync['components']) || !is_array($pendingSync['components'])) { return [ 'full' => !empty($pendingSync['full']), 'components' => [], ]; } $components = []; foreach ($pendingSync['components'] as $componentKey => $component) { if (!is_array($component)) { continue; } $type = strtolower(trim((string) ($component['type'] ?? ''))); $slug = strtolower(trim((string) ($component['slug'] ?? ''))); if ($type === '' || $slug === '') { continue; } $components[(string) $componentKey] = [ 'type' => $type, 'slug' => $slug, ]; } return [ 'full' => !empty($pendingSync['full']), 'components' => $components, ]; } /** * Save queued sync state. */ private function storePendingSync(array $pendingSync): void { update_option(self::PENDING_SYNC_OPTION, $pendingSync, false); } /** * Turn plugin path into plugin slug. */ private function getPluginSlugFromPath(string $pluginPath): string { $pluginPath = trim(plugin_basename($pluginPath)); if ($pluginPath === '') { return ''; } $directory = dirname($pluginPath); $slug = $directory !== '.' && $directory !== '/' ? $directory : basename($pluginPath, '.php'); return strtolower($slug); } /** * Extract slugs from upgrader payload. * * Plugins use plugin-file to slug conversion. * Themes use the stylesheet slug as-is. * * @param mixed $items Upgrader payload items. */ private function normalizeComponentSlugsFromUpgradePayload($items, string $type): array { if (!is_array($items)) { return []; } $slugs = []; foreach ($items as $item) { if (!is_string($item) || $item === '') { continue; } $slug = sanitize_key($item); if ($type === 'plugin') { $slug = $this->getPluginSlugFromPath($item); } if ($slug !== '' && !in_array($slug, $slugs, true)) { $slugs[] = $slug; } } return $slugs; } }