From fed77b058450cf3dd53fab257f3d05050de1dd18 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Wed, 30 Sep 2026 01:08:08 +0200 Subject: [PATCH] refactor: Divi pages adapter replaces DiviFlags Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Divi/Pages/DiviContentComposer.php | 2 +- src/Integrations/Divi/Pages/DiviFlags.php | 40 ------ src/Integrations/Divi/Pages/DiviPage.php | 2 +- src/Integrations/Divi/Pages/DiviPages.php | 98 +++++++++++++++ src/Integrations/Divi/Pages/DiviSaver.php | 12 +- src/Integrations/Divi/Pages/WpLinkTargets.php | 20 +-- .../Divi/Release/DiviAdapters.php | 2 + .../WordPress/Content/PostPresenter.php | 3 +- .../Divi/Library/GlobalEditsTest.php | 4 +- .../Integrations/Divi/Pages/DiviFlagsTest.php | 58 --------- .../Integrations/Divi/Pages/DiviPagesTest.php | 114 ++++++++++++++++++ .../Integrations/Divi/Pages/DiviSaverTest.php | 4 +- .../Divi/Release/DiviBoundaryTest.php | 5 - 13 files changed, 232 insertions(+), 132 deletions(-) delete mode 100644 src/Integrations/Divi/Pages/DiviFlags.php create mode 100644 src/Integrations/Divi/Pages/DiviPages.php delete mode 100644 tests/Unit/Integrations/Divi/Pages/DiviFlagsTest.php create mode 100644 tests/Unit/Integrations/Divi/Pages/DiviPagesTest.php diff --git a/src/Integrations/Divi/Pages/DiviContentComposer.php b/src/Integrations/Divi/Pages/DiviContentComposer.php index 0288114..b19726e 100644 --- a/src/Integrations/Divi/Pages/DiviContentComposer.php +++ b/src/Integrations/Divi/Pages/DiviContentComposer.php @@ -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) diff --git a/src/Integrations/Divi/Pages/DiviFlags.php b/src/Integrations/Divi/Pages/DiviFlags.php deleted file mode 100644 index b38da3e..0000000 --- a/src/Integrations/Divi/Pages/DiviFlags.php +++ /dev/null @@ -1,40 +0,0 @@ - '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)); - } - } - } -} diff --git a/src/Integrations/Divi/Pages/DiviPage.php b/src/Integrations/Divi/Pages/DiviPage.php index 5e099b1..22aec71 100644 --- a/src/Integrations/Divi/Pages/DiviPage.php +++ b/src/Integrations/Divi/Pages/DiviPage.php @@ -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); } } diff --git a/src/Integrations/Divi/Pages/DiviPages.php b/src/Integrations/Divi/Pages/DiviPages.php new file mode 100644 index 0000000..147c653 --- /dev/null +++ b/src/Integrations/Divi/Pages/DiviPages.php @@ -0,0 +1,98 @@ + '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_ 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); + } +} diff --git a/src/Integrations/Divi/Pages/DiviSaver.php b/src/Integrations/Divi/Pages/DiviSaver.php index 35eb777..f87b5f4 100644 --- a/src/Integrations/Divi/Pages/DiviSaver.php +++ b/src/Integrations/Divi/Pages/DiviSaver.php @@ -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); diff --git a/src/Integrations/Divi/Pages/WpLinkTargets.php b/src/Integrations/Divi/Pages/WpLinkTargets.php index 5be3764..bae4341 100644 --- a/src/Integrations/Divi/Pages/WpLinkTargets.php +++ b/src/Integrations/Divi/Pages/WpLinkTargets.php @@ -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_ (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); - } } diff --git a/src/Integrations/Divi/Release/DiviAdapters.php b/src/Integrations/Divi/Release/DiviAdapters.php index 86b9c9d..69bd9c8 100644 --- a/src/Integrations/Divi/Release/DiviAdapters.php +++ b/src/Integrations/Divi/Release/DiviAdapters.php @@ -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> */ public const CLASSES = [ + DiviPages::class, DiviModules::class, DynamicContentMap::class, ]; diff --git a/src/Integrations/WordPress/Content/PostPresenter.php b/src/Integrations/WordPress/Content/PostPresenter.php index 366f6e5..2799e2a 100644 --- a/src/Integrations/WordPress/Content/PostPresenter.php +++ b/src/Integrations/WordPress/Content/PostPresenter.php @@ -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, '