plugin_name = $plugin_name; $this->version = $version; } /** * Register hooks */ public function init() { // Add "Edit HTML" button to post row actions add_filter('post_row_actions', array($this, 'add_edit_html_button'), 10, 2); add_filter('page_row_actions', array($this, 'add_edit_html_button'), 10, 2); // Add admin menu page for the editor add_action('admin_menu', array($this, 'add_editor_page')); // Enqueue editor assets during the normal asset phase so stylesheets // land in
rather than being flushed late from the page body. add_action('admin_enqueue_scripts', array($this, 'maybe_enqueue_editor_assets')); // Register AJAX handlers add_action('wp_ajax_metasync_save_html', array($this, 'ajax_save_html')); add_action('wp_ajax_metasync_upload_image', array($this, 'ajax_upload_image')); } /** * Admin page slug for the visual editor. * * @return string */ private function get_editor_page_slug() { return Metasync_Admin::$page_slug . '-html-editor'; } /** * Enqueue the editor assets when the current request is the editor page. * * Keyed on the request rather than on the hook suffix because the editor is * registered as a hidden submenu page, so its generated suffix is not * stable to match against. */ public function maybe_enqueue_editor_assets() { $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to enqueue editor assets if ($page !== $this->get_editor_page_slug()) { return; } if (!current_user_can('edit_pages')) { return; } $this->enqueue_editor_assets(); } /** * Whether a page is a Landing Page Studio import. * * LPS pages are owned by the importer: a re-import replaces their stored * HTML wholesale, and they are always complete documents. Visual editing * them would both destroy the document and put the page out of sync with * the project it was imported from, so they are excluded from this editor * entirely rather than merely blocked at save. * * @param int $post_id Page being inspected. * @return bool */ public static function is_lps_page($post_id) { return get_post_meta($post_id, self::META_LPS_IMPORT, true) === '1'; } /** * Whether stored HTML carries content the visual canvas cannot round-trip. * * GrapesJS builds body fragments. Handing it anything document-level makes * it parse what it can and discard the rest, and the editor then saves that * reduced result over the original — so the loss happens when the page is * parsed into the canvas, before the user edits anything. * * Two classes of content are unsafe: * * - Document structure (doctype, , ): dropped outright, * because a fragment builder has nowhere to put it. * - Scripts: dropped even inside an otherwise ordinary body fragment, so a * check limited to document structure would still let a scripted page * lose its behaviour silently. * * is the exception that has to be judged on its attributes. The * canvas emits a bare wrapper around everything it serializes, so * treating the element itself as unsafe would make every page this editor * saves lock itself out of the editor on the next visit. What the canvas * does destroy is the element's attributes — a class, inline style, a data * attribute a script or stylesheet depends on — so an opening tag * is unsafe when it carries any. * * With one exception of its own: styling the body in the Styles panel * makes GrapesJS mint an id for it (``) purely so its * generated stylesheet has something to target. That id is the editor's * own bookkeeping, not authored content, and refusing it would lock a page * out of the editor the first time anyone edited a style — the very thing * the bare-wrapper allowance exists to prevent. * * Detection is deliberately a scan for opening tags rather than a parse: * the question is only whether unsupported constructs are present, and a * malformed document must answer yes as readily as a well-formed one. * * @param mixed $html Stored page HTML. Typed loosely because post meta can * come back as false when absent, or as an array from a * corrupted row, and neither is an unsafe document. * @return bool True when the document must not be edited visually. */ public static function is_unsafe_for_visual_editor($html) { if (!is_string($html) || '' === trim($html)) { return false; } // Opening tags only: a stray "" without its opening tag is not // evidence of a document, and matching bare words would misfire on // ordinary prose ("the html spec"). $patterns = array( '/]/i', '/]/i', '/