Files
WP-LIT-MCP/src/Integrations/Divi/Admin/DiviAdminExtras.php
T
Jan-Niclas LoosenandClaude Opus 5.5 f337a57f26 feat(admin): Reiter „Kompatibilität“ übersetzt und übersichtlicher
Sichtbarer Name „Kompatibilität“ (Slug compatibility bleibt) in Reiter, Hinweisen, Hilfe-Tab, CLI-Hilfe, Guides und readme. Oben eine Übersicht aller Plugins mit Gesamtzustand, Version, Zustand (Wort und Icon), Freigabe-Art in Klartext und Sprunglink; je Plugin erst Zustand und nächster Schritt, dann Aktionen, dann aufklappbare Details (offen bei Handlungsbedarf, ohne JavaScript). Tabellen mit Beschriftung und Kartenansicht bis 375 px, Buttons mit Plugin-Namen im zugänglichen Namen, Kontraste mit Reserve. Doku: Divi leert beim Prüfen den Cache einmal komplett.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:39:47 +01:00

324 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 „Kompatibilität“', '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 '<details class="lit-mcp-hint"><summary>'
. esc_html__('So funktioniert die automatische Freigabe', 'wp-lit-mcp') . '</summary>';
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></details>';
}
private function version(): string
{
return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')();
}
}