'.esc_html($text); if (! $renew) echo ' '.esc_html(I18n::t($kind === 'affected' || $kind === 'stale' ? 'notice_action' : 'settings')).''; echo '

'; if ($renew) Admin::form('automatic', 'renew_connection'); echo ''; } public static function expiry(): void { if (! Client::connected()) return; $expires = strtotime(Store::get('connection')['expires_at']); if ($expires > time() && $expires <= time() + 14 * DAY_IN_SECONDS) self::box('expiry', Store::get('connection')['site_id'].'|'.$expires, I18n::t('expires_soon', I18n::date($expires)), 'notice-warning', true); } private static function current(array $snapshot): array { $key = $snapshot['generation'].'|'.(string) Store::get('inventory_dirty', ''); $cached = get_site_transient('flx_security_notice_inventory'); if (is_array($cached) && ($cached['key'] ?? '') === $key) return $cached; global $wpdb; $items = Inventory::collect(); $versions = array_column($items, 'version', 'id'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Bounded plugin-owned snapshot read, cached for five minutes and invalidated on installation changes. $rows = $wpdb->get_results($wpdb->prepare('SELECT item_key,version FROM %i WHERE generation=%s AND status=%s AND item_key NOT IN (%s,%s)', Store::table('plugins'), $snapshot['generation'], 'affected', Inventory::selfKey(), Inventory::legacyKey())); if ($wpdb->last_error !== '') throw new \RuntimeException('storage_unavailable'); $count = 0; foreach ($rows as $row) if (($versions[$row->item_key] ?? null) === $row->version) $count++; $result = ['key' => $key, 'count' => $count, 'changed' => ! hash_equals($snapshot['fingerprint'] ?? '', Inventory::fingerprint($items))]; set_site_transient('flx_security_notice_inventory', $result, 300); return $result; } public static function render(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Page selection only limits a read-only notice. if (! Admin::allowed() || (int) Store::get('schema', 0) !== 2 || (isset($_GET['page']) && $_GET['page'] === 'fluxcdn-security')) return; if (ConnectionChanges::pending()) return; if (($error = Client::connectionError()) !== '') { echo ''; return; } if (! Client::connected()) { if (in_array(get_current_screen()?->base, ['dashboard', 'dashboard-network'], true)) { $pending = Store::get('disconnects'); $basis = $pending ? wp_json_encode(array_map(static fn ($row) => $row['created'] ?? 0, $pending)) : wp_json_encode(Store::get('installation')); self::box($pending ? 'disconnect' : 'connect', $basis, I18n::t($pending ? 'disconnect_remote_failed' : 'notice_connect'), 'notice-info'); } return; } self::expiry(); $snapshot = Store::get('snapshot'); if (empty($snapshot['generation'])) return; try { $current = self::current($snapshot); } catch (\Throwable) { return; } $stale = $current['changed'] || Store::get('inventory_dirty', false) || (Plugin::settings()['automatic'] && ($snapshot['checked_at'] ?? 0) < time() - Plugin::settings()['hours'] * HOUR_IN_SECONDS * 2); if ($current['count'] > 0) self::box('affected', $current['key'].'|'.$current['count'].'|'.(int) $stale, I18n::affectedNotice($current['count']).($stale ? ' '.I18n::t('stale') : ''), 'notice-error'); elseif ($stale) self::box('stale', $current['key'], I18n::t('stale'), 'notice-warning'); elseif (Store::get('error', '')) self::box('failed', $current['key'].'|'.Store::get('error', ''), I18n::t('notice_failed'), 'notice-warning'); } }