refactor: Divi-Reiter und divi-check in die Divi-Integration verschoben; CoreIndependenceTest prüft auch Cli, ChangeLog, Pods und die Wurzeldateien, erkennt Alias-Importe und kennt nur noch Plugin.php als Ausnahme

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 22:55:02 +02:00
co-authored by Claude Opus 5.5
parent d0d3488df8
commit 7f9a087b8b
10 changed files with 84 additions and 28 deletions
-489
View File
@@ -1,489 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Admin;
use Closure;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
use LIT\MCP\Plugin;
defined('ABSPATH') || exit;
/**
* Tab "Divi" (spec §6): release status of the installed Divi, the table of its features (state, source, problems,
* affected tools – E3), the latest check report, "Prüfung starten" (REST, DiviCheckRoute), "Divi <Version>
* freigeben" (only after a fresh green report) and "Freigabe zurückziehen" (whenever a database release applies,
* whatever the overall state). Loading the tab removes check pages a dead check left behind – in the page's load
* hook (removeOrphans()), not while rendering. renderNotice() is the admin notice on every admin page while a
* feature is unusable.
*/
final class DiviTab implements AdminTab
{
public const SLUG = 'divi';
public const RELEASE_ACTION = 'lit_mcp_divi_release';
public const WITHDRAW_ACTION = 'lit_mcp_divi_withdraw';
/** @var list<int> Check pages removeOrphans() deleted in this request; render() reports them. */
private array $removed = [];
/**
* @param (Closure(): string)|null $version Installed Divi version ('' = none); null = DiviEnvironment.
* @param (Closure(string): string)|null $stateKey null = ReleaseResolver::stateKey().
* @param FeatureOverview|null $overview null = a table without the affected tools.
*/
public function __construct(
private ?CheckReportStore $reports = null,
private ?ReleaseStore $releases = null,
private ?ChangeLog $log = null,
private ?AdminPostAction $action = null,
private ?Closure $version = null,
private ?Closure $stateKey = null,
private ?FeatureOverview $overview = null
) {
}
public function slug(): string
{
return self::SLUG;
}
public function label(): string
{
return __('Divi', 'wp-lit-mcp');
}
public function registerSettings(): void
{
}
public function registerActions(): void
{
add_action('admin_post_' . self::RELEASE_ACTION, [$this, 'release']);
add_action('admin_post_' . self::WITHDRAW_ACTION, [$this, 'withdraw']);
add_action('load-' . SettingsPage::SCREEN_HOOK, [$this, 'removeOrphans']);
$this->registerNotice();
}
/** The admin notice for administrators while a Divi feature is unusable (registerActions() runs in admin only). */
public function registerNotice(): void
{
add_action('admin_notices', [$this, 'renderNotice']);
}
/**
* Only for administrators, only with Divi active, only while a feature is not usable – and not on the Divi tab,
* which shows the same table. Reads the cached matrix (D7), so no check runs on an ordinary admin page.
*/
public function renderNotice(): void
{
if (! current_user_can('manage_options') || $this->version() === '' || $this->onDiviTab()) {
return;
}
$unusable = ReleaseGate::status()->matrix->unusable();
if ($unusable === []) {
return;
}
printf(
'<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>',
esc_html(sprintf(
/* translators: 1: Divi version, 2: comma separated feature names */
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'LIT MCP: Mit Divi %1$s sind diese Fähigkeiten nicht nutzbar: %2$s. Die betroffenen Werkzeuge lehnen ab, alle anderen laufen weiter.',
'wp-lit-mcp'
),
$this->version(),
implode(', ', array_map(
static fn (FeatureStatus $status): string => $status->feature->label(),
$unusable
))
)),
esc_url(SettingsPage::url(self::SLUG)),
esc_html__('Details im Reiter „Divi“', 'wp-lit-mcp')
);
}
private function onDiviTab(): bool
{
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
return is_object($screen) && $screen->id === SettingsPage::SCREEN_HOOK && $this->requested();
}
/**
* load-<page> of the settings page, before any output: on this tab, for manage_options and with Divi active,
* deletes the check pages a dead check left behind (WriteProbe::removeOrphans(): marked drafts with the check
* title, older than ten minutes – no running check uses them). On a GET without nonce on purpose: it removes
* only the plugin's own leftovers and is idempotent. render() only reports what was deleted.
*/
public function removeOrphans(): void
{
if (! $this->requested() || ! current_user_can('manage_options') || $this->version() === '') {
return;
}
$this->removed = WriteProbe::removeOrphans();
}
public function release(): void
{
$action = $this->action ?? new AdminPostAction();
$action->guard();
check_admin_referer(self::RELEASE_ACTION);
$version = $this->version();
$report = $this->reports()->latest();
$fresh = $report !== null
&& $report->freshFor($version, Plugin::VERSION, $this->stateKey($version));
if ($version === '' || ! $fresh) {
$action->redirect(self::SLUG, 'divi_release_refused');
return;
}
($this->releases ?? new ReleaseStore())->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
ReleaseGate::forget();
$this->log?->record(
'lit/divi-release',
'divi',
$version,
sprintf('Divi %s für LIT MCP %s freigegeben (Prüfbericht %s).', $version, Plugin::VERSION, $report->id)
);
$action->redirect(self::SLUG, 'divi_released');
}
public function withdraw(): void
{
$action = $this->action ?? new AdminPostAction();
$action->guard();
check_admin_referer(self::WITHDRAW_ACTION);
$version = $this->version();
if (! ($this->releases ?? new ReleaseStore())->withdraw($version, Plugin::VERSION)) {
$action->redirect(self::SLUG, 'divi_withdraw_failed');
return;
}
ReleaseGate::forget();
$this->log?->record(
'lit/divi-release',
'divi',
$version,
sprintf('Freigabe von Divi %s zurückgezogen.', $version)
);
$action->redirect(self::SLUG, 'divi_withdrawn');
}
public function render(): void
{
if ($this->version() === '') {
echo '<p>' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
'Divi ist auf dieser Website nicht aktiv; die Divi-Freigabe ist ohne Bedeutung.',
'wp-lit-mcp'
) . '</p>';
return;
}
$status = ReleaseGate::status();
$report = $this->reports()->latest();
echo '<h2>' . esc_html__('Divi-Freigabe', 'wp-lit-mcp') . '</h2>';
echo '<p>' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'LIT MCP prüft nach einem Divi-Update jede Fähigkeit einzeln (z. B. „Seiten schreiben“). Ist sie nicht nutzbar, lehnen nur die Werkzeuge ab, die sie brauchen; alle anderen laufen weiter.',
'wp-lit-mcp'
) . '</p>';
if ($this->removed !== []) {
printf(
'<div class="notice notice-warning inline"><p>%s</p></div>',
esc_html(sprintf(
/* translators: %s: comma separated page IDs */
__('Liegengebliebene Prüfseiten gelöscht: %s', 'wp-lit-mcp'),
implode(', ', $this->removed)
))
);
}
$this->renderStatus($status);
$this->renderFeatures($status);
$this->renderForms($status, $report);
if ($report !== null) {
$this->renderReport($report);
}
}
private function renderStatus(ReleaseStatus $status): void
{
echo '<table class="form-table" role="presentation"><tbody>';
printf(
'<tr><th scope="row">%s</th><td>%s</td></tr>',
esc_html__('Divi-Version', 'wp-lit-mcp'),
esc_html($status->version === '' ? __('nicht erkannt', 'wp-lit-mcp') : $status->version)
);
printf(
'<tr><th scope="row">%s</th><td><strong>%s</strong> – %s</td></tr>',
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
esc_html($this->stateLabel($status->state)),
esc_html($this->explanation($status))
);
if ($status->problems !== []) {
echo '<tr><th scope="row">' . esc_html__('Geändert in Divi', 'wp-lit-mcp') . '</th><td><ul>';
foreach ($status->problems as $problem) {
echo '<li>' . esc_html($problem) . '</li>';
}
echo '</ul></td></tr>';
}
echo '</tbody></table>';
}
private function renderFeatures(ReleaseStatus $status): void
{
echo '<h3>' . esc_html__('Fähigkeiten', 'wp-lit-mcp') . '</h3>';
echo '<table class="widefat striped lit-mcp-divi-features"><thead><tr>';
$headings = [
__('Fähigkeit', 'wp-lit-mcp'),
__('Zustand', 'wp-lit-mcp'),
__('Quelle', 'wp-lit-mcp'),
__('Probleme', 'wp-lit-mcp'),
__('Betroffene Werkzeuge', 'wp-lit-mcp'),
];
foreach ($headings as $heading) {
echo '<th scope="col">' . esc_html($heading) . '</th>';
}
echo '</tr></thead><tbody>';
foreach (($this->overview ?? FeatureOverview::withoutTools())->rows($status->matrix) as $row) {
echo '<tr><td>' . esc_html($row['label']) . '</td>';
echo '<td><strong>' . esc_html($this->stateLabel(ReleaseState::from($row['state']))) . '</strong></td>';
echo '<td>' . esc_html($this->sourceLabel($row['source'])) . '</td>';
echo '<td>';
$this->renderList($row['problems'], false);
echo '</td><td>';
$this->renderList($row['tools'], true);
echo '</td></tr>';
}
echo '</tbody></table>';
$this->renderAutomationNote();
}
/** @param list<string> $items */
private function renderList(array $items, bool $code): void
{
if ($items === []) {
echo '–';
return;
}
echo '<ul>';
foreach ($items as $item) {
echo $code ? '<li><code>' . esc_html($item) . '</code></li>' : '<li>' . esc_html($item) . '</li>';
}
echo '</ul>';
}
private function sourceLabel(?string $source): string
{
return match ($source) {
ReleaseStatus::SOURCE_CODE => __('Plugin (code)', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_DATABASE => __('Freigabe auf dieser Website (database)', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_AUTO => __('Automatik (auto)', 'wp-lit-mcp'),
default => '–',
};
}
/** E1: what the automatic release does and how to switch it off. */
private function renderAutomationNote(): void
{
echo '<p class="description">' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Automatik: Ist die installierte Divi-Version im Plugin nicht als getestet hinterlegt, gibt LIT MCP jede Fähigkeit einzeln frei, wenn alle benutzten Divi-Funktionen vorhanden sind, ihre Aufrufe (Signaturen) und die Fingerabdrücke nachgebauter Divi-Abläufe unverändert sind und alle Konstanten definiert sind. Das prüft Aufrufformen, nicht das Verhalten von Divi. Fehlt für eine Stelle die Referenz, ist die Fähigkeit ungetestet; weicht etwas ab, ist sie inkompatibel.',
'wp-lit-mcp'
) . '</p>';
echo '<p class="description">' . sprintf(
/* translators: %s: filter name in a code element */
esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Die Automatik abschalten: Filter %s auf false setzen (z. B. in einem mu-Plugin). Dann gelten nicht hinterlegte Divi-Versionen bis zur Freigabe unten als ungetestet.',
'wp-lit-mcp'
),
'<code>lit_mcp_divi_auto_release</code>'
) . '</p>';
}
private function stateLabel(ReleaseState $state): string
{
return match ($state) {
ReleaseState::Released => __('freigegeben', 'wp-lit-mcp'),
ReleaseState::Untested => __('ungetestet', 'wp-lit-mcp'),
ReleaseState::Incompatible => __('inkompatibel', 'wp-lit-mcp'),
};
}
private function explanation(ReleaseStatus $status): string
{
if ($status->state === ReleaseState::Incompatible) {
return __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Divi hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Fähigkeiten (Tabelle unten) bleiben gesperrt, bis ein Plugin-Update von LIT MCP eingespielt ist; alle anderen laufen weiter.',
'wp-lit-mcp'
);
}
if ($status->state === ReleaseState::Untested) {
return __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Diese Version ist nicht geprüft. Die Werkzeuge der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
'wp-lit-mcp'
);
}
if ($status->source === ReleaseStatus::SOURCE_CODE) {
return __('Diese Version ist im Plugin als getestet hinterlegt.', 'wp-lit-mcp');
}
if ($status->source === ReleaseStatus::SOURCE_AUTO) {
return __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Automatisch freigegeben: Alle Divi-Stellen, die LIT MCP nutzt, stimmen mit der geprüften Referenz überein.',
'wp-lit-mcp'
);
}
$user = get_userdata((int) ($status->release['user_id'] ?? 0));
$releasedAt = (int) strtotime((string) ($status->release['released_at'] ?? ''));
return sprintf(
/* translators: 1: date, 2: user name */
__('Auf dieser Website freigegeben am %1$s von %2$s.', 'wp-lit-mcp'),
date_i18n((string) get_option('date_format'), $releasedAt),
is_object($user) && isset($user->display_name) ? (string) $user->display_name : '?'
);
}
private function renderForms(ReleaseStatus $status, ?CheckReport $report): void
{
$fresh = $report !== null
&& $report->freshFor($status->version, Plugin::VERSION, $this->stateKey($status->version));
$untested = $status->state === ReleaseState::Untested && $status->version !== '';
printf('<form method="post" action="%s">', esc_url(DiviCheckRoute::url()));
printf('<input type="hidden" name="_wpnonce" value="%s">', esc_attr(wp_create_nonce('wp_rest')));
// The check is the one thing to do while untested without a usable report; otherwise the release leads.
submit_button(
__('Prüfung starten', 'wp-lit-mcp'),
$untested && ! $fresh ? 'primary' : 'secondary',
'lit-mcp-divi-check',
false
);
echo '<p class="description">' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Die Prüfung legt eine private Testseite an, prüft Lesen und Schreiben und löscht sie wieder. Sie dauert einige Sekunden.',
'wp-lit-mcp'
) . '</p>';
echo '</form>';
if ($untested) {
if ($fresh) {
$this->actionForm(
self::RELEASE_ACTION,
/* translators: %s: Divi version */
sprintf(__('Divi %s freigeben', 'wp-lit-mcp'), $status->version),
'primary'
);
} else {
echo '<p>' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Freigeben geht erst nach einer vollständig grünen Prüfung dieser Divi-Version – Prüfung erneut ausführen.',
'wp-lit-mcp'
) . '</p>';
}
}
// A database release applies whatever the overall state: one incompatible feature must not hide the way back.
if ($status->source === ReleaseStatus::SOURCE_DATABASE || $status->release !== null) {
$this->actionForm(self::WITHDRAW_ACTION, __('Freigabe zurückziehen', 'wp-lit-mcp'), 'secondary');
}
}
private function actionForm(string $action, string $label, string $type): void
{
printf('<form method="post" action="%s">', esc_url(admin_url('admin-post.php')));
printf('<input type="hidden" name="action" value="%s">', esc_attr($action));
wp_nonce_field($action);
submit_button($label, $type, 'submit', false);
echo '</form>';
}
private function renderReport(CheckReport $report): void
{
printf(
'<h3>%s</h3><p>%s</p><ul class="lit-mcp-divi-report">',
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
esc_html(sprintf(
/* translators: 1: Divi version, 2: plugin version, 3: date */
__('Divi %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
$report->diviVersion,
$report->pluginVersion,
date_i18n((string) get_option('date_format'), (int) strtotime($report->createdAt))
))
);
foreach ($report->steps as $step) {
$mark = $step->skipped ? '–' : ($step->green() ? '✓' : '✗');
printf('<li>%s %s', esc_html($mark), esc_html($step->label()));
if ($step->skipped) {
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
}
if ($step->problems !== []) {
echo '<ul>';
foreach ($step->problems as $problem) {
echo '<li>' . esc_html($problem) . '</li>';
}
echo '</ul>';
}
echo '</li>';
}
echo '</ul><p class="description">' . esc_html__(
'Optional: eine Divi-Seite im Visual Builder öffnen und speichern.',
'wp-lit-mcp'
) . '</p>';
}
/** Whether this request opens this tab of the settings page (the load hook fires for every tab). */
private function requested(): bool
{
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
$tab = isset($_GET['tab']) ? sanitize_key(wp_unslash((string) $_GET['tab'])) : '';
return $tab === self::SLUG;
}
private function version(): string
{
return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')();
}
private function stateKey(string $version): string
{
return ($this->stateKey ?? static fn (string $v): string => ReleaseResolver::stateKey($v))($version);
}
private function reports(): CheckReportStore
{
return $this->reports ?? new CheckReportStore();
}
}