From d04a50875174f873fbd1de803e1469a98b5e917f Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sun, 27 Sep 2026 15:18:42 +0200 Subject: [PATCH] feat: Theme Builder adapter, template value and per-object access DiviThemeBuilder wraps Divi's Theme Builder: reads through Divi's functions, writes layouts/templates through insert_layout/store_template behind a wp_die guard (edit_others_posts and function checks first, every handler filter and output buffer restored in finally), list/conditions/enabled mirrored from Divi's own save. Template: live = attached and enabled, whatever the post status (ruling Q8/B8). ThemeBuilderAccess: edit_others_posts plus Divi's theme_builder role, hidden IDs answer like missing ones, canEditTemplate (B11) and layoutsRefusal (B10). DiviCompatibility checks the Theme Builder functions and post types (E15). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Integrations/Divi/DiviCompatibility.php | 10 + .../Divi/ThemeBuilder/DiviThemeBuilder.php | 367 ++++++++++++++++++ .../Divi/ThemeBuilder/Template.php | 153 ++++++++ .../Divi/ThemeBuilder/ThemeBuilderAccess.php | 240 ++++++++++++ tests/Support/ThemeBuilderFixtures.php | 177 +++++++++ .../Divi/DiviCompatibilityTest.php | 10 + .../ThemeBuilder/DiviThemeBuilderTest.php | 330 ++++++++++++++++ .../Divi/ThemeBuilder/TemplateTest.php | 95 +++++ .../ThemeBuilder/ThemeBuilderAccessTest.php | 133 +++++++ 9 files changed, 1515 insertions(+) create mode 100644 src/Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php create mode 100644 src/Integrations/Divi/ThemeBuilder/Template.php create mode 100644 src/Integrations/Divi/ThemeBuilder/ThemeBuilderAccess.php create mode 100644 tests/Support/ThemeBuilderFixtures.php create mode 100644 tests/Unit/Integrations/Divi/ThemeBuilder/DiviThemeBuilderTest.php create mode 100644 tests/Unit/Integrations/Divi/ThemeBuilder/TemplateTest.php create mode 100644 tests/Unit/Integrations/Divi/ThemeBuilder/ThemeBuilderAccessTest.php diff --git a/src/Integrations/Divi/DiviCompatibility.php b/src/Integrations/Divi/DiviCompatibility.php index 73c96e1..0910d6e 100644 --- a/src/Integrations/Divi/DiviCompatibility.php +++ b/src/Integrations/Divi/DiviCompatibility.php @@ -8,6 +8,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance; use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore; use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap; use LIT\MCP\Integrations\Divi\Schema\DiviModules; +use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder; use Throwable; defined('ABSPATH') || exit; @@ -64,6 +65,15 @@ final class DiviCompatibility static fn (): bool => self::knowsGlobalLayout(), 'Taxonomien layout_category, layout_tag (Bibliothek)' => static fn (): bool => taxonomy_exists('layout_category') && taxonomy_exists('layout_tag'), + 'Theme Builder: Funktionen et_theme_builder_* und et_()' => + static fn (): bool => (new DiviThemeBuilder())->missing() === [], + 'Theme Builder: Post-Typen et_template und Layouts' => static fn (): bool => array_filter( + array_merge( + [DiviThemeBuilder::THEME_BUILDER, DiviThemeBuilder::TEMPLATE], + array_values(DiviThemeBuilder::LAYOUT_TYPES) + ), + static fn (string $type): bool => ! post_type_exists($type) + ) === [], ]; } diff --git a/src/Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php b/src/Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php new file mode 100644 index 0000000..e3f3c88 --- /dev/null +++ b/src/Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php @@ -0,0 +1,367 @@ + layout post type (theme-builder.php:7-23). */ + public const LAYOUT_TYPES = [ + 'header' => 'et_header_layout', + 'body' => 'et_body_layout', + 'footer' => 'et_footer_layout', + ]; + + public const TEMPLATE_LIST = '_et_template'; + public const USE_ON = '_et_use_on'; + public const EXCLUDE_FROM = '_et_exclude_from'; + public const ENABLED = '_et_enabled'; + public const UNUSED = '_et_theme_builder_marked_as_unused'; + public const BACKUP_OPTION = 'et_tb_templates_backup_'; + + /** Divi functions this adapter calls; DiviCompatibility reports missing ones. */ + public const FUNCTIONS = [ + 'et_theme_builder_get_theme_builder_post_id', + 'et_theme_builder_get_theme_builder_template_ids', + 'et_theme_builder_get_template', + 'et_theme_builder_insert_layout', + 'et_theme_builder_store_template', + 'et_theme_builder_get_flat_template_settings_options', + 'et_theme_builder_clear_wp_cache', + 'et_', + ]; + + private const DIE_FILTERS = [ + 'wp_die_handler', 'wp_die_ajax_handler', 'wp_die_json_handler', 'wp_die_jsonp_handler', + 'wp_die_xml_handler', 'wp_die_xmlrpc_handler', + ]; + + /** "header", "body" or "footer" for a layout post type, else null. */ + public static function areaOf(string $postType): ?string + { + $area = array_search($postType, self::LAYOUT_TYPES, true); + + return is_string($area) ? $area : null; + } + + /** @return list */ + public function missing(): array + { + return array_values(array_filter(self::FUNCTIONS, static fn (string $name): bool => ! function_exists($name))); + } + + /** The live Theme Builder post (publish, no library TB); $create makes it like Divi's first save. 0 = none. */ + public function liveId(bool $create = false): int + { + return function_exists('et_theme_builder_get_theme_builder_post_id') + ? (int) et_theme_builder_get_theme_builder_post_id(true, $create) + : 0; + } + + /** The Theme Builder draft (auto-draft) of an unsaved Theme Builder session; 0 = none (F15). */ + public function draftId(): int + { + return function_exists('et_theme_builder_get_theme_builder_post_id') + ? (int) et_theme_builder_get_theme_builder_post_id(false, false) + : 0; + } + + /** @return list Templates listed by Theme Builder post $themeBuilderId, in Divi's order (F15). */ + public function templateIds(int $themeBuilderId): array + { + if ($themeBuilderId <= 0 || ! function_exists('et_theme_builder_get_theme_builder_template_ids')) { + return []; + } + + return array_values(array_map( + 'intval', + (array) et_theme_builder_get_theme_builder_template_ids(true, $themeBuilderId) + )); + } + + /** Template $id as Divi reads it, null when it is no template. */ + public function template(int $id, bool $attached): ?Template + { + if (! function_exists('et_theme_builder_get_template')) { + return null; + } + + $divi = et_theme_builder_get_template($id); + if (! is_array($divi) || $divi === []) { + return null; + } + + $marked = (string) get_post_meta($id, self::UNUSED, true); + + return Template::fromDivi($divi, (string) get_post_status($id), $attached, $marked === '' ? null : $marked); + } + + /** @return list