refactor: Divi pages adapter replaces DiviFlags

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 01:08:08 +02:00
co-authored by Claude Opus 5.5
parent e10a766e2a
commit fed77b0584
13 changed files with 232 additions and 132 deletions
@@ -20,7 +20,7 @@ defined('ABSPATH') || exit;
/**
* Divi short form for lit/content-create (spec §6.2): the sections become a Divi 5 layout in
* exactly one divi/placeholder – inserted by TreeEditor like the first section of an empty page
* (ruling S9) – stored through DiviSaver's kses window, with the builder flags (DiviFlags, S11).
* (ruling S9) – stored through DiviSaver's kses window, with the builder flags (DiviPages, S11).
*
* Allow map (ruling S22) for an entry that does not exist yet: check() runs before the insert
* with the composition bound to post ID 0 – Divi's own registry for "no entry" (get_options(0)
-40
View File
@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Pages;
use RuntimeException;
defined('ABSPATH') || exit;
/**
* Marks an entry as a Divi 5 builder layout, like the Visual Builder's own save does
* (Divi 5.2.1 SyncToServerController.php:382-387: _et_pb_use_divi_5 = "on", legacy
* _et_pb_use_builder = "on", and _et_pb_show_page_creation = "off" so the builder does not show
* its page creation flow on the first open). Ruling S11: update_post_meta() returns false for
* an unchanged value too, so success is checked by reading the flags back. Used by DiviSaver
* (inside FollowUp) and the Divi content creation.
*/
final class DiviFlags
{
public const FLAGS = [
'_et_pb_use_divi_5' => 'on',
'_et_pb_use_builder' => 'on',
'_et_pb_show_page_creation' => 'off',
];
/** @throws RuntimeException When a flag does not read back as set. */
public static function set(int $postId): void
{
foreach (self::FLAGS as $key => $value) {
update_post_meta($postId, $key, $value);
}
foreach (self::FLAGS as $key => $value) {
if (get_post_meta($postId, $key, true) !== $value) {
throw new RuntimeException(sprintf('Divi flag %s of post %d is not "%s".', $key, $postId, $value));
}
}
}
}
+1 -1
View File
@@ -74,6 +74,6 @@ final class DiviPage
/** The entry opened in the Divi Visual Builder – the edit_url of every Divi write (review I-1). */
public static function builderUrl(WP_Post|int $post): string
{
return (string) add_query_arg('et_fb', '1', (string) get_permalink($post));
return DiviPages::builderUrl($post);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use RuntimeException;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Adapter "Seiten" (spec §3): the Divi 5 builder flags of an entry as the Visual Builder's own save sets them
* (mirror vb-sync-flags: _et_pb_use_divi_5 = "on", legacy _et_pb_use_builder = "on", _et_pb_show_page_creation =
* "off" so the builder skips its page creation flow), whether an entry uses the builder, its builder URL, the
* static CSS of one page and Divi's list of linkable post types. Ruling S11: update_post_meta() returns false for an
* unchanged value too, so setFlags() checks by reading the flags back. Used by DiviSaver, the Divi content creation,
* WpLinkTargets and PostPresenter.
*/
final class DiviPages implements DiviAdapter
{
public const FLAGS = [
'_et_pb_use_divi_5' => 'on',
'_et_pb_use_builder' => 'on',
'_et_pb_show_page_creation' => 'off',
];
public static function manifest(): Manifest
{
return new Manifest(
Manifest::PAGES,
calls: [
new Call('ET_Core_PageResource::remove_static_resources', 3),
new Call('et_builder_get_public_post_types', 0),
],
keys: ['_et_pb_use_divi_5', '_et_pb_use_builder', '_et_pb_show_page_creation', 'et_fb'],
mirrors: [
new Mirror(
'vb-sync-flags',
'includes/builder-5/server/VisualBuilder/REST/SyncToServer/SyncToServerController.php',
'update',
'SyncToServerController',
'Builder-Kennzeichen einer Seite (_et_pb_use_divi_5, _et_pb_use_builder, _et_pb_show_page_creation)'
),
]
);
}
/** @throws RuntimeException When a flag does not read back as set. */
public static function setFlags(int $postId): void
{
foreach (self::FLAGS as $key => $value) {
update_post_meta($postId, $key, $value);
}
foreach (self::FLAGS as $key => $value) {
if (get_post_meta($postId, $key, true) !== $value) {
throw new RuntimeException(sprintf('Divi flag %s of post %d is not "%s".', $key, $postId, $value));
}
}
}
/** Whether Divi's builder flag marks $postId as a builder layout. */
public static function usesBuilder(int $postId): bool
{
return get_post_meta($postId, '_et_pb_use_builder', true) === 'on';
}
/** The entry opened in the Divi Visual Builder: its permalink with et_fb=1. */
public static function builderUrl(WP_Post|int $post): string
{
return (string) add_query_arg('et_fb', '1', (string) get_permalink($post));
}
/** Removes the static CSS files Divi generated for $postId (spec §5.6 step 5). */
public static function clearPageCss(int $postId): void
{
if (class_exists('ET_Core_PageResource')) {
\ET_Core_PageResource::remove_static_resources($postId, 'all', true);
}
}
/** Whether Divi offers a post_link_url_<type> option for $postType. Fail closed: without Divi's list, none. */
public static function isLinkable(string $postType): bool
{
if (! function_exists('et_builder_get_public_post_types')) {
return false;
}
$types = et_builder_get_public_post_types();
return is_array($types) && array_key_exists($postType, $types);
}
}
+5 -7
View File
@@ -20,7 +20,7 @@ defined('ABSPATH') || exit;
/**
* Save pipeline of spec §5.6, steps 3–5: restore point and wp_update_post() with wp_slash() (the
* status stays), Divi 5 flags (DiviFlags), Divi's static CSS cache. A failure after saving is
* status stays), Divi 5 flags and the page's static CSS (DiviPages). A failure after saving is
* only a warning (FollowUp); a filter that changed the stored content is reported as well.
*
* kses (spec D7): for users without unfiltered_html WordPress' kses would run
@@ -45,7 +45,7 @@ defined('ABSPATH') || exit;
* for its single wp_update_post().
*
* Divi's own save filters are NOT kses and are never removed (ruling S7, defense in depth):
* Security::initialize() (Divi 5.2.1 Security.php:74-81) registers on wp_insert_post_data
* Security::initialize() (Divi's Security.php) registers on wp_insert_post_data
* AttributeSecurity (priority 5, always), HtmlSecurity (priority 6, by capability) and
* DynamicContentFixes (priority 10: strips enable_html for users without unfiltered_html). The first
* two re-serialize the whole page when it holds custom attributes or htmlBefore/htmlAfter/elementType, so on such
@@ -98,7 +98,7 @@ class DiviSaver
$warnings = [];
if ($scope->setsFlags()) {
FollowUp::run(
static fn () => DiviFlags::set($postId),
static fn () => DiviPages::setFlags($postId),
'divi builder flags',
'Die Divi-Kennzeichen der Seite konnten nicht gesetzt werden; der Inhalt ist gespeichert – die Seite '
. 'einmal im Divi-Builder öffnen und speichern.',
@@ -107,9 +107,7 @@ class DiviSaver
}
FollowUp::run(
static function () use ($postId, $scope): void {
if (class_exists('ET_Core_PageResource')) {
\ET_Core_PageResource::remove_static_resources($postId, 'all', true);
}
DiviPages::clearPageCss($postId);
// A global library entry is part of every page that shows it (spec §5.6 step 5).
if ($scope->clearsSiteCss()) {
DiviThemeBuilder::clearSiteCaches();
@@ -149,7 +147,7 @@ class DiviSaver
}
if ($scope->setsFlags()) {
DiviFlags::set($postId);
DiviPages::setFlags($postId);
}
$stored = get_post($postId);
+5 -15
View File
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
use Closure;
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use WP_Post;
defined('ABSPATH') || exit;
@@ -22,18 +23,19 @@ final class WpLinkTargets implements LinkTargets
private const DEAD = ['trash', 'auto-draft', 'inherit'];
/** Never link targets, whatever Divi lists: attachments and the Divi Library (ruling Q14). */
private const NEVER = ['attachment', 'et_pb_layout'];
private const NEVER = ['attachment', DiviLibraryStore::POST_TYPE];
/** @var Closure(string): bool */
private Closure $linkable;
/**
* @param (Closure(string): bool)|null $linkable Whether Divi offers post_link_url_<type> (tests);
* null = Divi's et_builder_get_public_post_types(), the list its link option uses.
* null = DiviPages::isLinkable() (Divi's et_builder_get_public_post_types(), the list its
* link option uses).
*/
public function __construct(?Closure $linkable = null)
{
$this->linkable = $linkable ?? self::diviLinkable(...);
$this->linkable = $linkable ?? DiviPages::isLinkable(...);
}
public function postTypeOf(int $id): ?string
@@ -50,16 +52,4 @@ final class WpLinkTargets implements LinkTargets
return current_user_can('read_post', $id) ? $type : null;
}
/** Fail closed: without Divi's list no type has a link option. */
private static function diviLinkable(string $type): bool
{
if (! function_exists('et_builder_get_public_post_types')) {
return false;
}
$types = et_builder_get_public_post_types();
return is_array($types) && array_key_exists($type, $types);
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
@@ -14,6 +15,7 @@ final class DiviAdapters
{
/** @var list<class-string<DiviAdapter>> */
public const CLASSES = [
DiviPages::class,
DiviModules::class,
DynamicContentMap::class,
];
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\WordPress\Content;
use LIT\MCP\Abilities\JsonMap;
use LIT\MCP\Abilities\Rev;
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
@@ -95,7 +96,7 @@ class PostPresenter
public static function usesDivi(WP_Post $post): bool
{
return get_post_meta((int) $post->ID, '_et_pb_use_builder', true) === 'on'
return DiviPages::usesBuilder((int) $post->ID)
|| str_contains((string) $post->post_content, '<!-- wp:divi/');
}