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
metasync / admin / class-metasync-html-visual-editor.php

class-metasync-html-visual-editor.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress trunk, at admin/class-metasync-html-visual-editor.php

458 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MetaSync HTML Visual Editor
4 *
5 * Provides a visual editing interface for raw HTML pages with:
6 * - Click to edit text
7 * - Color picker for backgrounds/colors
8 * - Image uploader
9 * - Drag and drop reordering
10 * - Live preview
11 *
12 * @package Metasync
13 * @subpackage Metasync/admin
14 * @since 2.0.0
15 */
16
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 class Metasync_HTML_Visual_Editor
22 {
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 /**
43 * Plugin name
44 *
45 * @var string
46 */
47 private $plugin_name;
48
49 /**
50 * Plugin version
51 *
52 * @var string
53 */
54 private $version;
55
56 /**
57 * Initialize the class
58 *
59 * @param string $plugin_name Plugin name
60 * @param string $version Plugin version
61 */
62 public function __construct($plugin_name, $version)
63 {
64 $this->plugin_name = $plugin_name;
65 $this->version = $version;
66 }
67
68 /**
69 * Register hooks
70 */
71 public function init()
72 {
73 // Add "Edit HTML" button to post row actions
74 add_filter('post_row_actions', array($this, 'add_edit_html_button'), 10, 2);
75 add_filter('page_row_actions', array($this, 'add_edit_html_button'), 10, 2);
76
77 // Add admin menu page for the editor
78 add_action('admin_menu', array($this, 'add_editor_page'));
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
84 // Register AJAX handlers
85 add_action('wp_ajax_metasync_save_html', array($this, 'ajax_save_html'));
86 add_action('wp_ajax_metasync_upload_image', array($this, 'ajax_upload_image'));
87 }
88
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 /**
122 * Add "Edit HTML" button to row actions
123 *
124 * @param array $actions Row actions
125 * @param WP_Post $post Post object
126 * @return array Modified actions
127 */
128 public function add_edit_html_button($actions, $post)
129 {
130 // Check if this is a raw HTML page
131 $has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true);
132
133 if ($has_raw_html) {
134 $edit_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-html-editor&post_id=' . $post->ID);
135 $label = Metasync::get_whitelabel_company_name() ?: 'SearchAtlas';
136
137 $actions['edit_html'] = sprintf(
138 '<a href="%s" title="%s">%s</a>',
139 esc_url($edit_url),
140 esc_attr(sprintf(__('Edit with %s Visual Editor', 'metasync'), $label)),
141 __('Edit HTML', 'metasync')
142 );
143 }
144
145 return $actions;
146 }
147
148 /**
149 * Add editor admin page
150 */
151 public function add_editor_page()
152 {
153 $label = Metasync::get_whitelabel_company_name() ?: 'SearchAtlas';
154 $page_title = sprintf(__('%s HTML Editor', 'metasync'), $label);
155
156 add_submenu_page(
157 '', // Hidden from menu
158 $page_title,
159 $page_title,
160 'edit_pages',
161 Metasync_Admin::$page_slug . '-html-editor',
162 array($this, 'render_editor_page')
163 );
164 }
165
166 /**
167 * Render the visual editor page
168 */
169 public function render_editor_page()
170 {
171 // Check permissions
172 if (!current_user_can('edit_pages')) {
173 wp_die(__('You do not have sufficient permissions to access this page.', 'metasync'));
174 }
175
176 // Get post ID
177 $post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
178
179 if (!$post_id) {
180 wp_die(__('Invalid page ID.', 'metasync'));
181 }
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
187 // Get post
188 $post = get_post($post_id);
189
190 if (!$post) {
191 wp_die(__('Page not found.', 'metasync'));
192 }
193
194 // Check if raw HTML is enabled
195 $has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true);
196
197 if (!$has_raw_html) {
198 wp_die(__('This page is not a raw HTML page.', 'metasync'));
199 }
200
201 // Get HTML content
202 $html_content = get_post_meta($post_id, '_metasync_raw_html_content', true);
203
204 if (empty($html_content)) {
205 $html_content = '<html><body><h1>Start editing...</h1></body></html>';
206 }
207
208 // Get label for branding
209 $label = Metasync::get_whitelabel_company_name() ?: 'SearchAtlas AI';
210
211 // Render editor UI. Assets are enqueued on admin_enqueue_scripts.
212 include plugin_dir_path(__FILE__) . 'partials/metasync-html-editor-page.php';
213 }
214
215 /**
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.
222 */
223 private function enqueue_editor_assets()
224 {
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 ),
243 );
244
245 $missing = array();
246
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 }
259
260 if (!isset($library['style'])) {
261 continue;
262 }
263
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
294 wp_enqueue_script(
295 'metasync-html-editor',
296 plugins_url('js/metasync-html-editor.js', __FILE__),
297 $editor_script_deps,
298 $this->version,
299 true
300 );
301
302 wp_enqueue_style(
303 'metasync-html-editor',
304 plugins_url('css/metasync-html-editor.css', __FILE__),
305 $editor_style_deps,
306 $this->version
307 );
308
309 // Localize script with data
310 wp_localize_script('metasync-html-editor', 'metasyncEditor', array(
311 'ajax_url' => admin_url('admin-ajax.php'),
312 'nonce' => wp_create_nonce('metasync_html_editor'),
313 'post_id' => isset($_GET['post_id']) ? intval($_GET['post_id']) : 0,
314 'preview_url' => get_permalink(isset($_GET['post_id']) ? intval($_GET['post_id']) : 0),
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,
319 'i18n' => array(
320 'saving' => __('Saving...', 'metasync'),
321 'saved' => __('Saved!', 'metasync'),
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'),
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'),
341 )
342 ));
343 }
344
345 /**
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.
355 */
356 public function ajax_save_html()
357 {
358 // Check nonce
359 check_ajax_referer('metasync_html_editor', 'nonce');
360
361 // Check permissions
362 if (!current_user_can('edit_pages')) {
363 wp_send_json_error(array('message' => __('Permission denied', 'metasync')), 403);
364 }
365
366 // Get data
367 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
368
369 if (!$post_id) {
370 wp_send_json_error(array('message' => __('No page selected', 'metasync')));
371 }
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
390 // Save HTML content
391 update_post_meta($post_id, '_metasync_raw_html_content', $html_content);
392
393 // Update modified date
394 wp_update_post(array(
395 'ID' => $post_id,
396 'post_modified' => current_time('mysql'),
397 'post_modified_gmt' => current_time('mysql', 1)
398 ));
399
400 wp_send_json_success(array(
401 'message' => __('Page saved successfully', 'metasync'),
402 'preview_url' => get_permalink($post_id)
403 ));
404 }
405
406 /**
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).
413 */
414 public function ajax_upload_image()
415 {
416 // Check nonce
417 check_ajax_referer('metasync_html_editor', 'nonce');
418
419 // Check permissions
420 if (!current_user_can('upload_files')) {
421 wp_send_json_error(array('message' => __('Permission denied', 'metasync')), 403);
422 }
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
430 // Handle file upload
431 if (!isset($_FILES['file'])) {
432 wp_send_json_error(array('message' => __('No file uploaded', 'metasync')));
433 }
434
435 require_once(ABSPATH . 'wp-admin/includes/image.php');
436 require_once(ABSPATH . 'wp-admin/includes/file.php');
437 require_once(ABSPATH . 'wp-admin/includes/media.php');
438
439 $attachment_id = media_handle_upload('file', 0);
440
441 if (is_wp_error($attachment_id)) {
442 wp_send_json_error(array('message' => $attachment_id->get_error_message()));
443 }
444
445 $image_url = wp_get_attachment_url($attachment_id);
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.
452 wp_send_json_success(array(
453 'src' => $image_url,
454 'attachment_id' => $attachment_id
455 ));
456 }
457 }
458