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:
co-authored by
Claude Opus 5.5
parent
85f4138b36
commit
b40b81f585
@@ -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() ?? '')();
|
||||
}
|
||||
}
|
||||
@@ -1,583 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Admin;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Admin\AdminPostAction;
|
||||
use LIT\MCP\Admin\AdminTab;
|
||||
use LIT\MCP\Admin\CheckRoute;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use LIT\MCP\Compatibility\CheckReport;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Compatibility\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Compatibility\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, Admin\CheckRoute), "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 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(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()->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
|
||||
ReleaseGate::invalidate();
|
||||
$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()->withdraw($version, Plugin::VERSION)) {
|
||||
$action->redirect(self::SLUG, 'divi_withdraw_failed');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ReleaseGate::invalidate();
|
||||
$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 Tools 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->renderNextStep($status);
|
||||
$this->renderStatus($status);
|
||||
$this->renderFeatures($status);
|
||||
$this->renderForms($status, $report);
|
||||
if ($report !== null) {
|
||||
$this->renderReport($report);
|
||||
}
|
||||
}
|
||||
|
||||
/** One sentence on what to do now, above the details; a released version needs none. */
|
||||
private function renderNextStep(ReleaseStatus $status): void
|
||||
{
|
||||
if ($status->version === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$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 unten 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)
|
||||
);
|
||||
}
|
||||
|
||||
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>%s – %s</td></tr>',
|
||||
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
||||
$this->stateBadge($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
|
||||
{
|
||||
// Open when something needs attention; a released version keeps the long table folded.
|
||||
printf(
|
||||
'<details class="lit-mcp-divi-details"%s><summary>%s</summary>',
|
||||
$status->state === ReleaseState::Released ? '' : ' open',
|
||||
esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
|
||||
);
|
||||
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 Tools', '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) {
|
||||
// data-label: on phones each row becomes a card and the cell names itself.
|
||||
printf('<tr><td data-label="%s">%s</td>', esc_attr($headings[0]), esc_html($row['label']));
|
||||
printf(
|
||||
'<td data-label="%s">%s</td>',
|
||||
esc_attr($headings[1]),
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
|
||||
$this->stateBadge(ReleaseState::from($row['state']))
|
||||
);
|
||||
printf(
|
||||
'<td data-label="%s">%s</td>',
|
||||
esc_attr($headings[2]),
|
||||
esc_html($this->sourceLabel($row['source']))
|
||||
);
|
||||
printf('<td data-label="%s">', esc_attr($headings[3]));
|
||||
$this->renderList($row['problems'], false);
|
||||
printf('</td><td data-label="%s">', esc_attr($headings[4]));
|
||||
$this->renderList($row['tools'], true);
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$this->renderAutomationNote();
|
||||
echo '</details>';
|
||||
}
|
||||
|
||||
/** @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 => __('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 => '–',
|
||||
};
|
||||
}
|
||||
|
||||
/** 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>';
|
||||
}
|
||||
|
||||
/** Same coloured icons as the status list on the tab "Verbindung"; the word stays, colour is never alone. */
|
||||
private function stateBadge(ReleaseState $state): string
|
||||
{
|
||||
$icon = match ($state) {
|
||||
ReleaseState::Released => 'yes-alt',
|
||||
ReleaseState::Untested => 'warning',
|
||||
ReleaseState::Incompatible => 'dismiss',
|
||||
};
|
||||
|
||||
return sprintf(
|
||||
'<span class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
|
||||
. ' aria-hidden="true"></span> <strong>%2$s</strong></span>',
|
||||
$icon,
|
||||
esc_html($this->stateLabel($state))
|
||||
);
|
||||
}
|
||||
|
||||
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 Tools 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(CheckRoute::url(DiviProfile::ID)));
|
||||
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->version,
|
||||
$report->pluginVersion,
|
||||
date_i18n((string) get_option('date_format'), (int) strtotime($report->createdAt))
|
||||
))
|
||||
);
|
||||
foreach ($report->steps as $step) {
|
||||
[$icon, $word] = match (true) {
|
||||
$step->skipped => ['minus', __('übersprungen', 'wp-lit-mcp')],
|
||||
$step->green() => ['yes-alt', __('bestanden', 'wp-lit-mcp')],
|
||||
default => ['dismiss', __('fehlgeschlagen', 'wp-lit-mcp')],
|
||||
};
|
||||
printf(
|
||||
'<li class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
|
||||
. ' aria-hidden="true"></span><span class="screen-reader-text">%2$s:</span> %3$s',
|
||||
esc_attr($icon),
|
||||
esc_html($word),
|
||||
esc_html(DiviCheckSteps::label($step))
|
||||
);
|
||||
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 releases(): ReleaseStore
|
||||
{
|
||||
return $this->releases ?? new ReleaseStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
}
|
||||
|
||||
private function reports(): CheckReportStore
|
||||
{
|
||||
return $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user