<?php
declare(strict_types=1);
namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\InstalledComponentRepositoryInterface;
/**
* Read-only repository for installed WordPress themes.
*
* Purpose:
* - Enumerates installed themes via WordPress APIs.
* - Normalizes theme metadata (slug, name, versions, active state).
* - Exposes each theme as an InstalledComponentDto for the vulnerability sync layer.
*
* This repository does not perform remote calls and does not persist anything.
* It only translates WordPress runtime state into a predictable DTO format.
*/
final class ThemeRepository implements InstalledComponentRepositoryInterface
{
/**
* Returns a materialized list of installed themes.
*
* @return list<InstalledComponentDto>
*/
public function getInstalledComponents(): array
{
$components = [];
foreach ($this->getNormalizedThemes() as $normalizedTheme) {
$components[] = new InstalledComponentDto(
'theme',
$normalizedTheme['slug'],
$normalizedTheme['name'],
$normalizedTheme['version'],
'',
$normalizedTheme['isActive'],
$normalizedTheme['latestVersion']
);
}
return $components;
}
/**
* Check whether WordPress currently offers an update for a theme.
*
* We rely on the `update_themes` site transient, which is refreshed by
* `wp_update_themes()`.
*
* @param string $stylesheet Theme stylesheet slug.
*/
public function hasThemeUpdateAvailable(string $stylesheet): bool
{
$updates = get_site_transient('update_themes');
if (! is_object($updates) || ! isset($updates->response) || ! is_array($updates->response)) {
return false;
}
return array_key_exists($stylesheet, $updates->response);
}
/**
* Read the currently installed theme version from WordPress.
*
* Uses `wp_get_theme($stylesheet)`.
*
* @param string $stylesheet Theme stylesheet slug.
*/
public function getThemeVersion(string $stylesheet): string
{
$theme = wp_get_theme($stylesheet);
if (! $theme->exists()) {
return '';
}
$version = $theme->get('Version');
return is_string($version) ? $version : '';
}
/**
* Build a normalized list of installed themes from WordPress theme APIs.
*
* Notes:
* - In WordPress, the stylesheet directory name acts as the theme identifier (slug).
* - A child theme is identified by stylesheet; the parent theme by template.
* - Update information is taken from the update_themes site transient when available.
*
* @return iterable<array{
* slug: string,
* name: string,
* version: string,
* isActive: bool,
* latestVersion: ?string
* }>
*/
private function getNormalizedThemes(): iterable
{
$this->ensureThemesApiLoaded();
/** @var array<string, WP_Theme> $themes */
$themes = wp_get_themes();
$activeTheme = wp_get_theme();
$activeStylesheet = (string) $activeTheme->get_stylesheet();
$activeTemplate = (string) $activeTheme->get_template();
foreach ($themes as $stylesheet => $theme) {
// In WordPress, the stylesheet directory name is the theme slug.
$slug = sanitize_key((string) $stylesheet);
$name = (string) $theme->get('Name');
$version = (string) $theme->get('Version');
// Child theme is identified by stylesheet; parent by template.
$isActive = $this->isThemeActive($stylesheet, $activeStylesheet, $activeTemplate);
$latestVersion = null;
$updates = get_theme_updates();
if (is_array($updates) && isset($updates[$stylesheet]) && is_object($updates[$stylesheet])) {
if (isset($updates[$stylesheet]->update['new_version']) && is_string($updates[$stylesheet]->update['new_version'])) {
$latestVersion = $updates[$stylesheet]->update['new_version'];
}
}
yield [
'slug' => $slug,
'name' => $name,
'version' => $version,
'isActive' => $isActive,
'latestVersion' => $latestVersion,
];
}
}
/**
* Ensure the WordPress theme API functions are available.
*
* Some execution paths may run before wp-admin includes are loaded.
* This makes wp_get_themes() available in a defensive way.
*/
private function ensureThemesApiLoaded(): void
{
// Defensive load for early/edge WP execution paths.
if (!function_exists('wp_get_themes')) {
require_once ABSPATH . 'wp-admin/includes/theme.php';
}
if (!function_exists('get_theme_updates')) {
require_once ABSPATH . 'wp-admin/includes/update.php';
}
}
/**
* Determine whether a theme is currently active.
*
* WordPress identifies the active child theme by stylesheet and the
* parent theme by template, so we check both.
*/
private function isThemeActive(string $stylesheet, string $activeStylesheet, string $activeTemplate): bool
{
return ($stylesheet === $activeStylesheet || $stylesheet === $activeTemplate);
}
}