PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 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 All 141 releases
metasync / custom-pages / class-metasync-custom-pages.php

class-metasync-custom-pages.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at custom-pages/class-metasync-custom-pages.php

728 lines 27.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * Custom HTML Pages Handler
10 *
11 * Allows users to upload HTML files and serve them directly on WordPress
12 * without theme templating.
13 *
14 * @package Metasync
15 * @subpackage Metasync/custom-pages
16 * @since 1.0.0
17 */
18
19 class Metasync_Custom_Pages
20 {
21 /**
22 * Post type for custom HTML pages (using regular WordPress pages)
23 */
24 const POST_TYPE = 'page';
25
26 /**
27 * Meta key for storing raw HTML content
28 */
29 const META_HTML_CONTENT = '_metasync_raw_html_content';
30
31 /**
32 * Meta key for enabling raw HTML mode
33 */
34 const META_HTML_ENABLED = '_metasync_raw_html_enabled';
35
36 /**
37 * Meta key for storing original filename
38 */
39 const META_HTML_FILENAME = '_metasync_raw_html_filename';
40
41 /**
42 * Meta key to mark pages as custom HTML pages (to show meta box)
43 */
44 const META_IS_CUSTOM_HTML_PAGE = '_metasync_is_custom_html_page';
45
46 /**
47 * Meta key to mark pages as created via API
48 */
49 const META_CREATED_VIA_API = '_metasync_created_via_api';
50
51 /**
52 * Meta key to mark a page as an LPS (Landing Page Studio) ZIP import.
53 * This is the unique marker the importer checks before overwriting: only
54 * pages carrying this flag may be replaced by a re-import. Manual custom
55 * HTML pages and other API-created pages never carry it, so they are never
56 * touched by the LPS importer.
57 */
58 const META_LPS_IMPORT = '_metasync_lps_import';
59
60 /**
61 * Meta key storing the on-disk assets folder name a page's bundle was
62 * extracted to (under wp-content/uploads/metasync-pages/). Lets delete and
63 * future multi-page cleanup target the correct folder even when it differs
64 * from the page slug.
65 */
66 const META_ASSETS_FOLDER = '_metasync_lps_assets_folder';
67
68 /**
69 * Meta key marking a page as the LPS home page that is set as the site's
70 * static front page. Its value is the project's assets_folder, so a
71 * re-publish of the same project finds and updates its own home (instead of
72 * creating a duplicate), regardless of the page's slug.
73 */
74 const META_LPS_HOME = '_metasync_lps_home';
75
76 // Stable per-project LPS UUID; primary key for home-page dedup when external_ref is present.
77 const META_LPS_PROJECT_REF = '_metasync_lps_project_ref';
78
79 // Stable per-page manifest path (e.g. "blog/2026/launch"), written on every
80 // imported non-home page + auto-created parent. Combined with the project key
81 // (external_ref / assets_folder) it identifies a page across re-imports
82 // regardless of the slug WordPress actually stored — so a re-import updates the
83 // same page instead of creating duplicates when a slug was renamed.
84 const META_LPS_PAGE_PATH = '_metasync_lps_page_path';
85
86 /**
87 * Initialize the custom pages functionality
88 */
89 public function __construct()
90 {
91 // Add meta boxes for page editor (only on marked custom HTML pages)
92 add_action('add_meta_boxes', array($this, 'add_custom_html_meta_box'));
93
94 // Mark new pages as custom HTML pages when created from dashboard
95 add_action('load-post-new.php', array($this, 'mark_new_custom_html_page'));
96
97 // Save post meta
98 add_action('save_post', array($this, 'save_custom_html_meta'), 10, 2);
99
100 // Template redirect to serve raw HTML
101 add_action('template_redirect', array($this, 'serve_raw_html'), 5);
102
103 // Handle file uploads
104 add_action('wp_ajax_metasync_upload_html', array($this, 'handle_html_upload'));
105
106 // Add admin scripts and styles
107 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
108
109 // Clear cache on save
110 add_action('save_post', array($this, 'clear_page_cache'), 20, 2);
111
112 // Add custom column to pages list to identify HTML pages
113 add_filter('manage_pages_columns', array($this, 'add_custom_html_column'));
114 add_action('manage_pages_custom_column', array($this, 'render_custom_html_column'), 10, 2);
115
116 // Initialize REST API endpoints
117 $this->init_api();
118 }
119
120 /**
121 * Initialize REST API endpoints
122 */
123 private function init_api()
124 {
125 require_once plugin_dir_path(__FILE__) . 'class-metasync-custom-pages-api.php';
126 new Metasync_Custom_Pages_API();
127 }
128
129 /**
130 * Mark new pages as custom HTML pages when created from dashboard
131 */
132 public function mark_new_custom_html_page()
133 {
134 // Check if this is a page being created
135 if (isset($_GET['post_type']) && $_GET['post_type'] === 'page') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- core gates post-new.php, this only flags the new auto-draft
136 // Check if the metasync_html_page parameter is set
137 if (isset($_GET['metasync_html_page']) && $_GET['metasync_html_page'] === '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- core gates post-new.php, this only flags the new auto-draft
138 // Set a flag in session or add a filter to mark this page on first save
139 add_filter('wp_insert_post_data', array($this, 'flag_custom_html_page_on_insert'), 10, 2);
140 }
141 }
142 }
143
144 /**
145 * Flag to mark the page as custom HTML page on first insert
146 */
147 public function flag_custom_html_page_on_insert($data, $postarr)
148 {
149 // Only mark if this is a new page (no ID yet)
150 if (empty($postarr['ID'])) {
151 // We'll use a transient to mark this page needs the flag
152 // The transient will be checked in save_post
153 set_transient('metasync_mark_as_html_page_' . get_current_user_id(), true, 60);
154 }
155
156 return $data;
157 }
158
159 /**
160 * Add meta box for custom HTML upload/edit
161 * Only shows on pages marked as custom HTML pages or new pages being created
162 */
163 public function add_custom_html_meta_box()
164 {
165 // Don't show meta box if user's role doesn't have plugin access
166 if (!Metasync::current_user_has_plugin_access()) {
167 return;
168 }
169
170 // Get all pages
171 global $post;
172
173 // Check if this is a marked custom HTML page
174 $is_custom_html_page = get_post_meta($post->ID, self::META_IS_CUSTOM_HTML_PAGE, true) === '1';
175
176 // OR check if this is a new page being created from dashboard (transient set)
177 $transient_key = 'metasync_mark_as_html_page_' . get_current_user_id();
178 $is_new_custom_page = get_transient($transient_key) ? true : false;
179
180 // Show meta box if either condition is true
181 if ($post && ($is_custom_html_page || $is_new_custom_page)) {
182 add_meta_box(
183 'metasync_custom_html',
184 'Custom HTML Settings',
185 array($this, 'render_custom_html_meta_box'),
186 'page',
187 'normal',
188 'high'
189 );
190 }
191 }
192
193 /**
194 * Render the custom HTML meta box
195 */
196 public function render_custom_html_meta_box($post)
197 {
198 // Add nonce for security
199 wp_nonce_field('metasync_custom_html_nonce', 'metasync_custom_html_nonce');
200
201 // Get current values
202 $html_enabled = get_post_meta($post->ID, self::META_HTML_ENABLED, true);
203 $html_content = get_post_meta($post->ID, self::META_HTML_CONTENT, true);
204 $html_filename = get_post_meta($post->ID, self::META_HTML_FILENAME, true);
205
206 // Debug: Check if content exists
207 $content_length = strlen($html_content);
208 $has_content = !empty($html_content);
209
210 ?>
211 <div class="metasync-custom-html-container">
212 <div class="metasync-html-mode">
213 <label>
214 <input type="checkbox"
215 name="metasync_html_enabled"
216 id="metasync_html_enabled"
217 value="1"
218 <?php checked($html_enabled, '1'); ?> />
219 <strong>Enable Raw HTML Mode</strong>
220 <span class="description">(When enabled, the page will display your custom HTML without theme styling)</span>
221 </label>
222 </div>
223
224 <div id="metasync-html-controls" style="<?php echo $html_enabled ? '' : 'display:none;'; ?>">
225 <hr style="margin: 20px 0;">
226
227 <div class="metasync-html-upload">
228 <h3>Upload HTML File</h3>
229 <p class="description">Upload a complete HTML file (including &lt;html&gt;, &lt;head&gt;, and &lt;body&gt; tags)</p>
230
231 <input type="file"
232 name="metasync_html_file"
233 id="metasync_html_file"
234 accept=".html,.htm"
235 class="metasync-file-input" />
236
237 <?php if ($html_filename): ?>
238 <p class="current-file">
239 <strong>Current file:</strong> <?php echo esc_html($html_filename); ?>
240 </p>
241 <?php endif; ?>
242 </div>
243
244 <div class="metasync-html-editor" style="margin-top: 20px;">
245 <h3>Or Edit HTML Directly</h3>
246 <p class="description">Edit your HTML code directly in the editor below</p>
247
248 <?php if ($has_content): ?>
249 <p style="padding: 10px; background: #d7f1e0; border-left: 4px solid #00a32a; margin-bottom: 10px;">
250 �
251 <strong>HTML Content Loaded:</strong> <?php echo number_format($content_length); ?> characters
252 </p>
253 <?php else: ?>
254 <p style="padding: 10px; background: #fff3cd; border-left: 4px solid #ffc107; margin-bottom: 10px;">
255 ℹ️ <strong>Tip:</strong> Upload an HTML file above or paste your HTML code in the editor below.
256 </p>
257 <?php endif; ?>
258
259 <textarea name="metasync_html_content"
260 id="metasync_html_content"
261 class="large-text code"
262 rows="20"
263 style="font-family: Consolas, Monaco, monospace; width: 100%;"><?php
264 // Output HTML content - don't use esc_textarea as it might break large HTML
265 echo htmlspecialchars($html_content, ENT_QUOTES, 'UTF-8'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- htmlspecialchars(ENT_QUOTES) is the escaping call (esc_textarea equivalent for large HTML)
266 ?></textarea>
267 </div>
268
269 <div class="metasync-html-preview" style="margin-top: 15px;">
270 <button type="button" class="button button-secondary" id="metasync-preview-html">
271 Preview HTML
272 </button>
273 <span class="description">Preview your custom HTML page in a new window</span>
274 </div>
275 </div>
276
277 </div>
278 <?php
279 }
280
281 /**
282 * Save custom HTML meta data
283 */
284 public function save_custom_html_meta($post_id, $post)
285 {
286 // Check if post object is valid and has post_type property
287 if (!is_object($post) || !isset($post->post_type)) {
288 return;
289 }
290
291 // Check if this is a page
292 if ('page' !== $post->post_type) {
293 return;
294 }
295
296 // Check if this is a new page created from Custom Pages dashboard
297 $transient_key = 'metasync_mark_as_html_page_' . get_current_user_id();
298 if (get_transient($transient_key)) {
299 // Mark this page as a custom HTML page
300 update_post_meta($post_id, self::META_IS_CUSTOM_HTML_PAGE, '1');
301 // Delete the transient
302 delete_transient($transient_key);
303 // Auto-enable HTML mode for new custom HTML pages
304 update_post_meta($post_id, self::META_HTML_ENABLED, '1');
305 }
306
307 // Only process HTML settings if this is a marked custom HTML page
308 if (get_post_meta($post_id, self::META_IS_CUSTOM_HTML_PAGE, true) !== '1') {
309 return;
310 }
311
312 // Verify nonce
313 if (!isset($_POST['metasync_custom_html_nonce']) ||
314 !wp_verify_nonce($_POST['metasync_custom_html_nonce'], 'metasync_custom_html_nonce')) {
315 return;
316 }
317
318 // Check autosave
319 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
320 return;
321 }
322
323 // Check permissions
324 if (!current_user_can('edit_post', $post_id)) {
325 return;
326 }
327
328 // Save HTML enabled status
329 $html_enabled = isset($_POST['metasync_html_enabled']) ? '1' : '0';
330 update_post_meta($post_id, self::META_HTML_ENABLED, $html_enabled);
331
332 // Save HTML content
333 if (isset($_POST['metasync_html_content'])) {
334 // Don't sanitize HTML content - we trust admin users
335 $html_content = wp_unslash($_POST['metasync_html_content']);
336 update_post_meta($post_id, self::META_HTML_CONTENT, $html_content);
337 }
338
339 // Handle file upload
340 if (isset($_FILES['metasync_html_file']) && $_FILES['metasync_html_file']['size'] > 0) {
341 $file = $_FILES['metasync_html_file'];
342
343 // Validate file type
344 $allowed_extensions = array('html', 'htm');
345 $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
346
347 if (in_array($file_extension, $allowed_extensions)) {
348 $html_content = file_get_contents($file['tmp_name']);
349
350 if ($html_content !== false && !empty($html_content)) {
351 update_post_meta($post_id, self::META_HTML_CONTENT, $html_content);
352 update_post_meta($post_id, self::META_HTML_FILENAME, sanitize_file_name($file['name']));
353
354 // Set admin notice
355 add_action('admin_notices', function() use ($file) {
356 echo '<div class="notice notice-success is-dismissible">';
357 echo '<p><strong>Success!</strong> HTML file "' . esc_html($file['name']) . '" uploaded and saved.</p>';
358 echo '</div>';
359 });
360 } else {
361 // File was empty or couldn't be read
362 add_action('admin_notices', function() use ($file) {
363 echo '<div class="notice notice-error is-dismissible">';
364 echo '<p><strong>Error:</strong> Could not read HTML file "' . esc_html($file['name']) . '". Please try again.</p>';
365 echo '</div>';
366 });
367 }
368 } else {
369 // Invalid file type
370 add_action('admin_notices', function() use ($file) {
371 echo '<div class="notice notice-error is-dismissible">';
372 echo '<p><strong>Error:</strong> Invalid file type. Only .html and .htm files are allowed.</p>';
373 echo '</div>';
374 });
375 }
376 }
377 }
378
379 /**
380 * Serve raw HTML content instead of WordPress template
381 *
382 * This function intercepts the WordPress template loading process and serves
383 * raw HTML content directly. It's fully compatible with OTTO, which fetches
384 * the raw HTML, enhances it with SEO metadata, and serves the optimized version.
385 *
386 * @since 1.0.0
387 */
388 public function serve_raw_html()
389 {
390 // Only process single page requests
391 if (!is_page()) {
392 return;
393 }
394
395 global $post;
396
397 // Verify post object exists
398 if (!$post || !isset($post->ID)) {
399 return;
400 }
401
402 // Check if raw HTML mode is enabled for this page
403 $html_enabled = get_post_meta($post->ID, self::META_HTML_ENABLED, true);
404
405 if ($html_enabled !== '1') {
406 return; // Use normal WordPress template
407 }
408
409 /**
410 * OTTO COMPATIBILITY:
411 *
412 * When OTTO is enabled and processing a page, it:
413 * 1. Makes an internal wp_remote_get() request with ?is_otto_page_fetch=1
414 * 2. Receives the raw HTML from this function
415 * 3. Processes it (adds meta tags, schema, AI enhancements)
416 * 4. Serves the enhanced HTML to the end-user
417 *
418 * This ensures both custom HTML design AND OTTO's SEO features work together.
419 */
420 # Same three-signal detection as the render strategy: a redirect that
421 # rebuilds the URL strips the query parameter, and the followed hop would
422 # otherwise be mistaken for a visitor request and emit cache headers into
423 # OTTO's internal fetch.
424 # @phpstan-ignore-next-line function.alreadyNarrowedType
425 $has_fetch_detector = class_exists('Metasync_Otto_Render_Strategy') && method_exists('Metasync_Otto_Render_Strategy', 'is_internal_fetch');
426
427 $is_otto_fetch = $has_fetch_detector
428 ? Metasync_Otto_Render_Strategy::is_internal_fetch()
429 : (isset($_GET['is_otto_page_fetch']) && $_GET['is_otto_page_fetch'] === '1'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only per-request flag to skip cache headers
430
431 // Permission check for draft/pending pages
432 if ($post->post_status !== 'publish') {
433 // Allow preview for users with edit permissions
434 if (!current_user_can('edit_post', $post->ID)) {
435 wp_die(
436 esc_html__('You do not have permission to view this page.', 'metasync'),
437 403
438 );
439 }
440 }
441
442 // Retrieve HTML content
443 $html_content = get_post_meta($post->ID, self::META_HTML_CONTENT, true);
444
445 // Handle empty content gracefully
446 if (empty($html_content)) {
447 $message = current_user_can('edit_post', $post->ID)
448 ? 'No HTML content available for this page. Please edit the page and add HTML content.'
449 : 'This page is not yet available.';
450
451 wp_die(
452 esc_html($message),
453 404
454 );
455 }
456
457 // Set proper headers for HTML content
458 header('Content-Type: text/html; charset=utf-8');
459
460 // Cache control headers (prevent caching of preview/draft pages).
461 //
462 // Nothing is emitted on OTTO's internal fetch: that response is consumed by the
463 // plugin itself and is never served to a visitor or stored under the public URL,
464 // so a cache header there is meaningless. It is also actively harmful now — the
465 // HTTP render path reads the internal response's Cache-Control as a "this page
466 // declared itself uncacheable" signal, so emitting one here would mark every
467 // custom page uncacheable and remove host caching for the whole feature.
468 if (!$is_otto_fetch) {
469 if ($post->post_status !== 'publish') {
470 header('Cache-Control: no-cache, must-revalidate, max-age=0');
471 header('Pragma: no-cache');
472 } else {
473 // Allow caching for published pages (CDN friendly)
474 header('Cache-Control: private, max-age=3600');
475 }
476 }
477
478 // Output raw HTML and stop WordPress processing
479 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw HTML is intentional
480 echo $html_content;
481 exit;
482 }
483
484 /**
485 * Handle AJAX HTML file upload
486 *
487 * Validates and processes HTML file uploads via AJAX
488 *
489 * @since 1.0.0
490 */
491 public function handle_html_upload()
492 {
493 // Verify nonce for security
494 check_ajax_referer('metasync_upload_html', 'nonce');
495
496 // Check user permissions
497 if (!current_user_can('edit_posts')) {
498 wp_send_json_error(array(
499 'message' => esc_html__('Insufficient permissions to upload files.', 'metasync')
500 ));
501 }
502
503 // Uploading raw HTML requires the unfiltered_html capability
504 if (!current_user_can('unfiltered_html')) {
505 wp_send_json_error(array(
506 'message' => esc_html__('You do not have permission to upload unfiltered HTML content.', 'metasync')
507 ));
508 }
509
510 // Verify file was uploaded
511 if (!isset($_FILES['html_file']) || empty($_FILES['html_file']['tmp_name'])) {
512 wp_send_json_error(array(
513 'message' => esc_html__('No file uploaded. Please select an HTML file.', 'metasync')
514 ));
515 }
516
517 $file = $_FILES['html_file'];
518
519 // Validate file type
520 $allowed_extensions = array('html', 'htm');
521 $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
522
523 if (!in_array($file_ext, $allowed_extensions, true)) {
524 wp_send_json_error(array(
525 'message' => sprintf(
526 /* translators: %s: list of allowed file extensions. */
527 esc_html__('Invalid file type. Only %s files are allowed.', 'metasync'),
528 implode(', ', $allowed_extensions)
529 )
530 ));
531 }
532
533 // Check file size (max 10MB)
534 $max_size = 10 * 1024 * 1024; // 10MB in bytes
535 if ($file['size'] > $max_size) {
536 wp_send_json_error(array(
537 'message' => esc_html__('File is too large. Maximum size is 10MB.', 'metasync')
538 ));
539 }
540
541 // Validate upload error
542 if ($file['error'] !== UPLOAD_ERR_OK) {
543 wp_send_json_error(array(
544 'message' => esc_html__('File upload failed. Please try again.', 'metasync')
545 ));
546 }
547
548 // Read file contents
549 $html_content = @file_get_contents($file['tmp_name']);
550
551 if ($html_content === false || empty($html_content)) {
552 wp_send_json_error(array(
553 'message' => esc_html__('Could not read file contents. Please verify the file is valid.', 'metasync')
554 ));
555 }
556
557 // Clean up temp file
558 @wp_delete_file($file['tmp_name']);
559
560 // Return success with content
561 wp_send_json_success(array(
562 'content' => $html_content,
563 'filename' => sanitize_file_name($file['name']),
564 'size' => size_format($file['size'])
565 ));
566 }
567
568 /**
569 * Enqueue admin assets
570 */
571 public function enqueue_admin_assets($hook)
572 {
573 global $post_type, $post;
574
575 if ('page' !== $post_type) {
576 return;
577 }
578
579 // Only enqueue on custom HTML pages
580 if ($post) {
581 $is_custom_html_page = get_post_meta($post->ID, self::META_IS_CUSTOM_HTML_PAGE, true) === '1';
582 $transient_key = 'metasync_mark_as_html_page_' . get_current_user_id();
583 $is_new_custom_page = get_transient($transient_key) ? true : false;
584
585 if (!$is_custom_html_page && !$is_new_custom_page) {
586 return; // Not a custom HTML page, don't load assets
587 }
588 }
589
590 // Enqueue CodeMirror for syntax highlighting
591 $cm_settings = wp_enqueue_code_editor(array('type' => 'text/html'));
592
593 // Check if CodeMirror loaded successfully
594 if ($cm_settings === false) {
595 return;
596 }
597
598 // Enqueue custom scripts for enhanced editor
599 wp_enqueue_script(
600 'metasync-custom-pages-editor',
601 plugin_dir_url(__FILE__) . 'js/custom-pages-editor.js',
602 array('jquery', 'code-editor'),
603 '1.0.2', // Fixed: CodeMirror now syncs content to textarea before save
604 true
605 );
606
607 // Pass CodeMirror settings to our script
608 wp_localize_script(
609 'metasync-custom-pages-editor',
610 'metasyncCustomPagesSettings',
611 array(
612 'codeEditorSettings' => $cm_settings,
613 'ajaxUrl' => admin_url('admin-ajax.php'),
614 'nonce' => wp_create_nonce('metasync_upload_html')
615 )
616 );
617
618 // Enqueue custom styles
619 wp_enqueue_style(
620 'metasync-custom-pages-editor',
621 plugin_dir_url(__FILE__) . 'css/custom-pages-editor.css',
622 array(),
623 '1.0.0'
624 );
625 }
626
627 /**
628 * Clear page cache when saving
629 */
630 public function clear_page_cache($post_id, $post)
631 {
632 if ('page' !== $post->post_type) {
633 return;
634 }
635
636 // Clear WordPress object cache
637 wp_cache_delete($post_id, 'posts');
638 wp_cache_delete($post_id, 'post_meta');
639
640 // Try to clear popular caching plugins
641 if (function_exists('wp_cache_clear_cache')) {
642 wp_cache_clear_cache();
643 }
644
645 if (function_exists('w3tc_flush_post')) {
646 w3tc_flush_post($post_id);
647 }
648
649 if (function_exists('wp_super_cache_post_delete')) {
650 wp_super_cache_post_delete($post_id);
651 }
652
653 // Clear cache plugins
654 Metasync_Cache_Purge::purge_all('custom_page_update');
655 }
656
657 /**
658 * Get all custom HTML pages (pages marked as custom HTML pages)
659 */
660 public static function get_custom_pages($args = array())
661 {
662 $defaults = array(
663 'post_type' => 'page',
664 'post_status' => array('publish', 'draft', 'pending'),
665 'posts_per_page' => -1,
666 'orderby' => 'title',
667 'order' => 'ASC',
668 'meta_query' => array(
669 array(
670 'key' => self::META_IS_CUSTOM_HTML_PAGE,
671 'value' => '1',
672 'compare' => '='
673 )
674 )
675 );
676
677 $args = wp_parse_args($args, $defaults);
678 return get_posts($args);
679 }
680
681 /**
682 * Add custom column to pages list
683 */
684 public function add_custom_html_column($columns)
685 {
686 $new_columns = array();
687
688 foreach ($columns as $key => $value) {
689 $new_columns[$key] = $value;
690
691 // Add our column after the title
692 if ($key === 'title') {
693 $new_columns['metasync_html'] = '🌐 HTML Mode';
694 }
695 }
696
697 return $new_columns;
698 }
699
700 /**
701 * Render custom column content
702 */
703 public function render_custom_html_column($column, $post_id)
704 {
705 if ($column === 'metasync_html') {
706 $is_custom_html_page = get_post_meta($post_id, self::META_IS_CUSTOM_HTML_PAGE, true);
707 $html_enabled = get_post_meta($post_id, self::META_HTML_ENABLED, true);
708
709 if ($is_custom_html_page === '1' && $html_enabled === '1') {
710 echo '<span style="display:inline-block;padding:3px 8px;background:#2271b1;color:#fff;border-radius:3px;font-size:11px;font-weight:600;">RAW HTML</span>';
711 } elseif ($is_custom_html_page === '1') {
712 echo '<span style="display:inline-block;padding:3px 8px;background:#999;color:#fff;border-radius:3px;font-size:11px;font-weight:600;">HTML PAGE</span>';
713 } else {
714 echo '<span style="color:#999;">—</span>';
715 }
716 }
717 }
718
719 /**
720 * Get custom page URL
721 */
722 public static function get_page_url($post_id)
723 {
724 return get_permalink($post_id);
725 }
726 }
727
728