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( '

%s %s

', 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> $columns * @return list> */ 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( '

%s

', 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 '

' . 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' ) . '

'; } /** * 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 '

' . esc_html__( 'Optional: eine Divi-Seite im Visual Builder öffnen und speichern.', 'wp-lit-mcp' ) . '

'; } } /** 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( '

%s

', $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 '
' . esc_html__('So funktioniert die automatische Freigabe', 'wp-lit-mcp') . ''; echo '

' . 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' ) . '

'; echo '

' . 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' ), 'lit_mcp_divi_auto_release' ) . '

'; } private function version(): string { return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')(); } }