= 2 ? Store::verifiedIdentities() : []; $needsRefresh = false; $items = []; foreach (['plugin' => $plugins, 'mu' => $mu] as $kind => $installed) { foreach ($installed as $basename => $metadata) { if (! is_string($basename) || ! is_array($metadata)) { throw new \RuntimeException('inventory_unavailable'); } if ($kind === 'plugin' && in_array($basename, [plugin_basename(FLX_SECURITY_FILE), 'wp-fluxcdn-security/wp-fluxcdn-security.php'], true)) { continue; } $version = trim((string) ($metadata['Version'] ?? '')); $key = hash('sha256', $kind.':'.$basename); $identity = self::identity($basename, $metadata, $updates); $hash = hash('sha256', wp_json_encode([$basename, $metadata['PluginURI'] ?? '', $metadata['UpdateURI'] ?? '', $metadata['TextDomain'] ?? ''])); $hasUpdate = isset($responses[$basename]) || isset($noUpdate[$basename]); if ($identity === null && ! $hasUpdate && ($verified[$key]['hash'] ?? null) === $hash) $identity = $verified[$key]['slug']; if ($identity !== null && (int) Store::get('schema', 0) >= 2 && (($verified[$key]['hash'] ?? null) !== $hash || ($verified[$key]['slug'] ?? null) !== $identity)) { Store::write('identities', ['item_key' => $key, 'metadata_hash' => $hash, 'slug' => $identity, 'checked_at' => gmdate('Y-m-d H:i:s')]); } $needsRefresh = $needsRefresh || ($identity === null && trim((string) ($metadata['UpdateURI'] ?? '')) === ''); $displayName = wp_html_excerpt(sanitize_text_field($metadata['Name'] ?? $basename), 200, ''); $items[] = ['id' => $key, 'name' => $identity, 'display_name' => $displayName !== '' ? $displayName : wp_html_excerpt($basename, 200, ''), 'version' => strlen($version) <= 80 ? $version : '', 'kind' => $kind, 'active' => $kind === 'mu' || (is_multisite() ? is_plugin_active_for_network($basename) : is_plugin_active($basename))]; } } usort($items, fn ($a, $b) => strcmp($a['id'], $b['id'])); if (count($items) > 2000) { throw new \RuntimeException('inventory_too_large'); } if ($refresh && $needsRefresh && ! get_site_transient('flx_security_identity_retry')) { set_site_transient('flx_security_identity_retry', 1, 15 * MINUTE_IN_SECONDS); $bounded = static function (array $args, string $url): array { if (wp_parse_url($url, PHP_URL_HOST) === 'api.wordpress.org') { $args['timeout'] = 5; $args['redirection'] = 0; $args['sslverify'] = true; } return $args; }; $requireTls = static function ($preempt, array $args, string $url) { return wp_parse_url($url, PHP_URL_HOST) === 'api.wordpress.org' && wp_parse_url($url, PHP_URL_SCHEME) !== 'https' ? new \WP_Error('https_required') : $preempt; }; add_filter('http_request_args', $bounded, 10, 2); add_filter('pre_http_request', $requireTls, PHP_INT_MAX, 3); try { wp_update_plugins(); } catch (\Throwable) { // An unavailable directory must not erase previously verified identities. } finally { remove_filter('http_request_args', $bounded, 10); remove_filter('pre_http_request', $requireTls, PHP_INT_MAX); } return self::collect(); } return $items; } public static function fingerprint(array $items): string { return hash('sha256', wp_json_encode($items, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } public static function selfKey(): string { return hash('sha256', 'plugin:'.plugin_basename(FLX_SECURITY_FILE)); } public static function legacyKey(): string { return hash('sha256', 'plugin:wp-fluxcdn-security/wp-fluxcdn-security.php'); } private static function slug(mixed $value): ?string { if (! is_string($value)) { return null; } $parts = wp_parse_url(trim($value)); if (! is_array($parts) || ! in_array(strtolower($parts['host'] ?? ''), ['wordpress.org', 'www.wordpress.org'], true) || ! in_array($parts['scheme'] ?? '', ['https', 'http'], true) || isset($parts['user']) || isset($parts['pass']) || isset($parts['port']) || ! preg_match('~^/(?:extend/)?plugins/([a-z0-9][a-z0-9_-]{0,199})/?$~D', $parts['path'] ?? '', $match)) { return null; } return $match[1]; } private static function identity(string $basename, array $metadata, array $updates): ?string { $uri = self::slug($metadata['PluginURI'] ?? ''); $updateUri = trim((string) ($metadata['UpdateURI'] ?? '')); $updateSlug = self::slug($updateUri); if (($updateUri !== '' && $updateSlug === null) || ($uri && $updateSlug && $uri !== $updateSlug)) { return null; } $declared = $updateSlug ?? $uri; $cached = null; foreach (['response', 'no_update'] as $section) { $rows = (array) ($updates[$section] ?? []); if (! isset($rows[$basename])) { continue; } $row = (array) $rows[$basename]; $slug = $row['slug'] ?? ''; $id = $row['id'] ?? ''; $official = self::slug($id) ?? (is_string($id) && preg_match('~^w\.org/plugins/([a-z0-9][a-z0-9_-]{0,199})/?$~D', $id, $m) ? $m[1] : null); if (($row['plugin'] ?? '') !== $basename || ! is_string($slug) || $official !== $slug || ($cached !== null && $cached !== $slug)) { return null; } $cached = $slug; } if ($cached !== null && ($declared === null || $cached === $declared)) { return $cached; } if ($cached !== null) { return null; } $folder = str_contains($basename, '/') ? explode('/', $basename, 2)[0] : basename($basename, '.php'); return $declared !== null && $declared === $folder && (($metadata['TextDomain'] ?? '') === '' || $metadata['TextDomain'] === $declared) ? $declared : null; } }