TB layouts load through ThemeBuilderAccess (P12), live layouts (attached + enabled, Q8) need a dry run and confirm (Q9) and get the shared restore point of VisibleEdits, extracted from GlobalEdits (Q2: backup when revisions are off, P9 refusals, P14 naming). divi/post-content only in body layouts (F22), also for nested stored nodes. SaveScope::ThemeBuilderLayout sets Divi flags and clears site CSS + TB cache. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
136 lines
5.5 KiB
PHP
136 lines
5.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||
|
||
use LIT\MCP\Abilities\Errors;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
|
||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||
use LIT\MCP\Log;
|
||
use WP_Error;
|
||
use WP_Post;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Loads an entry for the Divi tools: Divi Library entries via LibraryAccess (E1/E3), Theme Builder
|
||
* layouts via ThemeBuilderAccess (Etappe 3b; hidden ones answer like a missing ID, P12), every other
|
||
* entry with its rights and status via PostGuard (spec §4.5), then the block tree. Divi 4
|
||
* shortcode content and unbalanced markup are refused with a hint – spec §3.4: Divi 4 content
|
||
* is kept, never edited.
|
||
*/
|
||
final class DiviPageLoader
|
||
{
|
||
public const OUTLINE_HINT = 'Aktuellen Stand mit lit/divi-outline (direkt: lit-divi-outline) neu lesen und die '
|
||
. 'Änderung mit der neuen tree_rev wiederholen.';
|
||
|
||
public function __construct(
|
||
private ContentSchema $schema,
|
||
private Parser $parser = new Parser(),
|
||
private ?LibraryAccess $library = null,
|
||
private ?ThemeBuilderAccess $themeBuilder = null
|
||
) {
|
||
}
|
||
|
||
/** The library access the Divi tools use, null when library entries are not reachable. */
|
||
public function library(): ?LibraryAccess
|
||
{
|
||
return $this->library;
|
||
}
|
||
|
||
/** Ruling P13: whether the Divi tools may name library IDs (no library access = never). */
|
||
public function libraryIdsVisible(): bool
|
||
{
|
||
return $this->library?->revealsIds() ?? false;
|
||
}
|
||
|
||
/** Readers pass $forWrite = false (trashed entries may be read, like lit-content-get). */
|
||
public function load(mixed $postId, bool $forWrite): DiviPage|WP_Error
|
||
{
|
||
// Library entries (E1) and Theme Builder layouts (Etappe 3b) through their access, everything else PostGuard.
|
||
$item = $this->library?->item($postId, $forWrite);
|
||
$layout = $item === null ? $this->themeBuilder?->layout($postId, $forWrite) : null;
|
||
$post = $item ?? $layout ?? PostGuard::editable($postId, $this->schema, 'post_id', ! $forWrite);
|
||
if ($post instanceof WP_Error) {
|
||
return $post;
|
||
}
|
||
|
||
$content = (string) $post->post_content;
|
||
if (! str_contains($content, '<!-- wp:divi/') && str_contains($content, '[et_pb_')) {
|
||
return Errors::unsupported(
|
||
sprintf(
|
||
'Eintrag %d enthält Divi-4-Inhalt (Shortcodes); die Divi-Werkzeuge bearbeiten nur Divi 5.',
|
||
$post->ID
|
||
),
|
||
'Die Seite einmal im Divi-Builder öffnen und speichern – Divi 5 wandelt sie dabei um. Bis dahin '
|
||
. 'bleibt der Inhalt unverändert.'
|
||
);
|
||
}
|
||
|
||
try {
|
||
$document = $this->parser->parse($content);
|
||
} catch (MalformedMarkup $e) {
|
||
Log::failure(sprintf('divi parse of post %d', $post->ID), $e->getMessage());
|
||
|
||
return Errors::unsupported(
|
||
sprintf(
|
||
'Der Inhalt von Eintrag %d ist kein gültiges Block-Markup; nichts wurde gelesen oder geändert.',
|
||
$post->ID
|
||
),
|
||
'Die Seite im Divi-Builder öffnen und speichern (Divi repariert dabei die Struktur) oder den Nutzer '
|
||
. 'fragen, ob eine frühere Revision (lit/revisions-list) wiederhergestellt werden soll.'
|
||
);
|
||
}
|
||
|
||
$area = $layout instanceof WP_Post ? DiviThemeBuilder::areaOf((string) $layout->post_type) : null;
|
||
|
||
return new DiviPage($post, $document, NodeIndex::build($document), $item instanceof WP_Post, $area);
|
||
}
|
||
|
||
/** The entry loaded for writing and checked by writable() – the refusal of either step. */
|
||
public function loadWritable(mixed $postId): DiviPage|WP_Error
|
||
{
|
||
$page = $this->load($postId, true);
|
||
if ($page instanceof WP_Error) {
|
||
return $page;
|
||
}
|
||
|
||
return $this->writable($page) ?? $page;
|
||
}
|
||
|
||
/**
|
||
* Additional checks for writing: the type must use the Divi builder (library entries and Theme Builder
|
||
* layouts always do),
|
||
* the content must be a Divi 5 layout (or empty) and saving must not publish a missed schedule
|
||
* (spec §2).
|
||
*/
|
||
public function writable(DiviPage $page): ?WP_Error
|
||
{
|
||
$post = $page->post;
|
||
$builderPost = $page->isLibraryItem() || $page->isThemeBuilderLayout();
|
||
if (! $builderPost && ! $this->schema->diviEnabled((string) $post->post_type)) {
|
||
return Errors::unsupported(
|
||
sprintf('Für den Inhaltstyp "%s" ist der Divi-Builder nicht aktiviert.', $post->post_type),
|
||
'In Divi → Theme-Optionen → Builder den Builder für diesen Inhaltstyp aktivieren (Administrator).'
|
||
);
|
||
}
|
||
|
||
if (! $page->isDiviLayout()) {
|
||
return Errors::unsupported(
|
||
sprintf('Eintrag %d ist kein Divi-Layout (Block-Editor- oder klassischer Inhalt).', $post->ID),
|
||
'Den Inhalt mit lit/content-update (direkt: lit-content-update) bearbeiten, oder den Nutzer fragen, ob '
|
||
. 'die Seite im Divi-Builder neu aufgebaut werden soll.'
|
||
);
|
||
}
|
||
|
||
return PostGuard::statusStable($post);
|
||
}
|
||
}
|