• File: ThemeRepository.php
  • Full Path: /home/docteuh/www/Vulnerability/Repositories/ThemeRepository.php
  • Date Modified: 06/18/2026 4:25 PM
  • File size: 5.53 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?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);
    }
}