PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
← All changes | admin/class-metasync-html-visual-editor.php +205 -50 2.6.13trunk View file →
@@ -20,8 +20,27 @@
20 20
21 21 class Metasync_HTML_Visual_Editor
22 22 {
23 23 /**
24 + * Pinned version of the bundled GrapesJS core.
25 + *
26 + * @var string
27 + */
28 + const GRAPESJS_VERSION = '0.21.7';
29 +
30 + /**
31 + * Pinned version of the bundled grapesjs-blocks-basic plugin.
32 + *
33 + * Releases before 1.0.0 registered themselves by calling
34 + * grapesjs.plugins.add('gjs-blocks-basic'); 1.0.x only exposes a UMD
35 + * export, so the editor passes the plugin by reference rather than by
36 + * that legacy global name.
37 + *
38 + * @var string
39 + */
40 + const GRAPESJS_BLOCKS_BASIC_VERSION = '1.0.2';
41 +
42 + /**
24 43 * Plugin name
25 44 *
26 45 * @var string
27 46 */
@@ -57,8 +76,12 @@
57 76
58 77 // Add admin menu page for the editor
59 78 add_action('admin_menu', array($this, 'add_editor_page'));
60 79
80 + // Enqueue editor assets during the normal asset phase so stylesheets
81 + // land in <head> rather than being flushed late from the page body.
82 + add_action('admin_enqueue_scripts', array($this, 'maybe_enqueue_editor_assets'));
83 +
61 84 // Register AJAX handlers
62 85 add_action('wp_ajax_metasync_save_html', array($this, 'ajax_save_html'));
63 86 add_action('wp_ajax_metasync_upload_image', array($this, 'ajax_upload_image'));
64 87 }
@@ -63,8 +86,40 @@
63 86 add_action('wp_ajax_metasync_upload_image', array($this, 'ajax_upload_image'));
64 87 }
65 88
66 89 /**
90 + * Admin page slug for the visual editor.
91 + *
92 + * @return string
93 + */
94 + private function get_editor_page_slug()
95 + {
96 + return Metasync_Admin::$page_slug . '-html-editor';
97 + }
98 +
99 + /**
100 + * Enqueue the editor assets when the current request is the editor page.
101 + *
102 + * Keyed on the request rather than on the hook suffix because the editor is
103 + * registered as a hidden submenu page, so its generated suffix is not
104 + * stable to match against.
105 + */
106 + public function maybe_enqueue_editor_assets()
107 + {
108 + $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
109 +
110 + if ($page !== $this->get_editor_page_slug()) {
111 + return;
112 + }
113 +
114 + if (!current_user_can('edit_pages')) {
115 + return;
116 + }
117 +
118 + $this->enqueue_editor_assets();
119 + }
120 +
121 + /**
67 122 * Add "Edit HTML" button to row actions
68 123 *
69 124 * @param array $actions Row actions
70 125 * @param WP_Post $post Post object
@@ -114,9 +169,9 @@
114 169 public function render_editor_page()
115 170 {
116 171 // Check permissions
117 172 if (!current_user_can('edit_pages')) {
118 - wp_die(__('You do not have sufficient permissions to access this page.'));
173 + wp_die(__('You do not have sufficient permissions to access this page.', 'metasync'));
119 174 }
120 175
121 176 // Get post ID
122 177 $post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
@@ -121,16 +176,20 @@
121 176 // Get post ID
122 177 $post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
123 178
124 179 if (!$post_id) {
125 - wp_die(__('Invalid page ID.'));
180 + wp_die(__('Invalid page ID.', 'metasync'));
126 181 }
127 182
183 + if (!current_user_can('edit_post', $post_id)) {
184 + wp_die(__('You do not have sufficient permissions to access this page.', 'metasync'));
185 + }
186 +
128 187 // Get post
129 188 $post = get_post($post_id);
130 189
131 190 if (!$post) {
132 - wp_die(__('Page not found.'));
191 + wp_die(__('Page not found.', 'metasync'));
133 192 }
134 193
135 194 // Check if raw HTML is enabled
136 195 $has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true);
@@ -135,9 +194,9 @@
135 194 // Check if raw HTML is enabled
136 195 $has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true);
137 196
138 197 if (!$has_raw_html) {
139 - wp_die(__('This page is not a raw HTML page.'));
198 + wp_die(__('This page is not a raw HTML page.', 'metasync'));
140 199 }
141 200
142 201 // Get HTML content
143 202 $html_content = get_post_meta($post_id, '_metasync_raw_html_content', true);
@@ -148,68 +207,104 @@
148 207
149 208 // Get label for branding
150 209 $label = Metasync::get_whitelabel_company_name() ?: 'SearchAtlas AI';
151 210
152 - // Enqueue editor assets
153 - $this->enqueue_editor_assets();
154 -
155 - // Render editor UI
211 + // Render editor UI. Assets are enqueued on admin_enqueue_scripts.
156 212 include plugin_dir_path(__FILE__) . 'partials/metasync-html-editor-page.php';
157 213 }
158 214
159 215 /**
160 216 * Enqueue editor assets (GrapesJS + custom scripts)
217 + *
218 + * The editor runtime is served from the plugin's own bundled copies at
219 + * pinned versions. It used to be pulled from public CDNs, which made any
220 + * blocked, throttled or offline request render the editor as an empty
221 + * canvas with no explanation.
161 222 */
162 223 private function enqueue_editor_assets()
163 224 {
164 - // Font Awesome for icons
165 - wp_enqueue_style(
166 - 'font-awesome',
167 - 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css',
168 - array(),
169 - '6.4.0'
225 + $lib_url = plugin_dir_url(__FILE__) . 'lib/';
226 + $lib_path = plugin_dir_path(__FILE__) . 'lib/';
227 +
228 + // Bundled libraries, in load order. Anything missing from disk is
229 + // reported to the client so the editor can explain itself instead of
230 + // rendering blank. Handles are namespaced so a theme or plugin
231 + // registering a bare "grapesjs" handle cannot collide with ours.
232 + $libraries = array(
233 + 'metasync-grapesjs' => array(
234 + 'version' => self::GRAPESJS_VERSION,
235 + 'style' => 'grapesjs/grapes.min.css',
236 + 'script' => 'grapesjs/grapes.min.js',
237 + ),
238 + 'metasync-grapesjs-blocks-basic' => array(
239 + 'version' => self::GRAPESJS_BLOCKS_BASIC_VERSION,
240 + 'script' => 'grapesjs-blocks-basic/grapesjs-blocks-basic.min.js',
241 + 'deps' => array('metasync-grapesjs'),
242 + ),
170 243 );
171 244
172 - // GrapesJS core
173 - wp_enqueue_style(
174 - 'grapesjs',
175 - 'https://unpkg.com/grapesjs@0.21.7/dist/css/grapes.min.css',
176 - array(),
177 - '0.21.7'
178 - );
245 + $missing = array();
179 246
180 - wp_enqueue_script(
181 - 'grapesjs',
182 - 'https://unpkg.com/grapesjs@0.21.7/dist/grapes.min.js',
183 - array(),
184 - '0.21.7',
185 - true
186 - );
247 + foreach ($libraries as $handle => $library) {
248 + if (file_exists($lib_path . $library['script'])) {
249 + wp_enqueue_script(
250 + $handle,
251 + $lib_url . $library['script'],
252 + isset($library['deps']) ? $library['deps'] : array(),
253 + $library['version'],
254 + true
255 + );
256 + } else {
257 + $missing[] = $handle;
258 + }
187 259
188 - // GrapesJS Plugins
189 - wp_enqueue_script(
190 - 'grapesjs-blocks-basic',
191 - 'https://unpkg.com/grapesjs-blocks-basic',
192 - array('grapesjs'),
193 - null,
194 - true
195 - );
260 + if (!isset($library['style'])) {
261 + continue;
262 + }
196 263
197 - // Custom editor JS (with timestamp for cache busting)
264 + if (file_exists($lib_path . $library['style'])) {
265 + wp_enqueue_style(
266 + $handle,
267 + $lib_url . $library['style'],
268 + array(),
269 + $library['version']
270 + );
271 + } else {
272 + $missing[] = $handle;
273 + }
274 + }
275 +
276 + $missing = array_values(array_unique($missing));
277 +
278 + // The editor bundle depends on the bundled core only when that core
279 + // was actually enqueued. When a library file is absent from disk it is
280 + // never registered, and a dependency on an unregistered handle makes
281 + // WordPress suppress the editor bundle itself — including the localized
282 + // `missing` list, so the failure would go back to being a blank canvas
283 + // with no explanation.
284 + $editor_script_deps = array('jquery');
285 + // The editor chrome and the sidebar's panel switcher label their
286 + // buttons with dashicons glyphs, so the stylesheet is a real
287 + // dependency rather than something to inherit from the admin page.
288 + $editor_style_deps = array('dashicons');
289 + if (wp_script_is('metasync-grapesjs', 'registered')) {
290 + $editor_script_deps[] = 'metasync-grapesjs';
291 + $editor_style_deps[] = 'metasync-grapesjs';
292 + }
293 +
198 294 wp_enqueue_script(
199 295 'metasync-html-editor',
200 296 plugins_url('js/metasync-html-editor.js', __FILE__),
201 - array('jquery', 'grapesjs'),
202 - $this->version . '.' . time(),
297 + $editor_script_deps,
298 + $this->version,
203 299 true
204 300 );
205 301
206 - // Custom editor CSS (with timestamp for cache busting)
207 302 wp_enqueue_style(
208 303 'metasync-html-editor',
209 304 plugins_url('css/metasync-html-editor.css', __FILE__),
210 - array('grapesjs'),
211 - $this->version . '.' . time()
305 + $editor_style_deps,
306 + $this->version
212 307 );
213 308
214 309 // Localize script with data
215 310 wp_localize_script('metasync-html-editor', 'metasyncEditor', array(
@@ -216,14 +311,34 @@
216 311 'ajax_url' => admin_url('admin-ajax.php'),
217 312 'nonce' => wp_create_nonce('metasync_html_editor'),
218 313 'post_id' => isset($_GET['post_id']) ? intval($_GET['post_id']) : 0,
219 314 'preview_url' => get_permalink(isset($_GET['post_id']) ? intval($_GET['post_id']) : 0),
220 - 'back_url' => admin_url('edit.php?post_type=page'),
315 + // Names of bundled libraries that are absent from disk, so the
316 + // client can name the failing dependency without exposing paths
317 + // or other sensitive detail.
318 + 'missing' => $missing,
221 319 'i18n' => array(
222 320 'saving' => __('Saving...', 'metasync'),
223 321 'saved' => __('Saved!', 'metasync'),
224 322 'error' => __('Error saving', 'metasync'),
323 + 'session_expired' => __('Your session has expired. Copy your work before reloading the page.', 'metasync'),
324 + 'ready' => __('Ready', 'metasync'),
325 + 'unsaved_changes' => __('Unsaved changes', 'metasync'),
225 326 'confirm_exit' => __('You have unsaved changes. Are you sure you want to leave?', 'metasync'),
327 + 'confirm_preview' => __('You have unsaved changes. Preview will show the last saved version. Continue?', 'metasync'),
328 + 'panel_styles' => __('Styles', 'metasync'),
329 + 'panel_settings' => __('Settings', 'metasync'),
330 + 'panel_layers' => __('Layers', 'metasync'),
331 + 'panel_blocks' => __('Blocks', 'metasync'),
332 + 'load_failed_title' => __('The visual editor could not start', 'metasync'),
333 + 'load_failed_core' => __('The visual editor library could not be loaded, so this page cannot be edited visually. Reload the page, and if the problem continues check whether a browser extension, proxy or content security policy is blocking plugin scripts.', 'metasync'),
334 + 'load_failed_blocks' => __('The editor loaded, but its extra block library is unavailable, so the Blocks panel only offers the built-in blocks. Existing page content can still be edited and saved normally.', 'metasync'),
335 + 'load_failed_init' => __('The visual editor failed to start while loading this page. Reload to try again; the saved page content has not been changed.', 'metasync'),
336 + 'load_failed_detail' => __('Missing component: %s', 'metasync'),
337 + 'reload' => __('Reload page', 'metasync'),
338 + 'dismiss' => __('Dismiss', 'metasync'),
339 + 'save_disabled' => __('Saving is disabled because the editor did not load', 'metasync'),
340 + 'upload_failed' => __('Image upload failed', 'metasync'),
226 341 )
227 342 ));
228 343 }
229 344
@@ -228,8 +343,16 @@
228 343 }
229 344
230 345 /**
231 346 * AJAX handler for saving HTML
347 + *
348 + * The payload is stored verbatim, matching the contract of every other
349 + * writer of this meta key: the Custom Pages metabox stores the raw value
350 + * for users who can edit the page, and the front-end renderer echoes it
351 + * as authored. Filtering here with kses would silently strip the very
352 + * elements raw HTML pages exist to carry (doctype, head assets, forms,
353 + * iframes, inline SVG), and re-filtering an already-filtered value is
354 + * what let entity-encoded markup re-materialize as live tags.
232 355 */
233 356 public function ajax_save_html()
234 357 {
235 358 // Check nonce
@@ -236,19 +359,35 @@
236 359 check_ajax_referer('metasync_html_editor', 'nonce');
237 360
238 361 // Check permissions
239 362 if (!current_user_can('edit_pages')) {
240 - wp_send_json_error(array('message' => __('Permission denied', 'metasync')));
363 + wp_send_json_error(array('message' => __('Permission denied', 'metasync')), 403);
241 364 }
242 365
243 366 // Get data
244 367 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
245 - $html_content = isset($_POST['html']) ? wp_kses_post($_POST['html']) : '';
246 368
247 - if (!$post_id || empty($html_content)) {
248 - wp_send_json_error(array('message' => __('Invalid data', 'metasync')));
369 + if (!$post_id) {
370 + wp_send_json_error(array('message' => __('No page selected', 'metasync')));
249 371 }
250 372
373 + if (!current_user_can('edit_post', $post_id)) {
374 + wp_send_json_error(array('message' => __('Permission denied', 'metasync')), 403);
375 + }
376 +
377 + $html_content = isset($_POST['html']) ? wp_unslash($_POST['html']) : '';
378 +
379 + if (empty($html_content)) {
380 + wp_send_json_error(array('message' => __('Nothing to save', 'metasync')));
381 + }
382 +
383 + // Keep the value being replaced so a save that mangles the page can
384 + // be undone; postmeta is not revisioned, so this is the only undo.
385 + $previous = get_post_meta($post_id, '_metasync_raw_html_content', true);
386 + if ('' !== $previous) {
387 + update_post_meta($post_id, '_metasync_raw_html_content_previous', $previous);
388 + }
389 +
251 390 // Save HTML content
252 391 update_post_meta($post_id, '_metasync_raw_html_content', $html_content);
253 392
254 393 // Update modified date
@@ -265,8 +404,13 @@
265 404 }
266 405
267 406 /**
268 407 * AJAX handler for uploading images
408 + *
409 + * File validation is delegated entirely to the WordPress media pipeline
410 + * (wp_handle_upload + wp_check_filetype_and_ext via media_handle_upload),
411 + * the same chain the core media uploader uses; the capability gate
412 + * matches core's async-upload endpoint (upload_files).
269 413 */
270 414 public function ajax_upload_image()
271 415 {
272 416 // Check nonce
@@ -273,11 +417,17 @@
273 417 check_ajax_referer('metasync_html_editor', 'nonce');
274 418
275 419 // Check permissions
276 420 if (!current_user_can('upload_files')) {
277 - wp_send_json_error(array('message' => __('Permission denied', 'metasync')));
421 + wp_send_json_error(array('message' => __('Permission denied', 'metasync')), 403);
278 422 }
279 423
424 + // Tie the upload to the page being edited, when the editor names one.
425 + $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
426 + if ($post_id && !current_user_can('edit_post', $post_id)) {
427 + wp_send_json_error(array('message' => __('Permission denied', 'metasync')), 403);
428 + }
429 +
280 430 // Handle file upload
281 431 if (!isset($_FILES['file'])) {
282 432 wp_send_json_error(array('message' => __('No file uploaded', 'metasync')));
283 433 }
@@ -293,10 +443,15 @@
293 443 }
294 444
295 445 $image_url = wp_get_attachment_url($attachment_id);
296 446
447 + // The editor's asset manager adds response.data to the asset list
448 + // directly, and an asset's source attribute is called `src`. The
449 + // attachment is exposed as `attachment_id` rather than `id` — `id` is
450 + // the Backbone collection's identity key, so reusing it would make
451 + // repeated uploads of the same attachment silently dedupe.
297 452 wp_send_json_success(array(
298 - 'url' => $image_url,
299 - 'id' => $attachment_id
453 + 'src' => $image_url,
454 + 'attachment_id' => $attachment_id
300 455 ));
301 456 }
302 457 }