metabox = $metabox; } /** * Register the Divi Visual Builder hooks. * * The VB is a themed front-end request, so we hook the front-end enqueue + * footer actions and gate every callback on `is_visual_builder()`. * Registering unconditionally is harmless when Divi isn't active: the * detector simply returns false. * * @return void */ public function init(): void { add_action('wp_enqueue_scripts', [$this, 'enqueue_editor_assets']); add_action('wp_footer', [$this, 'render_root']); } /** * Whether the current request is the Divi Visual Builder. * * @return bool */ private function is_visual_builder(): bool { // Divi's own detector is authoritative when present. if (function_exists('et_core_is_fb_enabled') && et_core_is_fb_enabled()) { return true; } if (function_exists('et_fb_is_enabled') && et_fb_is_enabled()) { return true; } // Fallback: the VB always loads as ?et_fb=1. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request classification, no state change return !empty($_GET['et_fb']); } /** * Resolve the post currently open in the Visual Builder. * * The VB loads on the post's own permalink, so the queried object is the * post being edited. * * @return int Post ID, or 0 if it cannot be determined. */ private function get_post_id(): int { $queried = get_queried_object_id(); if ($queried) { return (int) $queried; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only context resolution, no state change foreach (['post', 'post_id', 'page_id', 'et_post_id'] as $key) { if (isset($_GET[$key])) { $candidate = absint(wp_unslash($_GET[$key])); if ($candidate) { return $candidate; } } } // phpcs:enable WordPress.Security.NonceVerification.Recommended return 0; } /** * Enqueue the Divi metabox bundle and localize its data. * * @return void */ public function enqueue_editor_assets(): void { if (!$this->is_visual_builder()) { return; } $post_id = $this->get_post_id(); if (!$post_id) { return; } // Editing SEO from the builder must respect the same capability the // classic metabox save enforces. if (!current_user_can('edit_post', $post_id)) { return; } $post = get_post($post_id); if (!$post || !in_array($post->post_type, $this->metabox->get_supported_post_types(), true)) { return; } // wp.media powers the social-image picker inside the drawer. wp_enqueue_media(); $asset_file = THINKRANK_PLUGIN_DIR . 'assets/divi.asset.php'; $asset = file_exists($asset_file) ? include $asset_file : [ 'dependencies' => ['react', 'react-dom', 'wp-element', 'wp-i18n', 'wp-api-fetch', 'wp-components'], 'version' => THINKRANK_VERSION, ]; wp_enqueue_script( 'thinkrank-divi', THINKRANK_PLUGIN_URL . 'assets/divi.js', $asset['dependencies'], $asset['version'], true ); // Reuse the exact metabox config, then add the data the classic editor // would normally hand the React app through hidden inputs. $data = $this->metabox->get_localized_data($post_id); $data['context'] = 'divi'; $data['existingMetadata'] = $this->metabox->get_post_metadata($post_id); $data['contentPreview'] = $this->metabox->get_content_preview($post); $data['postTitle'] = get_the_title($post_id); // AJAX route used to persist all fields (no #post form in the builder). $data['saveAction'] = 'thinkrank_save_metabox'; wp_localize_script('thinkrank-divi', 'thinkrankMetabox', $data); // Depend on wp-components so the @wordpress/components controls inside // the drawer keep their styling against the builder's global resets. wp_enqueue_style( 'thinkrank-divi', THINKRANK_PLUGIN_URL . 'assets/divi.css', ['wp-components'], THINKRANK_VERSION ); } /** * Output the React mount node into the Visual Builder footer. * * @return void */ public function render_root(): void { if (!$this->is_visual_builder()) { return; } echo '
'; } }