PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.32.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.32.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / admin / class-oxygen-metabox.php

class-oxygen-metabox.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.32.0, at includes/admin/class-oxygen-metabox.php

227 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Oxygen / Breakdance editor integration for the ThinkRank SEO metabox.
4 *
5 * Oxygen 6 is built on the Breakdance engine: its editor is a standalone Vue
6 * SPA served on a front-end builder request (`?breakdance=builder&id=ID`, or
7 * the legacy `?oxygen=builder`), NOT the WordPress post edit screen. It never
8 * renders WordPress metaboxes, never fires Elementor's `elementor/editor/*`
9 * hooks, and never submits the #post form, so neither the classic/block metabox
10 * nor the Elementor integration appears there.
11 *
12 * The builder template never calls `wp_head()`/`wp_footer()`, so the normal
13 * enqueue flow prints nothing there. Breakdance instead fires its own actions
14 * inside the builder chrome — `breakdance_builder_head` and
15 * `breakdance_builder_footer` — which fire only on a real builder request. This
16 * class hooks both and mounts the SAME React metabox app used elsewhere:
17 * - resolves the edited post from the `id` query var;
18 * - registers a dedicated `oxygen` bundle and localizes the same
19 * `thinkrankMetabox` data (via Metabox_Manager::get_localized_data()),
20 * augmented with the existing metadata + content preview that the classic
21 * editor would otherwise expose through hidden inputs;
22 * - prints the style in the builder <head> and the mount node + script in the
23 * builder footer by hand (wp_print_styles / wp_print_scripts), since neither
24 * wp_head nor wp_footer runs;
25 * - the bundle shows the drawer's own floating launcher and saves through the
26 * `thinkrank_save_metabox` AJAX route (Oxygen keeps its own content under the
27 * `_breakdance_data` postmeta via its own Save button — we never touch it).
28 *
29 * Scope: Oxygen 6 / Breakdance. The legacy shortcode builder (Oxygen < 6) is
30 * covered for content *extraction* (see Builder_Content) but not for this
31 * editor UI; the request detector still recognises its `?ct_builder=true` view
32 * so a future entry can hook it without touching this contract.
33 *
34 * @package ThinkRank
35 * @since 1.24.0
36 */
37
38 namespace ThinkRank\Admin;
39
40 if (!defined('ABSPATH')) {
41 exit;
42 }
43
44 /**
45 * Wires the React metabox into the Oxygen / Breakdance editor.
46 */
47 class Oxygen_Metabox {
48
49 /**
50 * Shared metabox manager (data builder + supported post types).
51 *
52 * @var Metabox_Manager
53 */
54 private Metabox_Manager $metabox;
55
56 /**
57 * Constructor.
58 *
59 * @param Metabox_Manager $metabox Shared metabox manager instance.
60 */
61 public function __construct(Metabox_Manager $metabox) {
62 $this->metabox = $metabox;
63 }
64
65 /**
66 * Handle used for the builder script + style + localized data.
67 */
68 private const HANDLE = 'thinkrank-oxygen';
69
70 /**
71 * Register the builder hooks.
72 *
73 * The Oxygen 6 / Breakdance builder template is a standalone HTML document
74 * that never calls `wp_head()` or `wp_footer()`, so the usual enqueue flow
75 * prints nothing there. Breakdance instead fires its own actions inside the
76 * builder chrome — `breakdance_builder_head` (in <head>) and
77 * `breakdance_builder_footer` (before </body>) — and those only fire on a
78 * genuine builder request. We hook both and print our assets by hand.
79 *
80 * Registering unconditionally is harmless when Oxygen isn't installed: the
81 * actions simply never fire.
82 *
83 * @return void
84 */
85 public function init(): void {
86 add_action('breakdance_builder_head', [$this, 'print_builder_styles']);
87 add_action('breakdance_builder_footer', [$this, 'print_builder_scripts']);
88 }
89
90 /**
91 * Resolve the post currently open in the builder.
92 *
93 * The builder loads as `?oxygen=builder&id=<postId>` (or `?breakdance=...`
94 * on Breakdance builds); the edited post is always the `id` query var.
95 *
96 * @return int Post ID, or 0 if it cannot be determined.
97 */
98 private function get_post_id(): int {
99 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only context resolution inside the builder request, no state change
100 foreach (['id', 'page_id', 'post', 'post_id'] as $key) {
101 if (isset($_GET[$key])) {
102 $candidate = absint(wp_unslash($_GET[$key]));
103 if ($candidate) {
104 return $candidate;
105 }
106 }
107 }
108 // phpcs:enable WordPress.Security.NonceVerification.Recommended
109
110 $queried = get_queried_object_id();
111 return $queried ? (int) $queried : 0;
112 }
113
114 /**
115 * Register the script/style and localize the data, once.
116 *
117 * Returns the post being edited when this request should get the panel, or
118 * null when it should be skipped (no post, wrong type, missing capability).
119 *
120 * @return \WP_Post|null Post to render for, or null to skip.
121 */
122 private function register_assets(): ?\WP_Post {
123 $post_id = $this->get_post_id();
124 if (!$post_id) {
125 return null;
126 }
127
128 // Editing SEO from the builder must respect the same capability the
129 // classic metabox save enforces.
130 if (!current_user_can('edit_post', $post_id)) {
131 return null;
132 }
133
134 $post = get_post($post_id);
135 if (!$post || !in_array($post->post_type, $this->metabox->get_supported_post_types(), true)) {
136 return null;
137 }
138
139 // Both hooks run per request; register only on the first.
140 if (wp_script_is(self::HANDLE, 'registered')) {
141 return $post;
142 }
143
144 $asset_file = THINKRANK_PLUGIN_DIR . 'assets/oxygen.asset.php';
145 $asset = file_exists($asset_file) ? include $asset_file : [
146 'dependencies' => ['react', 'react-dom', 'wp-element', 'wp-i18n', 'wp-api-fetch', 'wp-components'],
147 'version' => THINKRANK_VERSION,
148 ];
149
150 wp_register_script(
151 self::HANDLE,
152 THINKRANK_PLUGIN_URL . 'assets/oxygen.js',
153 $asset['dependencies'],
154 $asset['version'],
155 true
156 );
157
158 // Reuse the exact metabox config, then add the data the classic editor
159 // would normally hand the React app through hidden inputs.
160 $data = $this->metabox->get_localized_data($post_id);
161 $data['context'] = 'oxygen';
162 $data['existingMetadata'] = $this->metabox->get_post_metadata($post_id);
163 $data['contentPreview'] = $this->metabox->get_content_preview($post);
164 $data['postTitle'] = get_the_title($post_id);
165 // AJAX route used to persist all fields (no #post form in the builder).
166 $data['saveAction'] = 'thinkrank_save_metabox';
167
168 wp_localize_script(self::HANDLE, 'thinkrankMetabox', $data);
169
170 // Depend on wp-components so the @wordpress/components controls inside
171 // the drawer keep their styling against the builder's global resets.
172 wp_register_style(
173 self::HANDLE,
174 THINKRANK_PLUGIN_URL . 'assets/oxygen.css',
175 ['wp-components'],
176 THINKRANK_VERSION
177 );
178
179 return $post;
180 }
181
182 /**
183 * Print the drawer styles into the builder <head>.
184 *
185 * `wp_head()` never runs in the builder, so the enqueued style would never
186 * be emitted — print it (and its dependencies) directly.
187 *
188 * @return void
189 */
190 public function print_builder_styles(): void {
191 if (!$this->register_assets()) {
192 return;
193 }
194
195 wp_print_styles(self::HANDLE);
196 }
197
198 /**
199 * Print the mount node, media templates and drawer script into the builder
200 * footer, right before </body>.
201 *
202 * @return void
203 */
204 public function print_builder_scripts(): void {
205 if (!$this->register_assets()) {
206 return;
207 }
208
209 echo '<div id="thinkrank-oxygen-root" class="thinkrank-metabox"></div>';
210
211 // wp.media powers the social-image picker inside the drawer. Its
212 // templates normally print on wp_footer, which the builder never fires,
213 // so print the scripts and underscore templates by hand. Best-effort:
214 // the drawer still works without the picker if this is unavailable.
215 if (function_exists('wp_enqueue_media')) {
216 wp_enqueue_media();
217 wp_print_scripts('media-editor');
218 }
219
220 wp_print_scripts(self::HANDLE);
221
222 if (function_exists('wp_print_media_templates')) {
223 wp_print_media_templates();
224 }
225 }
226 }
227