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>
324 lines
14 KiB
PHP
324 lines
14 KiB
PHP
<?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() ?? '')();
|
||
}
|
||
}
|