refactor(divi): DiviTab geht im Reiter „Compatibility“ auf

Der eigene Divi-Reiter entfällt. DiviAdminExtras behält Admin-Hinweis, Orphan-Bereinigung vor der Ausgabe (lit_mcp_compat_profile_load), Hinweis zum nächsten Schritt und zur privaten Testseite (lit_mcp_compat_profile_intro), Zustandstexte mit Quelle (lit_mcp_compat_explanation), Automatik-Notiz und Visual-Builder-Hinweis (lit_mcp_compat_profile_extra) sowie die Spalten Quelle, Probleme und Betroffene Tools über die neue, gegen fehlerhafte Callbacks abgesicherte Erweiterung lit_mcp_compat_capability_columns. Texte, Doku, CSS (lit-mcp-compat-*) und Smoke auf den neuen Reiter umgestellt, tote divi_*-Hinweise entfernt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 13:32:29 +01:00
co-authored by Claude Opus 5.5
parent 85f4138b36
commit b40b81f585
23 changed files with 1020 additions and 1284 deletions
@@ -0,0 +1,321 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Admin;
use Closure;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\Compatibility\Capability;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\FeatureStatus;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
defined('ABSPATH') || exit;
/**
* What Divi adds to the tab "Compatibility" (Admin\CompatibilityTab, which owns check, release and withdrawal):
* the admin notice on every admin page while a feature is unusable, the removal of check pages a dead check left
* behind (in the tab's load action, not while rendering), the columns source / problems / affected tools of the
* capability table (E3), the source-specific explanation of the overall state, above the capability table (intro)
* the orphan report, the next step and what the check does and, closing the section, the automation note and – with a
* report – the Visual Builder hint.
*/
final class DiviAdminExtras
{
/** @var list<int> Check pages removeOrphans() deleted in this request; renderIntro() reports them. */
private array $removed = [];
/**
* @param FeatureOverview|null $overview null = a table without the affected tools.
* @param (Closure(): string)|null $version Installed Divi version ('' = none); null = DiviEnvironment.
* @param CheckReportStore|null $reports null = the Divi store.
*/
public function __construct(
private ?FeatureOverview $overview = null,
private ?Closure $version = null,
private ?CheckReportStore $reports = null
) {
}
public function register(): void
{
add_action('admin_notices', [$this, 'renderNotice']);
add_action(CompatibilityTab::LOAD_ACTION, [$this, 'removeOrphans']);
add_action(CompatibilityTab::INTRO_ACTION, [$this, 'renderIntro']);
add_action(CompatibilityTab::EXTRA_ACTION, [$this, 'renderExtras']);
add_filter(CompatibilityTab::EXPLANATION_FILTER, [$this, 'explanation'], 10, 3);
add_filter(CompatibilityTab::COLUMNS_FILTER, [$this, 'columns'], 10, 2);
}
/**
* Only for administrators, only with Divi active, only while a feature is not usable – and not on the tab
* "Compatibility", which shows the same table. Reads the cached matrix (D7), so no check runs on an admin page.
*/
public function renderNotice(): void
{
if (! current_user_can('manage_options') || $this->version() === '' || $this->onCompatibilityTab()) {
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 Tools 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(CompatibilityTab::SLUG)),
esc_html__('Details im Reiter „Compatibility“', 'wp-lit-mcp')
);
}
private function onCompatibilityTab(): bool
{
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
$tab = is_string($_GET['tab'] ?? null) ? sanitize_key(wp_unslash($_GET['tab'])) : '';
return is_object($screen) && $screen->id === SettingsPage::SCREEN_HOOK && $tab === CompatibilityTab::SLUG;
}
/**
* lit_mcp_compat_profile_load('divi'), before any output (the tab fires it for administrators on its own tab):
* 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. renderExtras() only reports what was deleted (renderIntro()).
*/
public function removeOrphans(string $profileId): void
{
if ($profileId !== DiviProfile::ID || ! current_user_can('manage_options') || $this->version() === '') {
return;
}
$this->removed = WriteProbe::removeOrphans();
}
/**
* Columns of Divi's capability table: where the release comes from, the problems of the capability and the
* tools it affects. Reads the overview once per table (the matrix is the same for every row).
*
* @param list<array<string, mixed>> $columns
* @return list<array<string, mixed>>
*/
public function columns(array $columns, string $profileId): array
{
if ($profileId !== DiviProfile::ID) {
return $columns;
}
$rows = null;
$row = function (Capability $capability, CompatibilityMatrix $matrix) use (&$rows): array {
$rows ??= array_column(($this->overview ?? FeatureOverview::withoutTools())->rows($matrix), null, 'id');
return $rows[$capability->id()] ?? [];
};
return [
...$columns,
[
'heading' => __('Quelle', 'wp-lit-mcp'),
'cell' => fn (Capability $c, CompatibilityMatrix $m): array => array_values(array_filter(
[$this->sourceLabel($row($c, $m)['source'] ?? null)]
)),
],
[
'heading' => __('Probleme', 'wp-lit-mcp'),
'cell' => static fn (Capability $c, CompatibilityMatrix $m): array => $row($c, $m)['problems'] ?? [],
],
[
'heading' => __('Betroffene Tools', 'wp-lit-mcp'),
'cell' => static fn (Capability $c, CompatibilityMatrix $m): array => $row($c, $m)['tools'] ?? [],
'code' => true,
],
];
}
private function sourceLabel(?string $source): string
{
return match ($source) {
ReleaseStatus::SOURCE_CODE => __('Im Plugin als getestet hinterlegt', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_DATABASE => __('Von dir auf dieser Website freigegeben', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_AUTO => __('Automatisch geprüft', 'wp-lit-mcp'),
default => '',
};
}
/**
* lit_mcp_compat_profile_intro('divi'), above the capability table: the deleted check pages, the next step and
* what the check does (it writes to the site, so it says so).
*/
public function renderIntro(string $profileId): void
{
if ($profileId !== DiviProfile::ID) {
return;
}
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->renderNextStep(ReleaseGate::status());
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.',
'wp-lit-mcp'
) . '</p>';
}
/**
* Filter lit_mcp_compat_explanation: Divi says where its release comes from (plugin, automatic, this website)
* instead of the generic sentence, so the section does not explain the state twice.
*/
public function explanation(string $text, string $profileId, ReleaseState $state): string
{
if ($profileId !== DiviProfile::ID) {
return $text;
}
$status = ReleaseGate::status();
return match (true) {
$status->state === ReleaseState::Incompatible => __(
// 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'
),
$status->state === ReleaseState::Untested => __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Diese Version ist nicht geprüft. Die Tools der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
'wp-lit-mcp'
),
$status->source === ReleaseStatus::SOURCE_CODE => __(
'Diese Version ist im Plugin als getestet hinterlegt.',
'wp-lit-mcp'
),
$status->source === ReleaseStatus::SOURCE_AUTO => __(
// 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'
),
default => $this->releasedOnThisSite($status),
};
}
private function releasedOnThisSite(ReleaseStatus $status): string
{
$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 : '?'
);
}
/** lit_mcp_compat_profile_extra('divi'): the end of the Divi section (shown only while Divi is active). */
public function renderExtras(string $profileId): void
{
if ($profileId !== DiviProfile::ID) {
return;
}
$this->renderAutomationNote();
$reports = $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
if ($reports->latest() !== null) {
echo '<p class="description">' . esc_html__(
'Optional: eine Divi-Seite im Visual Builder öffnen und speichern.',
'wp-lit-mcp'
) . '</p>';
}
}
/** One sentence on what to do now; a released version needs none. */
private function renderNextStep(ReleaseStatus $status): void
{
$text = match ($status->state) {
ReleaseState::Untested => sprintf(
/* translators: %s: Divi version */
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
'Divi %s ist noch nicht geprüft. Starte die Prüfung; danach kannst du die Version freigeben.',
'wp-lit-mcp'
),
$status->version
),
ReleaseState::Incompatible => sprintf(
/* translators: %s: Divi version */
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
'Divi %s hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Tools bleiben gesperrt, bis ein Plugin-Update erscheint.',
'wp-lit-mcp'
),
$status->version
),
default => null,
};
if ($text === null) {
return;
}
printf(
'<div class="notice notice-%s inline"><p><strong>%s</strong></p></div>',
$status->state === ReleaseState::Incompatible ? 'error' : 'warning',
esc_html($text)
);
}
/** 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 als ungetestet.',
'wp-lit-mcp'
),
'<code>lit_mcp_divi_auto_release</code>'
) . '</p>';
}
private function version(): string
{
return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')();
}
}