PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
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
← All changes | includes/class-metasync-settings-fields.php +440 -551 2.6.3 → trunk View file →
@@ -46,9 +46,23 @@
46 46 // Accordion / Section helpers
47 47 // ────────────────────────────────────────────────────────────────
48 48
49 49 public function get_accordion_sections_config() {
50 - return array(
50 + // Consent switch for writing into other SEO plugins. Loaded here rather
51 + // than through the committed classmap so a stale autoload map can never
52 + // make the switch — and with it the user's only way to stop those
53 + // writes — silently disappear from the page.
54 + require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-seo-sync-settings.php';
55 +
56 + $sections = array(
57 + 'analytics' => array(
58 + 'title' => 'Usage Data & Product Improvement',
59 + 'description' => 'Choose whether to share limited usage data to help improve MetaSync',
60 + 'icon' => 'chart-bar',
61 + 'priority' => 70,
62 + 'default_open' => false,
63 + 'render_callback' => array($this, 'render_analytics_section'),
64 + ),
51 65 'connection' => array(
52 66 'title' => 'Connection & Authentication',
53 67 'description' => 'Manage your API connection and authentication settings',
54 68 'icon' => 'admin-network',
@@ -68,9 +82,9 @@
68 82 'fields' => array(
69 83 'otto_pixel_uuid',
70 84 'otto_disable_on_loggedin',
71 85 'otto_disable_preview_button',
72 - 'otto_wp_rocket_compat',
86 + 'seo_priority',
73 87 )
74 88 ),
75 89 'edge_cache' => array(
76 90 'title' => 'Edge Cache / CDN',
@@ -79,8 +93,16 @@
79 93 'priority' => 22,
80 94 'default_open' => false,
81 95 'render_callback' => array('Metasync_Edge_Cache_Settings', 'render'),
82 96 ),
97 + 'seo_plugin_sync' => array(
98 + 'title' => 'Sync to Other SEO Plugins',
99 + 'description' => 'Control whether ' . Metasync::get_whitelabel_otto_name() . '\'s SEO values are written into Yoast, Rank Math or All in One SEO',
100 + 'icon' => 'update',
101 + 'priority' => 23,
102 + 'default_open' => false,
103 + 'render_callback' => array('Metasync_Seo_Sync_Settings', 'render'),
104 + ),
83 105 'bot_detection' => array(
84 106 'title' => 'Bot Detection & Filtering',
85 107 'description' => 'Manage bot traffic and reduce unnecessary API calls',
86 108 'icon' => 'filter',
@@ -92,16 +114,8 @@
92 114 'otto_bot_blacklist',
93 115 'otto_bot_statistics_link'
94 116 )
95 117 ),
96 - 'breadcrumbs' => array(
97 - 'title' => 'Breadcrumbs',
98 - 'description' => 'Configure breadcrumb navigation display',
99 - 'icon' => 'format-links',
100 - 'priority' => 35,
101 - 'default_open' => false,
102 - 'render_callback' => array($this, 'render_breadcrumbs_section')
103 - ),
104 118 'open_graph' => array(
105 119 'title' => 'Open Graph',
106 120 'description' => 'Control which Open Graph and article meta tags are output',
107 121 'icon' => 'share',
@@ -128,8 +142,10 @@
128 142 'disable_redirection_metabox',
129 143 'disable_canonical_metabox',
130 144 'disable_social_opengraph_metabox',
131 145 'disable_schema_markup_metabox',
146 + 'disable_language_alternates_metabox',
147 + 'disable_seo_metabox',
132 148 'open_external_links'
133 149 )
134 150 ),
135 151 'user_management' => array(
@@ -169,13 +185,22 @@
169 185 'fields' => array(
170 186 'permalink_structure',
171 187 'hide_dashboard_framework',
172 188 'show_admin_bar_status',
173 - 'enable_auto_updates',
174 189 'import_external_data'
175 190 )
176 191 )
177 192 );
193 +
194 + // Hidden only when there is nothing to say: no SEO plugin installed, the
195 + // switch off, and no saved originals. It is never hidden by the
196 + // access-control or whitelabel flags that can conceal Advanced Settings
197 + // — a switch that rewrites another plugin's data must stay reachable.
198 + if (!Metasync_Seo_Sync_Settings::should_display()) {
199 + unset($sections['seo_plugin_sync']);
200 + }
201 +
202 + return $sections;
178 203 }
179 204
180 205 public function get_advanced_accordion_config() {
181 206 $config = array();
@@ -200,8 +225,17 @@
200 225 'default_open' => false,
201 226 'render_callback' => array($this->admin_instance, 'render_debug_mode_section')
202 227 );
203 228
229 + $config['headless_mode'] = array(
230 + 'title' => 'Headless Mode',
231 + 'description' => 'Connect WordPress content to a separate public frontend',
232 + 'icon' => 'admin-site-alt3',
233 + 'priority' => 29,
234 + 'default_open' => false,
235 + 'render_callback' => array(Metasync_Settings_Registration::instance(), 'render_headless_mode_section')
236 + );
237 +
204 238 $config['error_logs'] = array(
205 239 'title' => 'Error Logs',
206 240 'description' => 'View and manage error logs to troubleshoot issues',
207 241 'icon' => 'warning',
@@ -274,9 +308,9 @@
274 308 $content_state = $is_open ? 'open' : 'closed';
275 309
276 310 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
277 311
278 - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
312 + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
279 313 echo '<div class="metasync-accordion-title">';
280 314 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
281 315 echo '<div class="metasync-accordion-text">';
282 316 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -287,9 +321,9 @@
287 321 echo '<span class="toggle-icon">▼</span>';
288 322 echo '</button>';
289 323 echo '</div>';
290 324
291 - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
325 + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
292 326
293 327 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
294 328 call_user_func($section_data['render_callback']);
295 329 }
@@ -311,14 +345,15 @@
311 345 </h4>
312 346 <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">This action will permanently delete:</p>
313 347 <ul style="color: var(--dashboard-text-secondary); margin: 0 0 12px 20px; line-height: 1.8;">
314 348 <li>All API keys and authentication tokens</li>
315 - <li>White label branding settings</li>
316 349 <li>Plugin configuration and preferences</li>
317 350 <li>Instant indexing settings</li>
351 + <li>Media optimization settings and cache</li>
352 + <li>Code minification settings</li>
318 353 <li>All cached data and crawl information</li>
319 354 </ul>
320 - <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin completely after this reset.</p>
355 + <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin's operational settings after this reset.</p>
321 356 </div>
322 357 <form method="post" action="" onsubmit="return confirmClearSettings(event)">
323 358 <?php wp_nonce_field('metasync_clear_all_settings_nonce', 'clear_all_settings_nonce'); ?>
324 359 <input type="hidden" name="clear_all_settings" value="yes" />
@@ -326,8 +361,31 @@
326 361 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear All Settings
327 362 </button>
328 363 </form>
329 364 </div>
365 + <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px; margin-top: 20px;">
366 + <h4 style="color: var(--dashboard-text-primary, #1e1e1e); margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
367 + <span class="dashicons dashicons-universal-access-alt" style="font-size:18px;width:18px;height:18px;"></span>
368 + <span>Delete data on uninstall</span>
369 + </h4>
370 + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
371 + By default, deleting the plugin keeps your data: sitemap and redirection settings, per-post SEO values, and the plugin's database tables stay in place in case you reinstall. Enable this to also remove all of that when the plugin itself is deleted from the Plugins screen.
372 + </p>
373 + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
374 + The Google service account and the IndexNow API key are always deleted on uninstall, regardless of this setting.
375 + </p>
376 + <form method="post" action="">
377 + <?php wp_nonce_field('metasync_uninstall_data_toggle_nonce', 'uninstall_data_toggle_nonce'); ?>
378 + <input type="hidden" name="metasync_uninstall_data_toggle_form" value="1" />
379 + <label style="color: var(--dashboard-text-primary, #1e1e1e); display: flex; align-items: center; gap: 8px; margin: 0 0 12px 0; cursor: pointer;">
380 + <input type="checkbox" name="metasync_delete_data_on_uninstall" value="yes" <?php checked(get_option('metasync_delete_data_on_uninstall', 'no'), 'yes'); ?> />
381 + Delete all plugin data (tables, settings, per-post SEO values, logs) when the plugin is uninstalled
382 + </label>
383 + <button type="submit" class="button button-secondary">
384 + Save Uninstall Setting
385 + </button>
386 + </form>
387 + </div>
330 388 <script>
331 389 function confirmClearSettings(event) {
332 390 event.preventDefault();
333 391
@@ -335,9 +393,9 @@
335 393 if (!firstConfirm) {
336 394 return false;
337 395 }
338 396
339 - var secondConfirm = confirm("🚨 FINAL WARNING 🚨\n\nThis will delete:\n• All API keys and authentication tokens\n• White label branding settings\n• Plugin configuration and preferences\n• Instant indexing settings\n• All cached data\n\nYou will need to reconfigure the entire plugin from scratch.\n\nType 'DELETE' in the next prompt to confirm.");
397 + var secondConfirm = confirm("🚨 FINAL WARNING 🚨\n\nThis will delete:\n• All API keys and authentication tokens\n• Plugin configuration and preferences\n• Instant indexing settings\n• Media optimization settings and cache\n• Code minification settings\n• All cached data\n\nYou will need to reconfigure the plugin's operational settings from scratch.\n\nType 'DELETE' in the next prompt to confirm.");
340 398 if (!secondConfirm) {
341 399 return false;
342 400 }
343 401
@@ -355,12 +413,21 @@
355 413 }
356 414
357 415 public function render_google_index_section() {
358 416 if (!function_exists('google_index_direct')) {
359 - require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
417 + if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
418 + require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
419 + } else {
420 + error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
421 + return;
422 + }
360 423 }
361 424
362 425 $google_index = google_index_direct();
426 + if (!$google_index) {
427 + // Nothing to render against, and calling through would be fatal.
428 + return;
429 + }
363 430 $service_info = $google_index->get_service_account_info();
364 431 $is_configured = !isset($service_info['error']);
365 432
366 433 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
@@ -437,8 +504,9 @@
437 504 <table class="form-table" style="margin-top: 0;">
438 505 <tr>
439 506 <th scope="row" style="width: 200px; padding-top: 15px;">
440 507 <label for="metasync_bing_api_key_inline">IndexNow API Key <span style="color: #d63638;">*</span></label>
508 + <?php Metasync::render_tooltip_icon('metasync_bing_api_key_inline', 'This key lets Bing trust indexing requests from your site. Click \'Generate Random API Key\' if you don\'t have one — the plugin publishes the matching verification file automatically, no manual upload needed.'); ?>
441 509 </th>
442 510 <td style="padding-top: 15px;">
443 511 <input type="text"
444 512 name="metasync_bing_api_key_inline"
@@ -638,9 +706,9 @@
638 706 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
639 707 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
640 708 </p>
641 709
642 - <form method="post" action="<?php echo admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced'); ?>" id="plugin-access-roles-form">
710 + <form method="post" action="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced')); ?>" id="plugin-access-roles-form">
643 711 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
644 712 <input type="hidden" name="save_plugin_access_roles" value="yes" />
645 713
646 714 <div class="metasync-role-selector-container" style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 16px; max-height: 300px; overflow-y: auto;">
@@ -732,8 +800,9 @@
732 800 'max_memory_limit' => 256,
733 801 'log_batch_size' => 1000,
734 802 'action_scheduler_batches' => 1,
735 803 'otto_rate_limit' => 10,
804 + 'otto_cache_ttl' => 30,
736 805 'queue_cleanup_days' => 31
737 806 );
738 807 }
739 808
@@ -757,9 +826,9 @@
757 826
758 827 public function can_change_memory_limit() {
759 828 $current_limit = ini_get('memory_limit');
760 829
761 - $test_result = @ini_set('memory_limit', $current_limit);
830 + $test_result = @ini_set('memory_limit', $current_limit); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
762 831
763 832 return $test_result !== false;
764 833 }
765 834
@@ -765,37 +834,47 @@
765 834
766 835 public function get_server_limits() {
767 836 $max_execution_time = ini_get('max_execution_time');
768 837 $memory_limit = ini_get('memory_limit');
769 -
838 +
839 + // Prefer the server-configured value: WordPress (wp_raise_memory_limit)
840 + // or this plugin (apply_memory_limit) may have already changed the
841 + // runtime limit via ini_set, so ini_get no longer reflects the server
842 + // configuration. get_cfg_var reads the php.ini value untouched.
843 + $config_memory_limit = get_cfg_var('memory_limit');
844 + if ($config_memory_limit !== false && $config_memory_limit !== '') {
845 + $memory_limit = $config_memory_limit;
846 + }
847 +
770 848 $memory_limit_mb = $this->parse_memory_limit_to_mb($memory_limit);
771 -
772 - $wp_memory_limit = null;
773 - if (defined('WP_MAX_MEMORY_LIMIT')) {
774 - $wp_memory_limit = WP_MAX_MEMORY_LIMIT;
775 - } elseif (defined('WP_MEMORY_LIMIT')) {
776 - $wp_memory_limit = WP_MEMORY_LIMIT;
777 - }
778 - $wp_memory_limit_mb = $wp_memory_limit ? $this->parse_memory_limit_to_mb($wp_memory_limit) : null;
779 -
780 - $actual_php_limit_mb = $memory_limit_mb;
781 - if ($wp_memory_limit_mb && $wp_memory_limit_mb >= 256) {
782 - $actual_php_limit_mb = 128;
783 - }
784 -
849 +
785 850 $can_change_memory = $this->can_change_memory_limit();
786 -
787 - $memory_limit_display = $actual_php_limit_mb == -1 ? 'Unlimited' : $actual_php_limit_mb . ' MB';
788 -
851 +
852 + $memory_limit_display = $memory_limit_mb == -1 ? 'Unlimited' : $memory_limit_mb . ' MB';
853 +
789 854 return array(
790 855 'max_execution_time' => $max_execution_time == -1 ? 'Unlimited' : $max_execution_time . ' seconds',
791 856 'memory_limit' => $memory_limit_display,
792 857 'max_execution_time_raw' => $max_execution_time,
793 - 'memory_limit_raw' => $actual_php_limit_mb,
858 + 'memory_limit_raw' => $memory_limit_mb,
794 859 'can_change_memory' => $can_change_memory
795 860 );
796 861 }
797 862
863 + /**
864 + * Allowed range for the Max Memory Limit setting: 64 MB up to the
865 + * server-configured memory limit (null max when the server is unlimited).
866 + */
867 + public function get_memory_limit_allowed_range() {
868 + $server_limits = $this->get_server_limits();
869 + $max = $server_limits['memory_limit_raw'] == -1 ? null : max(64, (int) $server_limits['memory_limit_raw']);
870 +
871 + return array(
872 + 'min' => 64,
873 + 'max' => $max
874 + );
875 + }
876 +
798 877 public function apply_memory_limit() {
799 878 if (!$this->can_change_memory_limit()) {
800 879 return false;
801 880 }
@@ -801,9 +880,9 @@
801 880 }
802 881
803 882 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
804 883
805 - $result = @ini_set('memory_limit', $memory_limit_mb . 'M');
884 + $result = @ini_set('memory_limit', $memory_limit_mb . 'M'); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
806 885
807 886 return $result !== false;
808 887 }
809 888
@@ -832,11 +911,12 @@
832 911
833 912 public function render_execution_settings_section() {
834 913 $settings = $this->get_all_execution_settings();
835 914 $server_limits = $this->get_server_limits();
915 + $memory_range = $this->get_memory_limit_allowed_range();
836 916
837 917 $warnings = array();
838 - if ($server_limits['max_execution_time_raw'] != -1 && $settings['max_execution_time'] > $server_limits['max_execution_time_raw']) {
918 + if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) {
839 919 $warnings['max_execution_time'] = true;
840 920 }
841 921 if ($server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
842 922 $warnings['max_memory_limit'] = true;
@@ -852,16 +932,18 @@
852 932
853 933 <!-- Execution & Memory Section -->
854 934 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
855 935 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Execution & Memory</h3>
856 -
857 - <div style="margin-bottom: 20px;">
936 +
937 + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
938 + <div>
858 939 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
859 940 Max Execution Time:
941 + <?php Metasync::render_tooltip_icon('max_execution_time', 'How many seconds a single plugin operation (like a bulk sync or scan) is allowed to run before PHP stops it. Raise this only if you see timeout errors, and only up to what your host allows.'); ?>
860 942 </label>
861 943 <div style="display: flex; align-items: center; gap: 10px;">
862 - <input type="number"
863 - id="max_execution_time"
944 + <input type="number"
945 + id="max_execution_time"
864 946 name="max_execution_time"
865 947 value="<?php echo esc_attr($settings['max_execution_time']); ?>"
866 948 min="1"
867 949 max="300"
@@ -887,16 +969,17 @@
887 969
888 970 <div>
889 971 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
890 972 Max Memory Limit:
973 + <?php Metasync::render_tooltip_icon('max_memory_limit', 'How much memory (RAM) the plugin is allowed to use for a single operation before PHP stops it. Raise this only if you see "out of memory" errors, and only up to what your host allows.'); ?>
891 974 </label>
892 975 <div style="display: flex; align-items: center; gap: 10px;">
893 - <input type="number"
894 - id="max_memory_limit"
976 + <input type="number"
977 + id="max_memory_limit"
895 978 name="max_memory_limit"
896 979 value="<?php echo esc_attr($settings['max_memory_limit']); ?>"
897 - min="64"
898 - max="512"
980 + min="<?php echo esc_attr($memory_range['min']); ?>"
981 + <?php if ($memory_range['max'] !== null): ?>max="<?php echo esc_attr($memory_range['max']); ?>"<?php endif; ?>
899 982 <?php if (!$server_limits['can_change_memory']): ?>
900 983 readonly
901 984 disabled
902 985 <?php endif; ?>
@@ -904,9 +987,9 @@
904 987 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: <?php echo $server_limits['can_change_memory'] ? 'var(--dashboard-card-bg)' : 'rgba(128, 128, 128, 0.1)'; ?>; color: <?php echo $server_limits['can_change_memory'] ? 'var(--dashboard-text-primary)' : 'var(--dashboard-text-secondary)'; ?>; cursor: <?php echo $server_limits['can_change_memory'] ? 'text' : 'not-allowed'; ?>;" />
905 988 <span style="color: var(--dashboard-text-secondary);">MB</span>
906 989 </div>
907 990 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;">
908 - Server Limit: <?php echo esc_html($server_limits['memory_limit']); ?>
991 + Server Limit: <?php echo esc_html($server_limits['memory_limit']); ?> &middot; Allowed: <?php echo $memory_range['max'] !== null ? esc_html($memory_range['min'] . '–' . $memory_range['max'] . ' MB') : esc_html($memory_range['min'] . ' MB or higher'); ?>
909 992 </p>
910 993 <?php if (!$server_limits['can_change_memory']): ?>
911 994 <p style="color: #f59e0b; font-size: 12px; margin: 4px 0 0 0; display: flex; align-items: center; gap: 4px;">
912 995 <span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
@@ -925,21 +1008,24 @@
925 1008 </script>
926 1009 <?php endif; ?>
927 1010 <?php endif; ?>
928 1011 </div>
1012 + </div>
929 1013 </div>
930 -
1014 +
931 1015 <!-- Batch Processing Section -->
932 1016 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
933 1017 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Batch Processing</h3>
934 -
935 - <div style="margin-bottom: 20px;">
1018 +
1019 + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1020 + <div>
936 1021 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
937 1022 Log Processing Batch Size:
1023 + <?php Metasync::render_tooltip_icon('log_batch_size', 'How many log lines the plugin processes at once when scanning its own logs. Lower it if this causes slowdowns on smaller hosting plans; most sites can leave the default.'); ?>
938 1024 </label>
939 1025 <div style="display: flex; align-items: center; gap: 10px;">
940 - <input type="number"
941 - id="log_batch_size"
1026 + <input type="number"
1027 + id="log_batch_size"
942 1028 name="log_batch_size"
943 1029 value="<?php echo esc_attr($settings['log_batch_size']); ?>"
944 1030 min="100"
945 1031 max="5000"
@@ -951,8 +1037,9 @@
951 1037
952 1038 <div>
953 1039 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
954 1040 Action Scheduler Concurrent Batches:
1041 + <?php Metasync::render_tooltip_icon('action_scheduler_batches', 'How many background sync jobs the plugin is allowed to run at the same time. Higher values finish syncing faster but use more server resources at once — lower this if your site slows down during syncs.'); ?>
955 1042 </label>
956 1043 <div style="display: flex; align-items: center; gap: 10px;">
957 1044 <input type="number"
958 1045 id="action_scheduler_batches"
@@ -967,17 +1054,20 @@
967 1054 <span class="dashicons dashicons-warning" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
968 1055 <span>Higher values increase server load</span>
969 1056 </p>
970 1057 </div>
1058 + </div>
971 1059 </div>
972 -
1060 +
973 1061 <!-- API Rate Limiting Section -->
974 1062 <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
975 1063 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">API Rate Limiting</h3>
976 -
977 - <div style="margin-bottom: 20px;">
1064 +
1065 + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1066 + <div>
978 1067 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
979 - OTTO API Calls Per Minute:
1068 + <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API Calls Per Minute:
1069 + <?php Metasync::render_tooltip_icon('otto_rate_limit', 'The maximum number of requests per minute the plugin will send to ' . Metasync::get_whitelabel_otto_name() . '\'s servers. Lower it if you\'re seeing rate-limit errors; most sites can leave the default.'); ?>
980 1070 </label>
981 1071 <div style="display: flex; align-items: center; gap: 10px;">
982 1072 <input type="number"
983 1073 id="otto_rate_limit"
@@ -992,8 +1082,9 @@
992 1082
993 1083 <div>
994 1084 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
995 1085 Queue Auto-Cleanup:
1086 + <?php Metasync::render_tooltip_icon('queue_cleanup_days', 'How many days a completed background job is kept before the plugin automatically deletes its record. Lower it to save database space; raise it if you need to look back further when troubleshooting.'); ?>
996 1087 </label>
997 1088 <div style="display: flex; align-items: center; gap: 10px;">
998 1089 <input type="number"
999 1090 id="queue_cleanup_days"
@@ -1005,10 +1096,11 @@
1005 1096 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1006 1097 <span style="color: var(--dashboard-text-secondary);">days</span>
1007 1098 </div>
1008 1099 </div>
1100 + </div>
1009 1101 </div>
1010 -
1102 +
1011 1103 <!-- Save Button -->
1012 1104 <div style="margin-top: 20px;">
1013 1105 <button type="submit"
1014 1106 id="metasync-execution-settings-save-btn"
@@ -1023,232 +1115,15 @@
1023 1115 <div id="metasync-execution-settings-message" style="display: none; margin-top: 16px; padding: 12px; border-radius: 6px;"></div>
1024 1116 </form>
1025 1117 </div>
1026 1118
1027 - <script>
1028 - jQuery(document).ready(function($) {
1029 - var $form = $('#metasync-execution-settings-form');
1030 - var $saveBtn = $('#metasync-execution-settings-save-btn');
1031 - var $message = $('#metasync-execution-settings-message');
1032 -
1033 - var serverMaxExecTime = <?php echo $server_limits['max_execution_time_raw'] == -1 ? 'Infinity' : $server_limits['max_execution_time_raw']; ?>;
1034 - var serverMaxMemory = <?php echo $server_limits['memory_limit_raw'] == -1 ? 'Infinity' : $server_limits['memory_limit_raw']; ?>;
1035 - var canChangeMemory = <?php echo $server_limits['can_change_memory'] ? 'true' : 'false'; ?>;
1036 -
1037 - function checkServerLimits() {
1038 - var maxExecTime = parseInt($('#max_execution_time').val()) || 0;
1039 - var maxMemory = parseInt($('#max_memory_limit').val()) || 0;
1040 -
1041 - if (serverMaxExecTime !== Infinity && maxExecTime > serverMaxExecTime) {
1042 - $('#max_execution_time_warning').show();
1043 - } else {
1044 - $('#max_execution_time_warning').hide();
1045 - }
1046 -
1047 - if (canChangeMemory && serverMaxMemory !== Infinity && maxMemory > serverMaxMemory) {
1048 - $('#max_memory_limit_warning').show();
1049 - } else {
1050 - $('#max_memory_limit_warning').hide();
1051 - }
1052 - }
1053 -
1054 - $('#max_execution_time, #max_memory_limit').on('input change', function() {
1055 - checkServerLimits();
1056 - $(this).css('border-color', 'var(--dashboard-border)');
1057 - });
1058 -
1059 - function highlightInvalidField($field, isValid) {
1060 - if (isValid) {
1061 - $field.css({
1062 - 'border-color': 'var(--dashboard-border)',
1063 - 'box-shadow': 'none'
1064 - });
1065 - } else {
1066 - $field.css({
1067 - 'border-color': '#ef4444',
1068 - 'box-shadow': '0 0 0 3px rgba(239, 68, 68, 0.1)'
1069 - });
1070 - }
1071 - }
1072 -
1073 - $('#max_execution_time').on('blur', function() {
1074 - var value = parseInt($(this).val()) || 0;
1075 - var isValid = value >= 1 && value <= 300 && (serverMaxExecTime === Infinity || value <= serverMaxExecTime);
1076 - highlightInvalidField($(this), isValid);
1077 - });
1078 -
1079 - $('#max_memory_limit').on('blur', function() {
1080 - if (!canChangeMemory) return;
1081 - var value = parseInt($(this).val()) || 0;
1082 - var isValid = value >= 64 && value <= 512 && (serverMaxMemory === Infinity || value <= serverMaxMemory);
1083 - highlightInvalidField($(this), isValid);
1084 - });
1085 -
1086 - checkServerLimits();
1087 -
1088 - $form.on('submit', function(e) {
1089 - e.preventDefault();
1090 -
1091 - var formData = {
1092 - action: 'metasync_save_execution_settings',
1093 - execution_settings_nonce: $('#execution_settings_nonce').val(),
1094 - max_execution_time: $('#max_execution_time').val(),
1095 - max_memory_limit: $('#max_memory_limit').val(),
1096 - log_batch_size: $('#log_batch_size').val(),
1097 - action_scheduler_batches: $('#action_scheduler_batches').val(),
1098 - otto_rate_limit: $('#otto_rate_limit').val(),
1099 - queue_cleanup_days: $('#queue_cleanup_days').val()
1100 - };
1101 -
1102 - $('input[type="number"]').css({
1103 - 'border-color': 'var(--dashboard-border)',
1104 - 'box-shadow': 'none'
1105 - });
1106 -
1107 - var hasError = false;
1108 - var errorField = null;
1109 -
1110 - if (formData.max_execution_time < 1 || formData.max_execution_time > 300) {
1111 - showMessage('Max Execution Time must be between 1 and 300 seconds.', 'error');
1112 - highlightInvalidField($('#max_execution_time'), false);
1113 - errorField = $('#max_execution_time');
1114 - hasError = true;
1115 - } else if (serverMaxExecTime !== Infinity && formData.max_execution_time > serverMaxExecTime) {
1116 - showMessage('Max Execution Time exceeds server limit of ' + serverMaxExecTime + ' seconds. Please reduce the value.', 'error');
1117 - highlightInvalidField($('#max_execution_time'), false);
1118 - errorField = $('#max_execution_time');
1119 - hasError = true;
1120 - }
1121 -
1122 - if (canChangeMemory) {
1123 - if (formData.max_memory_limit < 64 || formData.max_memory_limit > 512) {
1124 - showMessage('Max Memory Limit must be between 64 and 512 MB.', 'error');
1125 - highlightInvalidField($('#max_memory_limit'), false);
1126 - if (!hasError) {
1127 - errorField = $('#max_memory_limit');
1128 - hasError = true;
1129 - }
1130 - } else if (serverMaxMemory !== Infinity && formData.max_memory_limit > serverMaxMemory) {
1131 - showMessage('Max Memory Limit exceeds server limit of ' + serverMaxMemory + ' MB. Please reduce the value.', 'error');
1132 - highlightInvalidField($('#max_memory_limit'), false);
1133 - if (!hasError) {
1134 - errorField = $('#max_memory_limit');
1135 - hasError = true;
1136 - }
1137 - }
1138 - }
1139 -
1140 - if (formData.log_batch_size < 100 || formData.log_batch_size > 5000) {
1141 - showMessage('Log Batch Size must be between 100 and 5000 lines.', 'error');
1142 - highlightInvalidField($('#log_batch_size'), false);
1143 - if (!hasError) {
1144 - errorField = $('#log_batch_size');
1145 - hasError = true;
1146 - }
1147 - }
1148 - if (formData.action_scheduler_batches < 1 || formData.action_scheduler_batches > 10) {
1149 - showMessage('Action Scheduler Batches must be between 1 and 10.', 'error');
1150 - highlightInvalidField($('#action_scheduler_batches'), false);
1151 - if (!hasError) {
1152 - errorField = $('#action_scheduler_batches');
1153 - hasError = true;
1154 - }
1155 - }
1156 - if (formData.otto_rate_limit < 1 || formData.otto_rate_limit > 60) {
1157 - showMessage('OTTO Rate Limit must be between 1 and 60 calls per minute.', 'error');
1158 - highlightInvalidField($('#otto_rate_limit'), false);
1159 - if (!hasError) {
1160 - errorField = $('#otto_rate_limit');
1161 - hasError = true;
1162 - }
1163 - }
1164 - if (formData.queue_cleanup_days < 7 || formData.queue_cleanup_days > 90) {
1165 - showMessage('Queue Cleanup Days must be between 7 and 90 days.', 'error');
1166 - highlightInvalidField($('#queue_cleanup_days'), false);
1167 - if (!hasError) {
1168 - errorField = $('#queue_cleanup_days');
1169 - hasError = true;
1170 - }
1171 - }
1172 -
1173 - if (hasError) {
1174 - if (errorField) {
1175 - errorField.focus();
1176 - $('html, body').animate({
1177 - scrollTop: errorField.offset().top - 100
1178 - }, 300);
1179 - }
1180 - return;
1181 - }
1182 -
1183 - $saveBtn.prop('disabled', true);
1184 - $saveBtn.find('.save-text').text('Saving...');
1185 - $saveBtn.find('.save-spinner').show();
1186 - $message.hide();
1187 -
1188 - $.ajax({
1189 - url: ajaxurl,
1190 - type: 'POST',
1191 - data: formData,
1192 - success: function(response) {
1193 - if (response.success) {
1194 - showMessage(response.data.message || 'Settings saved successfully!', 'success');
1195 - $saveBtn.prop('disabled', false);
1196 - $saveBtn.find('.save-text').text('Save Settings');
1197 - $saveBtn.find('.save-spinner').hide();
1198 - setTimeout(function() {
1199 - checkServerLimits();
1200 - }, 100);
1201 - $('html, body').animate({
1202 - scrollTop: $form.offset().top - 100
1203 - }, 300);
1204 - } else {
1205 - showMessage(response.data.message || 'Error saving settings.', 'error');
1206 - $saveBtn.prop('disabled', false);
1207 - $saveBtn.find('.save-text').text('Save Settings');
1208 - $saveBtn.find('.save-spinner').hide();
1209 - $('html, body').animate({
1210 - scrollTop: $message.offset().top - 100
1211 - }, 300);
1212 - }
1213 - },
1214 - error: function() {
1215 - showMessage('An error occurred while saving settings. Please try again.', 'error');
1216 - $saveBtn.prop('disabled', false);
1217 - $saveBtn.find('.save-text').text('Save Settings');
1218 - $saveBtn.find('.save-spinner').hide();
1219 - $('html, body').animate({
1220 - scrollTop: $message.offset().top - 100
1221 - }, 300);
1222 - }
1223 - });
1224 - });
1225 -
1226 - function showMessage(text, type) {
1227 - $message.removeClass('notice-success notice-error')
1228 - .addClass('notice-' + type)
1229 - .css({
1230 - 'background': type === 'success' ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)',
1231 - 'border': '1px solid ' + (type === 'success' ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'),
1232 - 'color': type === 'success' ? '#22c55e' : '#ef4444',
1233 - 'padding': '12px 16px',
1234 - 'border-radius': '6px',
1235 - 'font-size': '14px',
1236 - 'line-height': '1.5',
1237 - 'display': 'block'
1238 - })
1239 - .html('<strong style="margin-right: 8px;">' + (type === 'success' ? '✓' : '✗') + '</strong>' + text)
1240 - .show();
1241 -
1242 - if (type === 'success') {
1243 - setTimeout(function() {
1244 - $message.fadeOut(300);
1245 - }, 5000);
1246 - }
1247 - }
1248 - });
1249 - </script>
1250 1119 <?php
1120 + // Real-time validation and AJAX save for this form live in
1121 + // admin/js/metasync-execution-settings.js (enqueued on this page).
1122 + // The previous inline duplicate was removed to avoid a double-bound
1123 + // submit handler that fired the save request twice.
1124 + ?>
1125 + <?php
1251 1126 }
1252 1127
1253 1128 public function get_field_tooltips() {
1254 1129 $plugin_name = Metasync::get_effective_plugin_name();
@@ -1254,36 +1129,73 @@
1254 1129 $plugin_name = Metasync::get_effective_plugin_name();
1255 1130 $otto_name = Metasync::get_whitelabel_otto_name();
1256 1131
1257 1132 return array(
1258 - 'searchatlas_api_key' => sprintf('Connect your WordPress site to your %s dashboard to retrieve your API key and OTTO UUID. This does not create a WordPress login session.', $plugin_name),
1133 + 'searchatlas_api_key' => sprintf('Connect your WordPress site to your %1$s dashboard to retrieve your API key and %2$s UUID. This does not create a WordPress login session.', $plugin_name, $otto_name),
1259 1134 'apikey' => sprintf('Auto-generated authentication token used for secure API communication between your WordPress site and %s services. You can refresh this token if needed for security purposes.', $plugin_name),
1260 1135 'otto_pixel_uuid' => sprintf('Your unique %s tracking pixel identifier. This UUID is used to track %s modifications and analytics on your website pages.', $otto_name, $otto_name),
1261 1136 'otto_disable_on_loggedin' => sprintf('Disable %s modifications when you are logged in to WordPress. This allows you to see and edit the original content without %s\'s enhancements during editing sessions.', $otto_name, $otto_name),
1262 1137 'otto_disable_preview_button' => sprintf('Hide the %s frontend toolbar that displays the status indicator, preview button, and debug button. Enable this for a cleaner frontend experience.', $otto_name),
1138 + 'seo_priority' => sprintf('Choose which value appears first on a page when both your custom SEO value (title, description or focus keyword, edited on the post/page screen) and an approved %s suggestion are available. This changes what visitors see; it does not delete or overwrite saved values.', $otto_name),
1263 1139 'otto_wp_rocket_compat' => sprintf('WP Rocket Compatibility Mode: Controls how %s interacts with WP Rocket. "Auto" (recommended) allows both to work together by avoiding DONOTCACHEPAGE constant unless necessary for Brizy pages or SG Optimizer conflicts. This ensures WP Rocket\'s JavaScript delay and optimization features continue working.', $otto_name),
1140 + 'otto_sg_optimizer_compat' => sprintf('SiteGround Optimizer Compatibility Mode: Controls how %s renders on SiteGround hosting. "Auto" (recommended) uses the internal HTTP fetch, which protects themes that defer inline CSS to the footer (e.g. Divi). "Buffer Mode" renders in-process with no internal fetch — faster on sites whose pages can never be host-cached, for example when a plugin starts a PHP session on every request (BookingPress sessions are removed automatically, so this does not apply to them).', $otto_name),
1141 + 'bookingpress_session_compat' => 'BookingPress Session Compatibility: Controls whether MetaSync removes the PHP session BookingPress starts on every public page, so hosts such as SiteGround can page-cache the site again.',
1264 1142 'otto_disable_for_bots' => sprintf('Enable bot detection to automatically skip %s processing for search engine crawlers, SEO tools, and other bots. This reduces unnecessary API calls and improves performance.', $otto_name),
1265 1143 'otto_bot_whitelist' => sprintf('Enter bot names or user-agent patterns (one per line) that should always be processed by %s, even when "Disable for Bots" is enabled. For example: Googlebot, Bingbot. This allows you to ensure specific search engines always see your optimized content.', $otto_name),
1266 1144 'otto_bot_blacklist' => sprintf('Enter bot names or user-agent patterns (one per line) that should always be blocked from %s processing, regardless of other settings. For example: BadBot, MaliciousCrawler. Use this to exclude problematic crawlers or unwanted traffic sources.', $otto_name),
1267 1145 'otto_bot_statistics_link' => 'View detailed bot detection statistics including total hits, API calls saved, breakdown by bot type, and unique bot entries with hit counts.',
1268 - 'disable_common_robots_metabox' => 'Hide the Common Robots meta box from post and page edit screens. This removes the robots meta tag controls (index/noindex, follow/nofollow) from the editor interface.',
1269 - 'disable_advance_robots_metabox' => 'Hide the Advanced Robots meta box from post and page edit screens. This removes advanced robots directives like max-snippet, max-image-preview, and max-video-preview settings.',
1270 - 'disable_redirection_metabox' => 'Hide the Redirection meta box from post and page edit screens. This removes the URL redirect configuration options from the editor interface.',
1271 - 'disable_canonical_metabox' => 'Hide the Canonical URL meta box from post and page edit screens. This removes the canonical URL override field from the editor interface.',
1272 - 'disable_social_opengraph_metabox' => 'Hide the Social Media & Open Graph meta box from post and page edit screens. This removes Facebook, Twitter, and other social media meta tag controls from the editor.',
1273 - 'disable_schema_markup_metabox' => 'Hide the Schema Markup meta box from post and page edit screens. This removes the structured data (Article, FAQ, Product, Recipe, etc.) configuration from the editor interface.',
1274 - 'open_external_links' => 'Automatically add target="_blank" attribute to external links appearing in your posts, pages, and other post types when rendered by Otto.',
1146 + 'disable_common_robots_metabox' => sprintf('Hide the Common Robots meta box and stop %1$s emitting index/noindex and follow/nofollow directives on the front end. %1$s also stops overriding WordPress core or another SEO plugin\'s robots tag. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1147 + 'disable_advance_robots_metabox' => sprintf('Hide the Advanced Robots meta box and stop %1$s emitting max-snippet, max-image-preview and max-video-preview directives, including the site-wide advanced defaults. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1148 + 'disable_redirection_metabox' => 'Hide the Redirection meta box and stop redirects configured through it from running. Rules added by hand on the Redirections screen keep working. Saved redirects are kept and resume if you re-enable this.',
1149 + 'disable_canonical_metabox' => sprintf('Hide the Canonical meta box and stop %1$s emitting or overriding the canonical URL on every render path. WordPress core and third-party canonicals are left in place. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1150 + 'disable_social_opengraph_metabox' => sprintf('Hide the Social Media & Open Graph meta box and stop %1$s emitting any Open Graph, Twitter or article tags. Tags from another SEO plugin are left in place. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1151 + 'disable_schema_markup_metabox' => sprintf('Hide the Schema Markup meta box and stop %1$s emitting any JSON-LD, including the site-wide Local SEO markup and OTTO structured data. Breadcrumb markup has its own setting. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1152 + 'disable_language_alternates_metabox' => sprintf('Hide the Language Alternates (hreflang) panel in the block editor and stop %1$s emitting any hreflang tags on the front end, including the auto-detected WPML entries. While disabled, %1$s does not manage or override this data — another SEO plugin, or WPML itself, is free to handle it. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')),
1153 + 'disable_seo_metabox' => 'Hide the SEO Title & Meta Description meta box from post and page edit screens. This removes the Classic editor SEO fields (the Gutenberg sidebar is unaffected).',
1154 + 'open_external_links' => sprintf('Automatically add target="_blank" attribute to external links appearing in your posts, pages, and other post types when rendered by %s.', $otto_name),
1275 1155 'content_genius_sync_roles' => 'Select which WordPress user roles should be synchronized with Content Genius. This determines which users will have their profiles and permissions synced for content collaboration.',
1276 1156 'permalink_structure' => sprintf('Displays your current WordPress permalink structure. %s works best with pretty permalinks (not "Plain"). If you see a warning, visit Settings > Permalinks to change your structure.', $plugin_name),
1277 1157 'hide_dashboard_framework' => sprintf('Hide the main %s dashboard from the WordPress admin menu. This is useful if you want to reduce menu clutter but still keep the plugin active.', $plugin_name),
1278 1158 'show_admin_bar_status' => sprintf('Display the %s status indicator in the WordPress admin bar at the top of your screen. This provides quick visibility of plugin status and key metrics.', $plugin_name),
1279 - 'enable_auto_updates' => sprintf('Allow WordPress to automatically update the %s plugin when new versions are released. Recommended for security patches, but you may prefer manual updates for major versions.', $plugin_name),
1280 1159 'import_external_data' => sprintf('Import your existing SEO settings and metadata from other popular SEO plugins like Yoast, Rank Math, or All in One SEO. This makes migration to %s seamless without losing your SEO data.', $plugin_name),
1281 - 'import_seo_metadata' => 'Migrate your existing SEO titles and meta descriptions from Yoast, Rank Math, or All in One SEO. This one-click import preserves your search rankings by copying your optimized meta data to MetaSync, even if the source plugin is deactivated.',
1282 - 'default_page_builder' => 'Choose how Content Genius stores synced articles. "Gutenberg" works with all themes and page builders. Selecting a specific builder converts content to that builder\'s widget format, which inherits the builder\'s global typography and layout styles.'
1160 + 'import_seo_metadata' => sprintf('Migrate your existing SEO titles and meta descriptions from Yoast, Rank Math, or All in One SEO. This one-click import preserves your search rankings by copying your optimized meta data to %s, even if the source plugin is deactivated.', $plugin_name),
1161 + 'default_page_builder' => 'Choose how Content Genius stores synced articles. "Gutenberg" works with all themes and page builders. Selecting a specific builder converts content to that builder\'s widget format, which inherits the builder\'s global typography and layout styles.',
1162 + 'og_image_dimensions' => 'Adds your share image\'s width, height, and file type to the Open Graph tags so Facebook, LinkedIn, and others render your preview instantly at the right size instead of guessing. Leave on unless another plugin already outputs these.',
1163 + 'article_timestamps' => 'Tells social platforms and search engines when each post was first published and last updated, so shares and rich results show accurate dates. Safe to leave enabled.',
1164 + 'article_author' => 'Adds a link to the author\'s profile or archive page to each post\'s social tags, so shared articles can show author attribution. You can override the author URL per-post. Turn off if you\'d rather not expose author pages.',
1165 + 'article_section' => 'Labels each post with its main topic (its primary category) in the social tags, helping platforms categorize your content. Falls back to the first category if no primary one is set.',
1166 + 'article_tags' => 'Adds each of your WordPress post tags as a separate social "tag" so platforms understand the topics a post covers. Turn off if you don\'t want your tags exposed in share metadata.',
1167 + 'twitter_image_alt' => 'Adds descriptive alt text to the image shown when your page is shared on X/Twitter, improving accessibility for screen-reader users. Uses your saved alt text or the image\'s own alt attribute.'
1283 1168 );
1284 1169 }
1285 1170
1171 + public function render_analytics_section() {
1172 + $enabled = get_option('metasync_analytics_opt_in', 'no') === 'yes';
1173 + $plugin_name = Metasync::get_effective_plugin_name();
1174 + /* translators: %s: Plugin name. */
1175 + $heading = sprintf(__('Help improve %s', 'metasync'), $plugin_name);
1176 + /* translators: %s: Plugin name. */
1177 + $intro = sprintf(__('Allow %s to collect limited usage analytics. This helps us understand which features are used, improve the product, and identify problems.', 'metasync'), $plugin_name);
1178 + /* translators: %s: Plugin name. */
1179 + $toggle_label = sprintf(__('Share usage data with %s', 'metasync'), $plugin_name);
1180 + ?>
1181 + <div class="metasync-analytics-consent-card">
1182 + <div class="metasync-analytics-consent-copy">
1183 + <h3><?php echo esc_html($heading); ?></h3>
1184 + <p><?php echo esc_html($intro); ?></p>
1185 + <p class="description"><?php esc_html_e('We collect limited technical information such as your site URL, plugin version, WordPress and PHP versions, user role, and feature usage. We do not collect passwords, API keys, customer content, names, or email addresses.', 'metasync'); ?></p>
1186 + </div>
1187 + <label class="metasync-consent-switch" for="metasync_analytics_opt_in">
1188 + <input type="hidden" name="metasync_analytics_opt_in" value="no" />
1189 + <input type="checkbox" id="metasync_analytics_opt_in" name="metasync_analytics_opt_in" value="yes" <?php checked($enabled); ?> />
1190 + <span class="metasync-consent-slider" aria-hidden="true"></span>
1191 + <span class="screen-reader-text"><?php echo esc_html($toggle_label); ?></span>
1192 + </label>
1193 + <span class="metasync-consent-status" aria-live="polite" data-enabled-label="<?php esc_attr_e('Enabled', 'metasync'); ?>" data-disabled-label="<?php esc_attr_e('Off', 'metasync'); ?>"><?php echo $enabled ? esc_html__('Enabled', 'metasync') : esc_html__('Off', 'metasync'); ?></span>
1194 + </div>
1195 + <?php
1196 + }
1197 +
1286 1198 public function get_field_section($field_id) {
1287 1199 $sections = $this->get_accordion_sections_config();
1288 1200
1289 1201 foreach ($sections as $section_key => $section_data) {
@@ -1317,9 +1229,9 @@
1317 1229 $content_state = $is_open ? 'open' : 'closed';
1318 1230
1319 1231 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1320 1232
1321 - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
1233 + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
1322 1234 echo '<div class="metasync-accordion-title">';
1323 1235 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1324 1236 echo '<div class="metasync-accordion-text">';
1325 1237 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -1330,9 +1242,9 @@
1330 1242 echo '<span class="toggle-icon">▼</span>';
1331 1243 echo '</button>';
1332 1244 echo '</div>';
1333 1245
1334 - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
1246 + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
1335 1247
1336 1248 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1337 1249 call_user_func($section_data['render_callback']);
1338 1250 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
@@ -1347,9 +1259,9 @@
1347 1259 echo '<tr>';
1348 1260 echo '<th scope="row">';
1349 1261 if (!empty($field['title'])) {
1350 1262 echo '<div class="metasync-field-label-wrapper">';
1351 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1263 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1352 1264
1353 1265 if (isset($tooltips[$field_id])) {
1354 1266 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1355 1267 echo '<svg class="metasync-info-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">';
@@ -1395,13 +1307,22 @@
1395 1307 return;
1396 1308 }
1397 1309
1398 1310 echo '<table class="form-table" role="presentation">';
1311 + $llms_tooltips = array(
1312 + 'llms_txt_enabled' => 'Publishes a file at yoursite.com/llms.txt listing your key pages so AI assistants like ChatGPT and Perplexity can find and cite your content. Safe to leave off if you don\'t want AI crawlers guided to your pages.',
1313 + 'llms_full_enabled' => 'Also publishes a larger file with the full text of your posts (not just links). This exposes complete article content and can be large — keep the post limit low on smaller hosting plans.',
1314 + );
1399 1315 foreach ($wp_settings_fields[$page][$section] as $field) {
1400 1316 echo '<tr>';
1401 1317 echo '<th scope="row">';
1402 1318 if (!empty($field['title'])) {
1403 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1319 + echo '<div class="metasync-field-label-wrapper">';
1320 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1321 + if (isset($llms_tooltips[$field['id']])) {
1322 + Metasync::render_tooltip_icon($field['id'], $llms_tooltips[$field['id']]);
1323 + }
1324 + echo '</div>';
1404 1325 }
1405 1326 echo '</th>';
1406 1327 echo '<td>';
1407 1328 call_user_func($field['callback'], $field['args']);
@@ -1451,31 +1372,32 @@
1451 1372 $display_value = $current_token;
1452 1373 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1453 1374 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1454 1375 } else {
1455 - $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name());
1456 - $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . esc_html(Metasync::get_effective_plugin_name()) . '".';
1376 + $display_value = 'Auto-generated when connecting to ' . Metasync::get_effective_plugin_name();
1377 + $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . Metasync::get_effective_plugin_name() . '".';
1457 1378 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1458 1379 }
1459 1380
1381 + printf('<div class="metasync-auth-token-row">');
1460 1382 printf(
1461 - '<input type="text" id="apikey" name="' . Metasync_Admin::option_key . '[general][apikey]" value="%s" size="40" readonly="readonly" /> ',
1383 + '<input type="text" id="apikey" name="' . esc_attr(Metasync_Admin::option_key) . '[general][apikey]" value="%s" size="40" class="regular-text" readonly="readonly" />',
1462 1384 esc_attr($display_value)
1463 1385 );
1464 -
1465 - printf('<button type="button" id="refresh-plugin-auth-token" class="button button-secondary" style="margin-left: 10px;"><span class="dashicons dashicons-controls-repeat" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Refresh Token</button>');
1466 - printf('<p class="description">%s %s</p>', $status_message, $refresh_help);
1386 + printf('<button type="button" id="refresh-plugin-auth-token" class="button button-secondary metasync-refresh-token-btn"><span class="dashicons dashicons-controls-repeat"></span> Refresh Token</button>');
1387 + printf('</div>');
1388 + printf('<p class="description">%s %s</p>', esc_html($status_message), esc_html($refresh_help));
1467 1389 }
1468 1390
1469 1391 public function linkgraph_token_callback()
1470 1392 {
1471 1393 printf(
1472 - '<input type="text" id="linkgraph_token" name="' . Metasync_Admin::option_key . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1394 + '<input type="text" id="linkgraph_token" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1473 1395 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1474 1396 );
1475 1397
1476 1398 printf(
1477 - '<input type="text" id="linkgraph_customer_id" name="' . Metasync_Admin::option_key . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1399 + '<input type="text" id="linkgraph_customer_id" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1478 1400 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1479 1401 );
1480 1402
1481 1403 ?>
@@ -1491,10 +1413,16 @@
1491 1413 {
1492 1414 if(empty($datetime)){
1493 1415 return "";
1494 1416 }
1495 - $now = new DateTime;
1496 - $ago = new DateTime($datetime);
1417 + # Stored timestamps use current_time('mysql') which is the WP site's
1418 + # local timezone (no offset marker on the string). Constructing
1419 + # DateTime without a timezone would default to UTC and produce a
1420 + # bogus diff equal to the WP timezone offset (QA: "4 hours ago"
1421 + # after a 4-minute sync on non-UTC sites).
1422 + $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
1423 + $now = new DateTime('now', $wp_tz);
1424 + $ago = new DateTime($datetime, $wp_tz);
1497 1425
1498 1426 $diff = $now->diff($ago);
1499 1427
1500 1428 $string = [
@@ -1519,12 +1447,26 @@
1519 1447 }
1520 1448
1521 1449 public function searchatlas_api_key_callback()
1522 1450 {
1523 - $current_api_key = isset(Metasync::get_option('general')['searchatlas_api_key']) ? esc_attr(Metasync::get_option('general')['searchatlas_api_key']) : '';
1451 + // Whether a key is stored at rest (encrypted or legacy plaintext).
1452 + $stored_api_key = Metasync::get_option('general')['searchatlas_api_key'] ?? '';
1453 + $has_api_key = ($stored_api_key !== '');
1454 +
1455 + // Decrypted key for masking only. Never rendered in cleartext.
1456 + // false => an encrypted key exists but cannot be decrypted (salts changed).
1457 + $decrypted_api_key = Metasync::get_searchatlas_api_key();
1458 + $decrypt_failed = ($decrypted_api_key === false);
1459 +
1460 + // Build a masked representation: bullets + last 4 chars. The cleartext
1461 + // key is never emitted into the page.
1462 + $masked_api_key = '';
1463 + if (!$decrypt_failed && $decrypted_api_key !== '') {
1464 + $masked_api_key = str_repeat('*', 8) . substr($decrypted_api_key, -4);
1465 + }
1466 +
1524 1467 $otto_uuid = isset(Metasync::get_option('general')['otto_pixel_uuid']) ? Metasync::get_option('general')['otto_pixel_uuid'] : '';
1525 -
1526 - $has_api_key = !empty($current_api_key);
1468 +
1527 1469 $has_otto_uuid = !empty($otto_uuid);
1528 1470
1529 1471 $is_fully_connected = $this->admin_instance->is_heartbeat_connected();
1530 1472
@@ -1578,11 +1520,11 @@
1578 1520 printf('<div style="padding: 10px 0; color: #666; font-size: 13px; line-height: 1.5;">');
1579 1521 printf('• Make sure you have a %s account before connecting<br/>', esc_html(Metasync::get_effective_plugin_name()));
1580 1522 printf('• The authentication window will open in a popup - please allow popups<br/>');
1581 1523 printf('• The process typically takes 15-30 seconds to complete<br/>');
1582 - printf('• Your API key will be automatically filled in the field below<br/>');
1524 + printf('• Your API key will be stored securely and shown masked below<br/>');
1583 1525 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1584 - printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', Metasync::SUPPORT_EMAIL, Metasync::SUPPORT_EMAIL);
1526 + printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', esc_attr(Metasync::SUPPORT_EMAIL), esc_html(Metasync::SUPPORT_EMAIL));
1585 1527 printf('</div>');
1586 1528 printf('</details>');
1587 1529 printf('</div>');
1588 1530
@@ -1597,30 +1539,42 @@
1597 1539 printf('<span style="color: #ff8c00; margin-left: 10px; font-weight: normal;">Partial Connection (Missing %s UUID)</span>', esc_html(Metasync::get_whitelabel_otto_name()));
1598 1540 }
1599 1541 printf('</label>');
1600 1542
1543 + // Read-only masked display. The API key is managed via one-click
1544 + // authentication only; the cleartext key is never rendered into the
1545 + // page HTML (View Source / DOM). No form input is emitted, so saving
1546 + // settings cannot overwrite or expose the stored key.
1547 + $display_has_key = (!$decrypt_failed && $masked_api_key !== '') ? '1' : '0';
1601 1548 printf(
1602 - '<input type="text" id="searchatlas-api-key" name="' . Metasync_Admin::option_key . '[general][searchatlas_api_key]" value="%s" size="40" class="regular-text" placeholder="Your API key will appear here after authentication" />',
1603 - $current_api_key
1549 + '<div id="searchatlas-api-key-display" class="metasync-api-key-masked" data-has-key="%s" style="display:inline-flex; align-items:center; min-width:320px; height:42px; padding:0 14px; font-family:monospace; font-size:15px; letter-spacing:4px; background:var(--dashboard-input-bg, #22272e); border:1px solid var(--dashboard-border, #3a424d); border-radius:6px; color:var(--dashboard-text-primary, #e6e8eb); cursor:default; user-select:none;">',
1550 + esc_attr($display_has_key)
1604 1551 );
1605 -
1552 + if ($decrypt_failed) {
1553 + printf('<em style="color:#b32d2e; font-family:sans-serif; letter-spacing:normal; font-style:normal; font-size:14px;">Stored API key could not be read. Please re-authenticate above.</em>');
1554 + } elseif ($masked_api_key !== '') {
1555 + echo esc_html($masked_api_key);
1556 + } else {
1557 + printf('<em style="color:#646970;">Not configured</em>');
1558 + }
1559 + printf('</div>');
1560 +
1606 1561 printf('<p class="description" style="margin-top: 8px;">');
1607 - if ($is_fully_connected) {
1608 - printf('Your %s API key for secure communication with the platform. Use the authentication button above to refresh or change accounts.', esc_html(Metasync::get_effective_plugin_name()));
1562 + if ($decrypt_failed) {
1563 + printf('Your stored API key could not be decrypted (your site security keys may have changed). Please re-authenticate above to reconnect.');
1564 + } elseif ($is_fully_connected) {
1565 + printf('Your %s API key is stored securely and used for communication with the platform. Use the authentication button above to refresh or change accounts.', esc_html(Metasync::get_effective_plugin_name()));
1609 1566 } elseif ($has_api_key && !$has_otto_uuid) {
1610 1567 printf('Your API key is configured but %s UUID is missing. Re-authenticate above to complete the setup and enable dashboard access.', esc_html(Metasync::get_whitelabel_otto_name()));
1611 1568 } else {
1612 - printf('This field will be automatically populated when you authenticate using the button above. You can also manually enter your API key if you have one.');
1569 + printf('Use the authentication button above to securely connect your account. Your API key is stored encrypted and shown masked here.');
1613 1570 }
1614 1571 printf('</p>');
1615 1572
1616 - ?>
1617 - <p class="description" style="margin-top: 10px; padding: 8px 12px; background: #fff9e6; border-left: 3px solid #f0b849; border-radius: 4px; font-size: 12px;">
1618 - <strong>Manual Authentication:</strong> If you manually enter your <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> API Key and OTTO UUID, you also consent to the same AI-powered automation permissions described above.
1619 - </p>
1620 - <?php
1573 + printf('</div>');
1621 1574
1622 - printf('</div>');
1575 + // The OTTO Pixel UUID is shown once, by the read-only Settings-API
1576 + // field in the OTTO Server-Side Rendering section; do not repeat it here.
1623 1577
1624 1578 if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){
1625 1579 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1626 1580 printf(
@@ -1625,9 +1579,9 @@
1625 1579 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1626 1580 printf(
1627 1581 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1628 1582 esc_attr($timestamp),
1629 - $this->time_elapsed_string($timestamp)
1583 + esc_html($this->time_elapsed_string($timestamp))
1630 1584 );
1631 1585
1632 1586
1633 1587 }
@@ -1635,9 +1589,9 @@
1635 1589
1636 1590 public function bing_site_verification_callback()
1637 1591 {
1638 1592 printf(
1639 - '<input type="text" id="bing_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1593 + '<input type="text" id="bing_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1640 1594 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1641 1595 );
1642 1596
1643 1597 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
@@ -1646,9 +1600,9 @@
1646 1600
1647 1601 public function yandex_site_verification_callback()
1648 1602 {
1649 1603 printf(
1650 - '<input type="text" id="yandex_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1604 + '<input type="text" id="yandex_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1651 1605 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1652 1606 );
1653 1607
1654 1608 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
@@ -1657,9 +1611,9 @@
1657 1611
1658 1612 public function google_site_verification_callback()
1659 1613 {
1660 1614 printf(
1661 - '<input type="text" id="google_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][google_site_verification]" value="%s" size="50" />',
1615 + '<input type="text" id="google_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][google_site_verification]" value="%s" size="50" />',
1662 1616 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1663 1617 );
1664 1618
1665 1619 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
@@ -1668,9 +1622,9 @@
1668 1622
1669 1623 public function pinterest_site_verification_callback()
1670 1624 {
1671 1625 printf(
1672 - '<input type="text" id="pinterest_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1626 + '<input type="text" id="pinterest_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1673 1627 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1674 1628 );
1675 1629
1676 1630 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
@@ -1679,15 +1633,21 @@
1679 1633
1680 1634 public function local_seo_person_organization_callback()
1681 1635 {
1682 1636 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1637 + $valid_person_organizations = array('Person', 'Organization');
1683 1638 ?>
1684 - <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1685 - <?php
1686 - printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1687 - printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1688 - ?>
1689 - </select>
1639 + <div class="metasync-type-select-wrap">
1640 + <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1641 + <?php
1642 + if (!in_array($person_organization, $valid_person_organizations, true)) {
1643 + printf('<option value="0" selected="selected">Select Type</option>');
1644 + }
1645 + printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1646 + printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1647 + ?>
1648 + </select>
1649 + </div>
1690 1650 <?php
1691 1651 printf(' <br> <span class="description"> Choose whether the site represents a person or an organization. </span>');
1692 1652 }
1693 1653
@@ -1693,10 +1653,10 @@
1693 1653
1694 1654 public function local_seo_name_callback()
1695 1655 {
1696 1656 printf(
1697 - '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1698 - isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : get_bloginfo()
1657 + '<input type="text" id="local_seo_name" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_name]" value="%s" size="50" />',
1658 + isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : esc_attr(get_bloginfo())
1699 1659 );
1700 1660
1701 1661 printf(' <br> <span class="description"> Your name or company name </span>');
1702 1662 }
@@ -1704,31 +1664,82 @@
1704 1664 public function local_seo_logo_callback()
1705 1665 {
1706 1666 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1707 1667
1668 + // Rows saved by the legacy sanitizer hold the corrupted form of
1669 + // an attachment ID ("http://45589"). Repair it before anything reads
1670 + // the value — both so the preview resolves and so the hidden input
1671 + // re-submits the recovered ID instead of the corrupted string, which
1672 + // would otherwise survive every subsequent Save Settings.
1673 + $local_seo_logo = metasync_repair_scheme_prefixed_media_id($local_seo_logo);
1674 +
1708 1675 printf(
1709 - '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1710 - isset(Metasync::get_option('localseo')['local_seo_logo']) ? esc_attr(Metasync::get_option('localseo')['local_seo_logo']) : ''
1676 + '<input type="hidden" id="local_seo_logo" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_logo]" value="%s" size="50" />',
1677 + esc_attr((string) $local_seo_logo)
1711 1678 );
1712 1679
1713 - printf(' <br> <input class="button-secondary" type="button" id="logo_upload_button" value="Add or Upload File">');
1680 + printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">');
1714 1681
1715 1682 printf(' <br><br> <span class="description bold"> Min Size: 160Χ90px, Max Size: 1920X1080px. </span> <br> <span class="description"> A squared image is preferred by the search engines. </span> <br><br> ');
1716 1683
1717 - printf('<img src="%s" id="local_seo_business_logo" width="300">', wp_get_attachment_image_src($local_seo_logo, 'medium')[0] ?? '');
1684 + printf('<span class="metasync-logo-preview-wrap">');
1718 1685
1719 - $button_type = 'hidden';
1720 - if ($local_seo_logo) {
1721 - $button_type = 'button';
1686 + // The stored value may be an attachment ID or a raw URL, so resolve
1687 + // both the way the front-end schema output already does. Anything that
1688 + // resolves to nothing — a deleted attachment, a legacy value mangled by
1689 + // an earlier save — must render no preview at all, rather than an empty
1690 + // src next to an orphaned remove button.
1691 + $logo_url = $this->resolve_media_url($local_seo_logo, 'medium');
1692 +
1693 + printf(
1694 + '<img src="%s" id="local_seo_business_logo" width="300"%s>',
1695 + esc_url($logo_url),
1696 + $logo_url ? '' : ' style="display:none"'
1697 + );
1698 +
1699 + $button_type = $logo_url ? 'button' : 'hidden';
1700 + printf('<input type="%s" class="button-secondary no-loading metasync-logo-remove-btn" id="local_seo_logo_close_btn" value="X">', esc_attr($button_type));
1701 + printf('</span>');
1702 + }
1703 +
1704 + /**
1705 + * Resolve a stored media setting to a displayable URL.
1706 + *
1707 + * These settings accept either a media-library attachment ID or a raw URL;
1708 + * returns an empty string when the value resolves to neither, so callers
1709 + * can distinguish "no image" from "image we could not resolve".
1710 + *
1711 + * @param mixed $value Attachment ID or URL as stored in the options.
1712 + * @param string $size Registered image size to request for an attachment.
1713 + * @return string Resolved URL, or '' when there is nothing to show.
1714 + */
1715 + private function resolve_media_url($value, $size = 'medium')
1716 + {
1717 + if (empty($value)) {
1718 + return '';
1722 1719 }
1723 - printf('<input type="%s" class="button-secondary" id="local_seo_logo_close_btn" value="X">', $button_type);
1720 +
1721 + // A row saved by the legacy sanitizer stores a corrupted attachment ID ("http://45589"),
1722 + // which is neither resolvable as an ID nor a usable image URL.
1723 + $value = metasync_repair_scheme_prefixed_media_id($value);
1724 +
1725 + if (empty($value)) {
1726 + return '';
1727 + }
1728 +
1729 + if (is_numeric($value)) {
1730 + $url = wp_get_attachment_image_url((int) $value, $size);
1731 + return $url ? $url : '';
1732 + }
1733 +
1734 + return (string) $value;
1724 1735 }
1725 1736
1726 1737 public function local_seo_url_callback()
1727 1738 {
1728 1739 printf(
1729 - '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1730 - isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : home_url()
1740 + '<input type="text" id="local_seo_url" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_url]" value="%s" size="50" />',
1741 + isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : esc_attr(home_url())
1731 1742 );
1732 1743
1733 1744 printf(' <br> <span class="description"> URL of the item. </span>');
1734 1745 }
@@ -1735,9 +1746,9 @@
1735 1746
1736 1747 public function local_seo_email_callback()
1737 1748 {
1738 1749 printf(
1739 - '<input type="text" id="local_seo_email" name="' . Metasync_Admin::option_key . '[localseo][local_seo_email]" value="%s" size="50" />',
1750 + '<input type="text" id="local_seo_email" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_email]" value="%s" size="50" />',
1740 1751 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1741 1752 );
1742 1753
1743 1754 printf(' <br> <span class="description"> Search engines display your email address. </span>');
@@ -1745,9 +1756,9 @@
1745 1756
1746 1757 public function local_seo_phone_callback()
1747 1758 {
1748 1759 printf(
1749 - '<input type="text" id="local_seo_phone" name="' . Metasync_Admin::option_key . '[localseo][local_seo_phone]" value="%s" size="50" />',
1760 + '<input type="text" id="local_seo_phone" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_phone]" value="%s" size="50" />',
1750 1761 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1751 1762 );
1752 1763
1753 1764 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
@@ -1755,29 +1766,29 @@
1755 1766
1756 1767 public function local_seo_address_callback()
1757 1768 {
1758 1769 printf(
1759 - '<input type="text" id="local_seo_address_street" name="' . Metasync_Admin::option_key . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>',
1770 + '<input type="text" id="local_seo_address_street" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>',
1760 1771 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1761 1772 );
1762 1773
1763 1774 printf(
1764 - '<input type="text" id="local_seo_address_locality" name="' . Metasync_Admin::option_key . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>',
1775 + '<input type="text" id="local_seo_address_locality" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>',
1765 1776 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1766 1777 );
1767 1778
1768 1779 printf(
1769 - '<input type="text" id="local_seo_address_region" name="' . Metasync_Admin::option_key . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>',
1780 + '<input type="text" id="local_seo_address_region" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>',
1770 1781 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1771 1782 );
1772 1783
1773 1784 printf(
1774 - '<input type="text" id="local_seo_address_postalcode" name="' . Metasync_Admin::option_key . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>',
1785 + '<input type="text" id="local_seo_address_postalcode" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>',
1775 1786 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1776 1787 );
1777 1788
1778 1789 printf(
1779 - '<input type="text" id="local_seo_address_country" name="' . Metasync_Admin::option_key . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>',
1790 + '<input type="text" id="local_seo_address_country" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>',
1780 1791 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1781 1792 );
1782 1793 }
1783 1794
@@ -1792,9 +1803,9 @@
1792 1803 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1793 1804 <option value='0'>Select Business Type</option>
1794 1805 <?php
1795 1806 foreach ($types as $type) {
1796 - printf('<option value="%s" %s >%s</option>', $type, selected($type, esc_attr($business_type)), $type);
1807 + printf('<option value="%s" %s >%s</option>', esc_attr($type), selected($type, $business_type, false), esc_html($type));
1797 1808 }
1798 1809 ?>
1799 1810 </select>
1800 1811 <?php
@@ -1818,9 +1829,9 @@
1818 1829 <li>
1819 1830 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1820 1831 <?php
1821 1832 foreach ($days_name as $name) {
1822 - printf('<option value="%s" %s >%s</option>', $name, selected(esc_attr($name), esc_attr($day_name)), esc_attr($name));
1833 + printf('<option value="%s" %s >%s</option>', esc_attr($name), selected($name, $day_name, false), esc_html($name));
1823 1834 }
1824 1835 ?>
1825 1836 </select>
1826 1837 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
@@ -1899,9 +1910,9 @@
1899 1910
1900 1911 public function local_seo_price_range_callback()
1901 1912 {
1902 1913 printf(
1903 - '<input type="text" id="local_seo_price_range" name="' . Metasync_Admin::option_key . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1914 + '<input type="text" id="local_seo_price_range" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1904 1915 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1905 1916 );
1906 1917 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1907 1918 }
@@ -1914,9 +1925,9 @@
1914 1925 <?php
1915 1926 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1916 1927 $pages = get_pages();
1917 1928 foreach ($pages as $page) {
1918 - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($about_page)), $page->post_title);
1929 + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $about_page, false), esc_html($page->post_title));
1919 1930 }
1920 1931 ?>
1921 1932 </select>
1922 1933 <?php
@@ -1931,9 +1942,9 @@
1931 1942 <?php
1932 1943 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1933 1944 $pages = get_pages();
1934 1945 foreach ($pages as $page) {
1935 - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($contact_page)), $page->post_title);
1946 + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $contact_page, false), esc_html($page->post_title));
1936 1947 }
1937 1948 ?>
1938 1949 </select>
1939 1950 <?php
@@ -1942,9 +1953,9 @@
1942 1953
1943 1954 public function local_seo_map_key_callback()
1944 1955 {
1945 1956 printf(
1946 - '<input type="text" id="local_seo_map_key" name="' . Metasync_Admin::option_key . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1957 + '<input type="text" id="local_seo_map_key" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1947 1958 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1948 1959 );
1949 1960
1950 1961 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
@@ -1952,9 +1963,9 @@
1952 1963
1953 1964 public function local_seo_geo_coordinates_callback()
1954 1965 {
1955 1966 printf(
1956 - '<input type="text" id="local_seo_geo_coordinates" name="' . Metasync_Admin::option_key . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1967 + '<input type="text" id="local_seo_geo_coordinates" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1957 1968 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1958 1969 );
1959 1970
1960 1971 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
@@ -1962,9 +1973,9 @@
1962 1973
1963 1974 public function header_snippets_callback()
1964 1975 {
1965 1976 printf(
1966 - '<textarea class="wide-text" id="header_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][header_snippet]" >%s</textarea>',
1977 + '<textarea class="wide-text" id="header_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][header_snippet]" >%s</textarea>',
1967 1978 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1968 1979 );
1969 1980 }
1970 1981
@@ -1970,163 +1981,13 @@
1970 1981
1971 1982 public function footer_snippets_callback()
1972 1983 {
1973 1984 printf(
1974 - '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][footer_snippet]" >%s</textarea>',
1985 + '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][footer_snippet]" >%s</textarea>',
1975 1986 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1976 1987 );
1977 1988 }
1978 1989
1979 - public function no_index_posts_callback()
1980 - {
1981 - printf(
1982 - '<input type="checkbox" id="no_index_posts" name="' . Metasync_Admin::option_key . '[optimal_settings][no_index_posts]" value="true" %s />',
1983 - isset(Metasync::get_option('optimal_settings')['no_index_posts']) && Metasync::get_option('optimal_settings')['no_index_posts'] == 'true' ? 'checked' : ''
1984 - );
1985 -
1986 - printf(' <br> <span class="description"> Setting empty archives to <code>noindex</code> is useful for avoiding indexation of thin content pages and dilution of page rank. As soon as a post is added, the page is updated to index. </span>');
1987 - }
1988 -
1989 - public function no_follow_links_callback()
1990 - {
1991 - printf(
1992 - '<input type="checkbox" id="no_follow_links" name="' . Metasync_Admin::option_key . '[optimal_settings][no_follow_links]" value="true" %s />',
1993 - isset(Metasync::get_option('optimal_settings')['no_follow_links']) && Metasync::get_option('optimal_settings')['no_follow_links'] == 'true' ? 'checked' : ''
1994 - );
1995 -
1996 - printf(' <br> <span class="description"> Automatically add <code>rel="nofollow"</code> attribute to external links appearing in your posts, pages, and other post types. The attribute is dynamically applied when the url is displayed</span>');
1997 - }
1998 -
1999 - public function open_external_links_callback()
2000 - {
2001 - printf(
2002 - '<input type="checkbox" id="open_external_links" name="' . Metasync_Admin::option_key . '[optimal_settings][open_external_links]" value="true" %s />',
2003 - isset(Metasync::get_option('optimal_settings')['open_external_links']) && Metasync::get_option('optimal_settings')['open_external_links'] == 'true' ? 'checked' : ''
2004 - );
2005 -
2006 - printf(' <br> <span class="description"> Automatically add <code>target="_blank"</code> attribute to external links appearing in your posts, pages, and other post types. The attribute is applied when the url is displayed.</span>');
2007 - }
2008 -
2009 - public function add_alt_image_tags_callback()
2010 - {
2011 - printf(
2012 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_alt_image_tags]" value="true" %s />',
2013 - isset(Metasync::get_option('optimal_settings')['add_alt_image_tags']) && Metasync::get_option('optimal_settings')['add_alt_image_tags'] == 'true' ? 'checked' : ''
2014 - );
2015 -
2016 - printf(' <br> <span class="description"> Automatically add <code>alt</code> attribute to Image Tags appearing in your posts, pages, and other post types. The attribute is applied when the content is displayed.</span>');
2017 - }
2018 -
2019 - public function add_title_image_tags_callback()
2020 - {
2021 - printf(
2022 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_title_image_tags]" value="true" %s />',
2023 - isset(Metasync::get_option('optimal_settings')['add_title_image_tags']) && Metasync::get_option('optimal_settings')['add_title_image_tags'] == 'true' ? 'checked' : ''
2024 - );
2025 -
2026 - printf(' <br> <span class="description"> Automatically add <code>title</code> attribute to Image Tags appearing in your posts, pages, and other post types. The attribute is applied when the content is displayed.</span>');
2027 - }
2028 -
2029 - public function site_type_callback()
2030 - {
2031 -
2032 - $site_type = Metasync::get_option('optimal_settings')['site_info']['type'] ?? '';
2033 -
2034 - $types = [
2035 - ['name' => 'Personal Blog', 'value' => 'blog'],
2036 - ['name' => 'Community Blog/News Site', 'value' => 'news'],
2037 - ['name' => 'Personal Portfolio', 'value' => 'portfolio'],
2038 - ['name' => 'Small Business Site', 'value' => 'business'],
2039 - ['name' => 'Webshop', 'value' => 'webshop'],
2040 - ['name' => 'Other Personal Website', 'value' => 'otherpersonal'],
2041 - ['name' => 'Other Business Website', 'value' => 'otherbusiness'],
2042 - ];
2043 -
2044 - ?>
2045 - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][type]') ?>" id="site_info_type">
2046 - <?php
2047 - foreach ($types as $type) {
2048 - printf('<option value="%s" %s >%s</option>', esc_attr($type['value']), selected(esc_attr($type['value']), esc_attr($site_type)), ($type['name']));
2049 - }
2050 - ?>
2051 - </select>
2052 - <?php
2053 -
2054 - }
2055 -
2056 - public function site_business_type_callback()
2057 - {
2058 -
2059 - $business_type = Metasync::get_option('optimal_settings')['site_info']['business_type'] ?? '';
2060 -
2061 - $types = self::get_business_types();
2062 - sort($types);
2063 -
2064 - ?>
2065 - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][business_type]') ?>">
2066 - <option value='0'>Select Business Type</option>
2067 - <?php
2068 - foreach ($types as $type) {
2069 - printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($business_type)), esc_attr($type));
2070 - }
2071 - ?>
2072 - </select>
2073 - <?php
2074 - }
2075 -
2076 - public function site_company_name_callback()
2077 - {
2078 -
2079 - $company_name = Metasync::get_option('optimal_settings')['site_info']['company_name'] ?? get_bloginfo('name');
2080 -
2081 - printf(
2082 - '<input type="text" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][company_name]" value="%s" size="50" />',
2083 - $company_name ? $company_name : get_bloginfo('name')
2084 - );
2085 - }
2086 -
2087 - public function site_google_logo_callback()
2088 - {
2089 -
2090 - $google_logo = Metasync::get_option('optimal_settings')['site_info']['google_logo'] ?? '';
2091 -
2092 - printf(
2093 - '<input type="hidden" id="site_google_logo" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][google_logo]" value="%s" size="50" />',
2094 - $google_logo
2095 - );
2096 -
2097 - printf(' <br> <input class="button-secondary" type="button" id="google_logo_btn" value="Add or Upload File">');
2098 - printf(' <br><br> <span class="description bold"> Min Size: 160X90px, Max Size: 1920X1080px. </span> <br> <span class="description"> A squared image is preferred by the search engines. </span> <br><br> ');
2099 - printf('<img src="%s" id="site_google_logo_img" width="300">', wp_get_attachment_image_src($google_logo, 'medium')[0] ?? '');
2100 -
2101 - $button_type = 'hidden';
2102 - if ($google_logo) {
2103 - $button_type = 'button';
2104 - }
2105 - printf('<input type="%s" class="button-secondary" id="site_google_logo_close_btn" value="X">', $button_type);
2106 - }
2107 -
2108 - public function site_social_share_image_callback()
2109 - {
2110 -
2111 - $social_share_image = Metasync::get_option('optimal_settings')['site_info']['social_share_image'] ?? '';
2112 -
2113 - printf(
2114 - '<input type="hidden" id="site_social_share_image" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][social_share_image]" value="%s" size="50" />',
2115 - $social_share_image
2116 - );
2117 -
2118 - printf(' <br> <input class="button-secondary" type="button" id="social_share_image_btn" value="Add or Upload File">');
2119 - printf(' <br><br> <span class="description bold"> The recommended image size is 1200 x 630 pixels. </span> <br> <span class="description"> When a featured image or an OpenGraph Image is not set for individual posts/pages/CPTs, this image will be used as a fallback thumbnail when your post is shared on Facebook. </span> <br><br> ');
2120 - printf('<img src="%s" id="site_social_share_img" width="300">', wp_get_attachment_image_src($social_share_image, 'medium')[0] ?? '');
2121 -
2122 - $button_type = 'hidden';
2123 - if ($social_share_image) {
2124 - $button_type = 'button';
2125 - }
2126 - printf('<input type="%s" class="button-secondary" id="site_social_image_close_btn" value="X">', $button_type);
2127 - }
2128 -
2129 1990 public function common_robot_meta_tags_callback()
2130 1991 {
2131 1992 $common_robots_meta = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
2132 1993
@@ -2132,16 +1993,8 @@
2132 1993
2133 1994 ?>
2134 1995 <ul class="checkbox-list">
2135 1996 <li>
2136 - <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][index]') ?>" id="robots_common1" value="index" <?php isset($common_robots_meta['index']) ? checked('index', $common_robots_meta['index']) : '' ?>>
2137 - <label for="robots_common1">Index </br>
2138 - <span class="description">
2139 - <span>Search engines to index and show these pages in the search results.</span>
2140 - </span>
2141 - </label>
2142 - </li>
2143 - <li>
2144 1997 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][noindex]') ?>" id="robots_common2" value="noindex" <?php isset($common_robots_meta['noindex']) ? checked('noindex', $common_robots_meta['noindex']) : '' ?>>
2145 1998 <label for="robots_common2">No Index </br>
2146 1999 <span class="description">
2147 2000 <span>Search engines not indexed and displayed this pages in search engine results</span>
@@ -2267,9 +2120,9 @@
2267 2120
2268 2121 public function global_open_graph_meta_callback()
2269 2122 {
2270 2123 printf(
2271 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2124 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2272 2125 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
2273 2126 );
2274 2127 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
2275 2128 }
@@ -2276,9 +2129,9 @@
2276 2129
2277 2130 public function global_facebook_meta_callback()
2278 2131 {
2279 2132 printf(
2280 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2133 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2281 2134 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
2282 2135 );
2283 2136 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
2284 2137 }
@@ -2285,9 +2138,9 @@
2285 2138
2286 2139 public function global_twitter_meta_callback()
2287 2140 {
2288 2141 printf(
2289 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2142 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2290 2143 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2291 2144 );
2292 2145 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2293 2146 }
@@ -2295,9 +2148,9 @@
2295 2148 public function og_image_dimensions_callback()
2296 2149 {
2297 2150 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2298 2151 printf(
2299 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2152 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2300 2153 $val === 'true' ? 'checked' : ''
2301 2154 );
2302 2155 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2303 2156 }
@@ -2305,9 +2158,9 @@
2305 2158 public function article_timestamps_callback()
2306 2159 {
2307 2160 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2308 2161 printf(
2309 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_timestamps]" value="true" %s />',
2162 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_timestamps]" value="true" %s />',
2310 2163 $val === 'true' ? 'checked' : ''
2311 2164 );
2312 2165 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2313 2166 }
@@ -2315,9 +2168,9 @@
2315 2168 public function article_author_callback()
2316 2169 {
2317 2170 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2318 2171 printf(
2319 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_author]" value="true" %s />',
2172 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_author]" value="true" %s />',
2320 2173 $val === 'true' ? 'checked' : ''
2321 2174 );
2322 2175 printf('<span class="description"> Output article:author using the post author website URL or archive URL (overridable per-post via _metasync_og_article_author meta).</span>');
2323 2176 }
@@ -2325,9 +2178,9 @@
2325 2178 public function article_section_callback()
2326 2179 {
2327 2180 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2328 2181 printf(
2329 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_section]" value="true" %s />',
2182 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_section]" value="true" %s />',
2330 2183 $val === 'true' ? 'checked' : ''
2331 2184 );
2332 2185 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2333 2186 }
@@ -2335,9 +2188,9 @@
2335 2188 public function article_tags_callback()
2336 2189 {
2337 2190 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2338 2191 printf(
2339 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_tags]" value="true" %s />',
2192 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_tags]" value="true" %s />',
2340 2193 $val === 'true' ? 'checked' : ''
2341 2194 );
2342 2195 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2343 2196 }
@@ -2345,9 +2198,9 @@
2345 2198 public function twitter_image_alt_callback()
2346 2199 {
2347 2200 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2348 2201 printf(
2349 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2202 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2350 2203 $val === 'true' ? 'checked' : ''
2351 2204 );
2352 2205 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2353 2206 }
@@ -2354,9 +2207,9 @@
2354 2207
2355 2208 public function facebook_page_url_callback()
2356 2209 {
2357 2210 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2358 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url));
2211 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url));
2359 2212 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2360 2213 }
2361 2214
2362 2215 public function facebook_authorship_callback()
@@ -2361,9 +2214,9 @@
2361 2214
2362 2215 public function facebook_authorship_callback()
2363 2216 {
2364 2217 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2365 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2218 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2366 2219 printf('<br><span class="description"> Enter Facebook profile URL to show Facebook Authorship when your articles are being shared on Facebook. eg: <code>https://www.facebook.com/shahrukh/</code> </span>');
2367 2220 }
2368 2221
2369 2222 public function facebook_admin_callback()
@@ -2368,9 +2221,9 @@
2368 2221
2369 2222 public function facebook_admin_callback()
2370 2223 {
2371 2224 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2372 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2225 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2373 2226 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2374 2227 }
2375 2228
2376 2229 public function facebook_app_callback()
@@ -2375,9 +2228,9 @@
2375 2228
2376 2229 public function facebook_app_callback()
2377 2230 {
2378 2231 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2379 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2232 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2380 2233 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2381 2234 }
2382 2235
2383 2236 public function facebook_secret_callback()
@@ -2382,9 +2235,9 @@
2382 2235
2383 2236 public function facebook_secret_callback()
2384 2237 {
2385 2238 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2386 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2239 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2387 2240 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2388 2241 }
2389 2242
2390 2243 public function twitter_username_callback()
@@ -2389,9 +2242,9 @@
2389 2242
2390 2243 public function twitter_username_callback()
2391 2244 {
2392 2245 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2393 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2246 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2394 2247 printf(' <br> <span class="description"> Twitter username of the author to add <code>twitter:creator</code> tag to post. eg: <code>MetaSync</code> </span>');
2395 2248 }
2396 2249
2397 2250 public static function get_business_types()
@@ -2566,8 +2419,14 @@
2566 2419 }
2567 2420
2568 2421 /**
2569 2422 * Render breadcrumbs settings section
2423 + *
2424 + * Rendered by the dedicated Breadcrumbs admin page. The three checkbox
2425 + * fields each ship a hidden companion input so their keys are always
2426 + * present in the submitted payload, which lets the save handlers treat an
2427 + * absent key as "this form does not manage that field" instead of
2428 + * "unchecked".
2570 2429 */
2571 2430 public function render_breadcrumbs_section() {
2572 2431 $options = Metasync::get_option('breadcrumbs', array());
2573 2432
@@ -2572,17 +2431,29 @@
2572 2431 $options = Metasync::get_option('breadcrumbs', array());
2573 2432
2574 2433 $defaults = array(
2575 2434 'enabled' => true,
2576 - 'separator' => '&raquo;',
2435 + 'separator' => '»',
2577 2436 'home_label' => 'Home',
2578 2437 'home_url' => '',
2579 2438 'show_current_page' => true,
2580 2439 'prefix_text' => '',
2581 2440 'archive_label_format' => '{name}',
2441 + 'disable_schema' => false,
2582 2442 );
2583 2443
2584 2444 $options = wp_parse_args($options, $defaults);
2445 +
2446 + # Older builds stored the separator as an HTML entity (e.g. "&raquo;").
2447 + # Decode before matching so the saved choice still lights up its radio.
2448 + $separator_presets = array('»', '/', '>', '·');
2449 + $separator_current = html_entity_decode((string) $options['separator'], ENT_QUOTES, 'UTF-8');
2450 + if ($separator_current === '') {
2451 + $separator_current = '»';
2452 + }
2453 + # A separator that predates the radio picker (or came from the old free
2454 + # text field) gets its own option so it stays selected and round-trips.
2455 + $separator_custom = in_array($separator_current, $separator_presets, true) ? '' : $separator_current;
2585 2456 ?>
2586 2457 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2587 2458
2588 2459 <!-- Enable/Disable -->
@@ -2587,8 +2458,9 @@
2587 2458
2588 2459 <!-- Enable/Disable -->
2589 2460 <div style="margin-bottom: 24px;">
2590 2461 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2462 + <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0">
2591 2463 <input type="checkbox"
2592 2464 name="metasync_options[breadcrumbs][enabled]"
2593 2465 value="1"
2594 2466 <?php checked($options['enabled'], 1); ?>
@@ -2593,8 +2465,9 @@
2593 2465 value="1"
2594 2466 <?php checked($options['enabled'], 1); ?>
2595 2467 style="width: 18px; height: 18px; cursor: pointer;">
2596 2468 <span style="font-weight: 500; color: var(--dashboard-text);">Enable breadcrumbs globally</span>
2469 + <?php Metasync::render_tooltip_icon('breadcrumbs_enabled', 'Shows a trail of links (e.g. Home > Blog > This Post) near the top of your pages so visitors can see where they are and navigate back up a level. Turn off to hide breadcrumbs everywhere.'); ?>
2597 2470 </label>
2598 2471 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2599 2472 Enable or disable breadcrumb navigation on your site
2600 2473 </p>
@@ -2599,8 +2472,25 @@
2599 2472 Enable or disable breadcrumb navigation on your site
2600 2473 </p>
2601 2474 </div>
2602 2475
2476 + <!-- Disable Schema Markup -->
2477 + <div style="margin-bottom: 24px;">
2478 + <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2479 + <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0">
2480 + <input type="checkbox"
2481 + name="metasync_options[breadcrumbs][disable_schema]"
2482 + value="1"
2483 + <?php checked($options['disable_schema'], 1); ?>
2484 + style="width: 18px; height: 18px; cursor: pointer;">
2485 + <span style="font-weight: 500; color: var(--dashboard-text);">Disable breadcrumb schema markup (JSON-LD)</span>
2486 + <?php Metasync::render_tooltip_icon('breadcrumbs_disable_schema', 'Leave off. Only check this if another SEO plugin (Yoast, Rank Math, etc.) already sends breadcrumb data to Google, so you don\'t send it twice.'); ?>
2487 + </label>
2488 + <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2489 + When checked, MetaSync will not output the <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">BreadcrumbList</code> JSON-LD in <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">&lt;head&gt;</code>. Use this if another SEO plugin already handles breadcrumb schema.
2490 + </p>
2491 + </div>
2492 +
2603 2493 <!-- Separator Character -->
2604 2494 <div style="margin-bottom: 24px;">
2605 2495 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2606 2496 Separator Character
@@ -2605,24 +2495,21 @@
2605 2495 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2606 2496 Separator Character
2607 2497 </label>
2608 2498 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2499 + <?php foreach ($separator_presets as $separator_option) : ?>
2609 2500 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2610 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2611 - <span style="color: var(--dashboard-text);">»</span>
2501 + <input type="radio" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_option); ?>" <?php checked($separator_current, $separator_option); ?>>
2502 + <span style="color: var(--dashboard-text);"><?php echo esc_html($separator_option); ?></span>
2612 2503 </label>
2613 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2614 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2615 - <span style="color: var(--dashboard-text);">/</span>
2616 - </label>
2617 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2618 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2619 - <span style="color: var(--dashboard-text);">></span>
2620 - </label>
2621 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2622 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2623 - <span style="color: var(--dashboard-text);">·</span>
2624 - </label>
2504 + <?php endforeach; ?>
2505 + <div style="display: flex; align-items: center; gap: 8px;">
2506 + <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2507 + <input type="radio" id="metasync-breadcrumb-separator-custom" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_custom); ?>" <?php checked($separator_custom !== ''); ?>>
2508 + <span style="color: var(--dashboard-text);">Custom</span>
2509 + </label>
2510 + <input type="text" value="<?php echo esc_attr($separator_custom); ?>" placeholder="~" maxlength="16" aria-label="Custom separator character" oninput="var r=document.getElementById('metasync-breadcrumb-separator-custom');r.value=this.value;r.checked=true" style="width: 80px; padding: 8px 10px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px;">
2511 + </div>
2625 2512 </div>
2626 2513 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2627 2514 Choose how to separate breadcrumb items
2628 2515 </p>
@@ -2662,8 +2549,9 @@
2662 2549
2663 2550 <!-- Show Current Page -->
2664 2551 <div style="margin-bottom: 24px;">
2665 2552 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2553 + <input type="hidden" name="metasync_options[breadcrumbs][show_current_page]" value="0">
2666 2554 <input type="checkbox"
2667 2555 name="metasync_options[breadcrumbs][show_current_page]"
2668 2556 value="1"
2669 2557 <?php checked($options['show_current_page'], 1); ?>
@@ -2694,8 +2582,9 @@
2694 2582 <!-- Archive Label Format -->
2695 2583 <div style="margin-bottom: 24px;">
2696 2584 <label for="breadcrumb_archive_format" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2697 2585 Archive Label Format
2586 + <?php Metasync::render_tooltip_icon('breadcrumb_archive_format', 'On category/archive pages, {name} is replaced with that section\'s name. Type {name} alone for just the name, or something like "Category: {name}".'); ?>
2698 2587 </label>
2699 2588 <input type="text"
2700 2589 id="breadcrumb_archive_format"
2701 2590 name="metasync_options[breadcrumbs][archive_label_format]"