Files
WP-LIT-MCP/src/Integrations/Divi/Release/WriteProbe.php
T
janniclas.loosenandClaude Opus 5.5 43c0a78374 fix: Divi-Reiter ohne Divi, Zustandstexte, Schreibsperren und Aufräum-Filter aus der Abschlussprüfung
Behebt die fünf Befunde der Abschlussprüfung von divi-absicherung: Divi-Reiter ohne aktives Divi zeigt nur
noch einen Hinweis (kein Status, keine Prüfung, kein Aufräumen), Zustandstexte werden erst beim Rendern
übersetzt, DiviPages::setFlags() und DiviThemeBuilder::liveId(true) prüfen jetzt selbst die
Freigabesperre, WriteProbe::removeOrphans() löscht nur noch Entwürfe mit passendem Titel, und
wp lit-mcp divi-check --update-reference verweigert sich ohne Entwicklungskopie, bevor irgendetwas
geschrieben wird.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:33:16 +02:00

225 lines
8.0 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\Release;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
use Throwable;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Spec §6.2 step 4: a throwaway draft page written through the normal Divi write path (ContentComposer → Composer
* → Serializer → DiviSaver, with the release lock opened for this page only), checked (content, builder flags,
* Divi renders every module without a PHP warning from Divi's files) and always deleted for good – in finally,
* not to the trash, with its revisions and meta. Pages a dead check left behind carry MARKER; removeOrphans()
* deletes them. No change log entry. Not final so CompatibilityCheck can be tested with a mock.
*/
class WriteProbe
{
public const MARKER = '_lit_mcp_check_page';
public const TITLE = '[LIT MCP Prüfung]';
/** Everyday modules without media, each with a text to find in the rendered page. */
public const SECTIONS = [[
'type' => 'section',
'label' => 'LIT MCP Prüfung',
'rows' => [
[
'columns' => '1_2,1_2',
'modules' => [
[
['type' => 'heading', 'text' => 'litcheck-heading', 'level' => 'h2'],
['type' => 'text', 'content' => '<p>litcheck-text</p>'],
['type' => 'button', 'text' => 'litcheck-button', 'url' => '/'],
],
[
['type' => 'blurb', 'title' => 'litcheck-blurb', 'content' => '<p>litcheck-blurb-text</p>'],
[
'type' => 'cta',
'title' => 'litcheck-cta',
'button_text' => 'litcheck-cta-button',
'button_url' => '/',
],
['type' => 'divider'],
],
],
],
[
'columns' => '4_4',
'modules' => [
['type' => 'toggle', 'title' => 'litcheck-toggle', 'content' => '<p>litcheck-toggle-text</p>'],
['type' => 'accordion', 'items' => [['title' => 'litcheck-accordion', 'content' => '<p>a</p>']]],
['type' => 'tabs', 'items' => [['title' => 'litcheck-tab', 'content' => '<p>t</p>']]],
['type' => 'icon-list', 'items' => [['text' => 'litcheck-icon-list']]],
],
],
],
]];
public const TEXTS = [
'litcheck-heading', 'litcheck-text', 'litcheck-button', 'litcheck-blurb', 'litcheck-cta', 'litcheck-toggle',
'litcheck-accordion', 'litcheck-tab', 'litcheck-icon-list',
];
/** Only pages older than this are orphans – a running check's page is younger. */
private const ORPHAN_AGE = '10 minutes ago';
/**
* @param string|null $diviDir PHP warnings from files below it fail the check; null = DiviEnvironment::themeDir().
*/
public function __construct(
private ContentComposer $composer,
private ?PageRenderer $renderer = null,
private ?string $diviDir = null
) {
}
/** @return list<string> German problems; [] = green. */
public function run(): array
{
$id = wp_insert_post([
'post_type' => 'page',
'post_status' => 'draft',
'post_title' => self::TITLE . ' ' . gmdate('Y-m-d H:i'),
'post_content' => '',
'meta_input' => [self::MARKER => '1'],
], true);
if ($id instanceof WP_Error || (int) $id <= 0) {
return ['Die Testseite konnte nicht angelegt werden: '
. ($id instanceof WP_Error ? $id->get_error_message() : 'keine ID')];
}
$id = (int) $id;
try {
$problems = $this->exercise($id);
} catch (Throwable $e) {
$problems = ['Schreibtest abgebrochen: ' . $e->getMessage()];
} finally {
if (! self::delete($id)) {
$problems[] = sprintf(
'Die Testseite %d konnte nicht gelöscht werden; bitte unter Seiten endgültig löschen.',
$id
);
}
}
return $problems;
}
/**
* @return list<int> IDs of check pages a dead check left behind, deleted now. Only drafts whose title starts
* with self::TITLE are considered – the marker meta alone is not enough (it is only a query hint here).
*/
public static function removeOrphans(): array
{
$posts = get_posts([
'post_type' => 'page',
'post_status' => 'draft',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a handful of marked pages at most.
'meta_key' => self::MARKER,
'date_query' => [['before' => self::ORPHAN_AGE, 'column' => 'post_date_gmt']],
'fields' => 'all',
'posts_per_page' => 20,
'no_found_rows' => true,
'suppress_filters' => true,
]);
$removed = [];
foreach (is_array($posts) ? $posts : [] as $post) {
if (! $post instanceof WP_Post || ! str_starts_with((string) $post->post_title, self::TITLE)) {
continue;
}
if (self::delete((int) $post->ID)) {
$removed[] = (int) $post->ID;
}
}
return $removed;
}
/** @return list<string> */
private function exercise(int $id): array
{
$written = ReleaseGate::whileProbing(
$id,
fn (): array|WP_Error => $this->composer->write($id, 'page', self::SECTIONS)
);
if ($written instanceof WP_Error) {
return ['Die Kurzform der Testseite wurde abgelehnt: ' . Errors::clientMessage($written)];
}
$post = get_post($id);
if (! $post instanceof WP_Post) {
return ['Die Testseite ist nach dem Schreiben nicht lesbar.'];
}
$problems = [];
if (! str_contains((string) $post->post_content, '<!-- wp:divi/')) {
$problems[] = 'Es wurde kein Divi-Inhalt gespeichert.';
}
foreach (DiviPages::FLAGS as $key => $value) {
if (get_post_meta($id, $key, true) !== $value) {
$problems[] = sprintf('Builder-Kennzeichen %s fehlt.', $key);
}
}
[$html, $warnings] = $this->render($post);
foreach (self::TEXTS as $text) {
if (! str_contains($html, $text)) {
$problems[] = sprintf('„%s“ fehlt in der gerenderten Seite.', $text);
}
}
foreach ($warnings as $warning) {
$problems[] = 'PHP-Warnung aus Divi beim Rendern: ' . $warning;
}
return $problems;
}
/** @return array{0: string, 1: list<string>} The rendered content and PHP warnings raised in Divi's files. */
private function render(WP_Post $post): array
{
$dir = $this->diviDir ?? DiviEnvironment::themeDir();
$warnings = [];
set_error_handler(
static function (
int $level,
string $message,
string $file = '',
int $line = 0
) use (
$dir,
&$warnings
): bool {
if ($dir !== null && str_starts_with($file, $dir)) {
$warnings[] = sprintf('%s (%s:%d)', $message, basename($file), $line);
}
return false;
},
E_WARNING | E_USER_WARNING
);
try {
$html = ($this->renderer ?? new PageRenderer())->render($post);
} finally {
restore_error_handler();
}
return [$html, $warnings];
}
private static function delete(int $id): bool
{
return wp_delete_post($id, true) instanceof WP_Post;
}
}