PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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 +382 -548 2.6.6 → 2.7.1 View file →
@@ -46,9 +46,15 @@
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(
51 57 'connection' => array(
52 58 'title' => 'Connection & Authentication',
53 59 'description' => 'Manage your API connection and authentication settings',
54 60 'icon' => 'admin-network',
@@ -68,9 +74,12 @@
68 74 'fields' => array(
69 75 'otto_pixel_uuid',
70 76 'otto_disable_on_loggedin',
71 77 'otto_disable_preview_button',
78 + 'seo_priority',
72 79 'otto_wp_rocket_compat',
80 + 'otto_sg_optimizer_compat',
81 + 'bookingpress_session_compat',
73 82 )
74 83 ),
75 84 'edge_cache' => array(
76 85 'title' => 'Edge Cache / CDN',
@@ -79,8 +88,16 @@
79 88 'priority' => 22,
80 89 'default_open' => false,
81 90 'render_callback' => array('Metasync_Edge_Cache_Settings', 'render'),
82 91 ),
92 + 'seo_plugin_sync' => array(
93 + 'title' => 'Sync to Other SEO Plugins',
94 + 'description' => 'Control whether ' . Metasync::get_whitelabel_otto_name() . '\'s SEO values are written into Yoast, Rank Math or All in One SEO',
95 + 'icon' => 'update',
96 + 'priority' => 23,
97 + 'default_open' => false,
98 + 'render_callback' => array('Metasync_Seo_Sync_Settings', 'render'),
99 + ),
83 100 'bot_detection' => array(
84 101 'title' => 'Bot Detection & Filtering',
85 102 'description' => 'Manage bot traffic and reduce unnecessary API calls',
86 103 'icon' => 'filter',
@@ -92,16 +109,8 @@
92 109 'otto_bot_blacklist',
93 110 'otto_bot_statistics_link'
94 111 )
95 112 ),
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 113 'open_graph' => array(
105 114 'title' => 'Open Graph',
106 115 'description' => 'Control which Open Graph and article meta tags are output',
107 116 'icon' => 'share',
@@ -128,8 +137,10 @@
128 137 'disable_redirection_metabox',
129 138 'disable_canonical_metabox',
130 139 'disable_social_opengraph_metabox',
131 140 'disable_schema_markup_metabox',
141 + 'disable_language_alternates_metabox',
142 + 'disable_seo_metabox',
132 143 'open_external_links'
133 144 )
134 145 ),
135 146 'user_management' => array(
@@ -169,13 +180,22 @@
169 180 'fields' => array(
170 181 'permalink_structure',
171 182 'hide_dashboard_framework',
172 183 'show_admin_bar_status',
173 - 'enable_auto_updates',
174 184 'import_external_data'
175 185 )
176 186 )
177 187 );
188 +
189 + // Hidden only when there is nothing to say: no SEO plugin installed, the
190 + // switch off, and no saved originals. It is never hidden by the
191 + // access-control or whitelabel flags that can conceal Advanced Settings
192 + // — a switch that rewrites another plugin's data must stay reachable.
193 + if (!Metasync_Seo_Sync_Settings::should_display()) {
194 + unset($sections['seo_plugin_sync']);
195 + }
196 +
197 + return $sections;
178 198 }
179 199
180 200 public function get_advanced_accordion_config() {
181 201 $config = array();
@@ -200,8 +220,17 @@
200 220 'default_open' => false,
201 221 'render_callback' => array($this->admin_instance, 'render_debug_mode_section')
202 222 );
203 223
224 + $config['headless_mode'] = array(
225 + 'title' => 'Headless Mode',
226 + 'description' => 'Connect WordPress content to a separate public frontend',
227 + 'icon' => 'admin-site-alt3',
228 + 'priority' => 29,
229 + 'default_open' => false,
230 + 'render_callback' => array(Metasync_Settings_Registration::instance(), 'render_headless_mode_section')
231 + );
232 +
204 233 $config['error_logs'] = array(
205 234 'title' => 'Error Logs',
206 235 'description' => 'View and manage error logs to troubleshoot issues',
207 236 'icon' => 'warning',
@@ -274,9 +303,9 @@
274 303 $content_state = $is_open ? 'open' : 'closed';
275 304
276 305 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
277 306
278 - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
307 + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
279 308 echo '<div class="metasync-accordion-title">';
280 309 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
281 310 echo '<div class="metasync-accordion-text">';
282 311 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -287,9 +316,9 @@
287 316 echo '<span class="toggle-icon">▼</span>';
288 317 echo '</button>';
289 318 echo '</div>';
290 319
291 - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
320 + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
292 321
293 322 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
294 323 call_user_func($section_data['render_callback']);
295 324 }
@@ -314,8 +343,10 @@
314 343 <li>All API keys and authentication tokens</li>
315 344 <li>White label branding settings</li>
316 345 <li>Plugin configuration and preferences</li>
317 346 <li>Instant indexing settings</li>
347 + <li>Media optimization settings and cache</li>
348 + <li>Code minification settings</li>
318 349 <li>All cached data and crawl information</li>
319 350 </ul>
320 351 <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin completely after this reset.</p>
321 352 </div>
@@ -326,8 +357,31 @@
326 357 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear All Settings
327 358 </button>
328 359 </form>
329 360 </div>
361 + <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px; margin-top: 20px;">
362 + <h4 style="color: var(--dashboard-text-primary, #1e1e1e); margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
363 + <span class="dashicons dashicons-universal-access-alt" style="font-size:18px;width:18px;height:18px;"></span>
364 + <span>Delete data on uninstall</span>
365 + </h4>
366 + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
367 + 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.
368 + </p>
369 + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">
370 + The Google service account and the IndexNow API key are always deleted on uninstall, regardless of this setting.
371 + </p>
372 + <form method="post" action="">
373 + <?php wp_nonce_field('metasync_uninstall_data_toggle_nonce', 'uninstall_data_toggle_nonce'); ?>
374 + <input type="hidden" name="metasync_uninstall_data_toggle_form" value="1" />
375 + <label style="color: var(--dashboard-text-primary, #1e1e1e); display: flex; align-items: center; gap: 8px; margin: 0 0 12px 0; cursor: pointer;">
376 + <input type="checkbox" name="metasync_delete_data_on_uninstall" value="yes" <?php checked(get_option('metasync_delete_data_on_uninstall', 'no'), 'yes'); ?> />
377 + Delete all plugin data (tables, settings, per-post SEO values, logs) when the plugin is uninstalled
378 + </label>
379 + <button type="submit" class="button button-secondary">
380 + Save Uninstall Setting
381 + </button>
382 + </form>
383 + </div>
330 384 <script>
331 385 function confirmClearSettings(event) {
332 386 event.preventDefault();
333 387
@@ -335,9 +389,9 @@
335 389 if (!firstConfirm) {
336 390 return false;
337 391 }
338 392
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.");
393 + 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• Media optimization settings and cache\n• Code minification 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.");
340 394 if (!secondConfirm) {
341 395 return false;
342 396 }
343 397
@@ -358,14 +412,18 @@
358 412 if (!function_exists('google_index_direct')) {
359 413 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
360 414 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
361 415 } else {
362 - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
416 + 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
363 417 return;
364 418 }
365 419 }
366 420
367 421 $google_index = google_index_direct();
422 + if (!$google_index) {
423 + // Nothing to render against, and calling through would be fatal.
424 + return;
425 + }
368 426 $service_info = $google_index->get_service_account_info();
369 427 $is_configured = !isset($service_info['error']);
370 428
371 429 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
@@ -442,8 +500,9 @@
442 500 <table class="form-table" style="margin-top: 0;">
443 501 <tr>
444 502 <th scope="row" style="width: 200px; padding-top: 15px;">
445 503 <label for="metasync_bing_api_key_inline">IndexNow API Key <span style="color: #d63638;">*</span></label>
504 + <?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.'); ?>
446 505 </th>
447 506 <td style="padding-top: 15px;">
448 507 <input type="text"
449 508 name="metasync_bing_api_key_inline"
@@ -643,9 +702,9 @@
643 702 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
644 703 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
645 704 </p>
646 705
647 - <form method="post" action="<?php echo admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced'); ?>" id="plugin-access-roles-form">
706 + <form method="post" action="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced')); ?>" id="plugin-access-roles-form">
648 707 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
649 708 <input type="hidden" name="save_plugin_access_roles" value="yes" />
650 709
651 710 <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;">
@@ -737,8 +796,9 @@
737 796 'max_memory_limit' => 256,
738 797 'log_batch_size' => 1000,
739 798 'action_scheduler_batches' => 1,
740 799 'otto_rate_limit' => 10,
800 + 'otto_cache_ttl' => 30,
741 801 'queue_cleanup_days' => 31
742 802 );
743 803 }
744 804
@@ -762,9 +822,9 @@
762 822
763 823 public function can_change_memory_limit() {
764 824 $current_limit = ini_get('memory_limit');
765 825
766 - $test_result = @ini_set('memory_limit', $current_limit);
826 + $test_result = @ini_set('memory_limit', $current_limit); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
767 827
768 828 return $test_result !== false;
769 829 }
770 830
@@ -770,37 +830,47 @@
770 830
771 831 public function get_server_limits() {
772 832 $max_execution_time = ini_get('max_execution_time');
773 833 $memory_limit = ini_get('memory_limit');
774 -
834 +
835 + // Prefer the server-configured value: WordPress (wp_raise_memory_limit)
836 + // or this plugin (apply_memory_limit) may have already changed the
837 + // runtime limit via ini_set, so ini_get no longer reflects the server
838 + // configuration. get_cfg_var reads the php.ini value untouched.
839 + $config_memory_limit = get_cfg_var('memory_limit');
840 + if ($config_memory_limit !== false && $config_memory_limit !== '') {
841 + $memory_limit = $config_memory_limit;
842 + }
843 +
775 844 $memory_limit_mb = $this->parse_memory_limit_to_mb($memory_limit);
776 -
777 - $wp_memory_limit = null;
778 - if (defined('WP_MAX_MEMORY_LIMIT')) {
779 - $wp_memory_limit = WP_MAX_MEMORY_LIMIT;
780 - } elseif (defined('WP_MEMORY_LIMIT')) {
781 - $wp_memory_limit = WP_MEMORY_LIMIT;
782 - }
783 - $wp_memory_limit_mb = $wp_memory_limit ? $this->parse_memory_limit_to_mb($wp_memory_limit) : null;
784 -
785 - $actual_php_limit_mb = $memory_limit_mb;
786 - if ($wp_memory_limit_mb && $wp_memory_limit_mb >= 256) {
787 - $actual_php_limit_mb = 128;
788 - }
789 -
845 +
790 846 $can_change_memory = $this->can_change_memory_limit();
791 -
792 - $memory_limit_display = $actual_php_limit_mb == -1 ? 'Unlimited' : $actual_php_limit_mb . ' MB';
793 -
847 +
848 + $memory_limit_display = $memory_limit_mb == -1 ? 'Unlimited' : $memory_limit_mb . ' MB';
849 +
794 850 return array(
795 851 'max_execution_time' => $max_execution_time == -1 ? 'Unlimited' : $max_execution_time . ' seconds',
796 852 'memory_limit' => $memory_limit_display,
797 853 'max_execution_time_raw' => $max_execution_time,
798 - 'memory_limit_raw' => $actual_php_limit_mb,
854 + 'memory_limit_raw' => $memory_limit_mb,
799 855 'can_change_memory' => $can_change_memory
800 856 );
801 857 }
802 858
859 + /**
860 + * Allowed range for the Max Memory Limit setting: 64 MB up to the
861 + * server-configured memory limit (null max when the server is unlimited).
862 + */
863 + public function get_memory_limit_allowed_range() {
864 + $server_limits = $this->get_server_limits();
865 + $max = $server_limits['memory_limit_raw'] == -1 ? null : max(64, (int) $server_limits['memory_limit_raw']);
866 +
867 + return array(
868 + 'min' => 64,
869 + 'max' => $max
870 + );
871 + }
872 +
803 873 public function apply_memory_limit() {
804 874 if (!$this->can_change_memory_limit()) {
805 875 return false;
806 876 }
@@ -806,9 +876,9 @@
806 876 }
807 877
808 878 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
809 879
810 - $result = @ini_set('memory_limit', $memory_limit_mb . 'M');
880 + $result = @ini_set('memory_limit', $memory_limit_mb . 'M'); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
811 881
812 882 return $result !== false;
813 883 }
814 884
@@ -837,11 +907,12 @@
837 907
838 908 public function render_execution_settings_section() {
839 909 $settings = $this->get_all_execution_settings();
840 910 $server_limits = $this->get_server_limits();
911 + $memory_range = $this->get_memory_limit_allowed_range();
841 912
842 913 $warnings = array();
843 - if ($server_limits['max_execution_time_raw'] != -1 && $settings['max_execution_time'] > $server_limits['max_execution_time_raw']) {
914 + if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) {
844 915 $warnings['max_execution_time'] = true;
845 916 }
846 917 if ($server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
847 918 $warnings['max_memory_limit'] = true;
@@ -857,16 +928,18 @@
857 928
858 929 <!-- Execution & Memory Section -->
859 930 <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;">
860 931 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Execution & Memory</h3>
861 -
862 - <div style="margin-bottom: 20px;">
932 +
933 + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
934 + <div>
863 935 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
864 936 Max Execution Time:
937 + <?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.'); ?>
865 938 </label>
866 939 <div style="display: flex; align-items: center; gap: 10px;">
867 - <input type="number"
868 - id="max_execution_time"
940 + <input type="number"
941 + id="max_execution_time"
869 942 name="max_execution_time"
870 943 value="<?php echo esc_attr($settings['max_execution_time']); ?>"
871 944 min="1"
872 945 max="300"
@@ -892,16 +965,17 @@
892 965
893 966 <div>
894 967 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
895 968 Max Memory Limit:
969 + <?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.'); ?>
896 970 </label>
897 971 <div style="display: flex; align-items: center; gap: 10px;">
898 - <input type="number"
899 - id="max_memory_limit"
972 + <input type="number"
973 + id="max_memory_limit"
900 974 name="max_memory_limit"
901 975 value="<?php echo esc_attr($settings['max_memory_limit']); ?>"
902 - min="64"
903 - max="512"
976 + min="<?php echo esc_attr($memory_range['min']); ?>"
977 + <?php if ($memory_range['max'] !== null): ?>max="<?php echo esc_attr($memory_range['max']); ?>"<?php endif; ?>
904 978 <?php if (!$server_limits['can_change_memory']): ?>
905 979 readonly
906 980 disabled
907 981 <?php endif; ?>
@@ -909,9 +983,9 @@
909 983 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'; ?>;" />
910 984 <span style="color: var(--dashboard-text-secondary);">MB</span>
911 985 </div>
912 986 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;">
913 - Server Limit: <?php echo esc_html($server_limits['memory_limit']); ?>
987 + 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'); ?>
914 988 </p>
915 989 <?php if (!$server_limits['can_change_memory']): ?>
916 990 <p style="color: #f59e0b; font-size: 12px; margin: 4px 0 0 0; display: flex; align-items: center; gap: 4px;">
917 991 <span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
@@ -930,21 +1004,24 @@
930 1004 </script>
931 1005 <?php endif; ?>
932 1006 <?php endif; ?>
933 1007 </div>
1008 + </div>
934 1009 </div>
935 -
1010 +
936 1011 <!-- Batch Processing Section -->
937 1012 <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;">
938 1013 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Batch Processing</h3>
939 -
940 - <div style="margin-bottom: 20px;">
1014 +
1015 + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1016 + <div>
941 1017 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
942 1018 Log Processing Batch Size:
1019 + <?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.'); ?>
943 1020 </label>
944 1021 <div style="display: flex; align-items: center; gap: 10px;">
945 - <input type="number"
946 - id="log_batch_size"
1022 + <input type="number"
1023 + id="log_batch_size"
947 1024 name="log_batch_size"
948 1025 value="<?php echo esc_attr($settings['log_batch_size']); ?>"
949 1026 min="100"
950 1027 max="5000"
@@ -956,8 +1033,9 @@
956 1033
957 1034 <div>
958 1035 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
959 1036 Action Scheduler Concurrent Batches:
1037 + <?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.'); ?>
960 1038 </label>
961 1039 <div style="display: flex; align-items: center; gap: 10px;">
962 1040 <input type="number"
963 1041 id="action_scheduler_batches"
@@ -972,17 +1050,20 @@
972 1050 <span class="dashicons dashicons-warning" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
973 1051 <span>Higher values increase server load</span>
974 1052 </p>
975 1053 </div>
1054 + </div>
976 1055 </div>
977 -
1056 +
978 1057 <!-- API Rate Limiting Section -->
979 1058 <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;">
980 1059 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">API Rate Limiting</h3>
981 -
982 - <div style="margin-bottom: 20px;">
1060 +
1061 + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1062 + <div>
983 1063 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
984 - OTTO API Calls Per Minute:
1064 + <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API Calls Per Minute:
1065 + <?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.'); ?>
985 1066 </label>
986 1067 <div style="display: flex; align-items: center; gap: 10px;">
987 1068 <input type="number"
988 1069 id="otto_rate_limit"
@@ -997,8 +1078,9 @@
997 1078
998 1079 <div>
999 1080 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1000 1081 Queue Auto-Cleanup:
1082 + <?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.'); ?>
1001 1083 </label>
1002 1084 <div style="display: flex; align-items: center; gap: 10px;">
1003 1085 <input type="number"
1004 1086 id="queue_cleanup_days"
@@ -1010,10 +1092,11 @@
1010 1092 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1011 1093 <span style="color: var(--dashboard-text-secondary);">days</span>
1012 1094 </div>
1013 1095 </div>
1096 + </div>
1014 1097 </div>
1015 -
1098 +
1016 1099 <!-- Save Button -->
1017 1100 <div style="margin-top: 20px;">
1018 1101 <button type="submit"
1019 1102 id="metasync-execution-settings-save-btn"
@@ -1028,232 +1111,15 @@
1028 1111 <div id="metasync-execution-settings-message" style="display: none; margin-top: 16px; padding: 12px; border-radius: 6px;"></div>
1029 1112 </form>
1030 1113 </div>
1031 1114
1032 - <script>
1033 - jQuery(document).ready(function($) {
1034 - var $form = $('#metasync-execution-settings-form');
1035 - var $saveBtn = $('#metasync-execution-settings-save-btn');
1036 - var $message = $('#metasync-execution-settings-message');
1037 -
1038 - var serverMaxExecTime = <?php echo $server_limits['max_execution_time_raw'] == -1 ? 'Infinity' : $server_limits['max_execution_time_raw']; ?>;
1039 - var serverMaxMemory = <?php echo $server_limits['memory_limit_raw'] == -1 ? 'Infinity' : $server_limits['memory_limit_raw']; ?>;
1040 - var canChangeMemory = <?php echo $server_limits['can_change_memory'] ? 'true' : 'false'; ?>;
1041 -
1042 - function checkServerLimits() {
1043 - var maxExecTime = parseInt($('#max_execution_time').val()) || 0;
1044 - var maxMemory = parseInt($('#max_memory_limit').val()) || 0;
1045 -
1046 - if (serverMaxExecTime !== Infinity && maxExecTime > serverMaxExecTime) {
1047 - $('#max_execution_time_warning').show();
1048 - } else {
1049 - $('#max_execution_time_warning').hide();
1050 - }
1051 -
1052 - if (canChangeMemory && serverMaxMemory !== Infinity && maxMemory > serverMaxMemory) {
1053 - $('#max_memory_limit_warning').show();
1054 - } else {
1055 - $('#max_memory_limit_warning').hide();
1056 - }
1057 - }
1058 -
1059 - $('#max_execution_time, #max_memory_limit').on('input change', function() {
1060 - checkServerLimits();
1061 - $(this).css('border-color', 'var(--dashboard-border)');
1062 - });
1063 -
1064 - function highlightInvalidField($field, isValid) {
1065 - if (isValid) {
1066 - $field.css({
1067 - 'border-color': 'var(--dashboard-border)',
1068 - 'box-shadow': 'none'
1069 - });
1070 - } else {
1071 - $field.css({
1072 - 'border-color': '#ef4444',
1073 - 'box-shadow': '0 0 0 3px rgba(239, 68, 68, 0.1)'
1074 - });
1075 - }
1076 - }
1077 -
1078 - $('#max_execution_time').on('blur', function() {
1079 - var value = parseInt($(this).val()) || 0;
1080 - var isValid = value >= 1 && value <= 300 && (serverMaxExecTime === Infinity || value <= serverMaxExecTime);
1081 - highlightInvalidField($(this), isValid);
1082 - });
1083 -
1084 - $('#max_memory_limit').on('blur', function() {
1085 - if (!canChangeMemory) return;
1086 - var value = parseInt($(this).val()) || 0;
1087 - var isValid = value >= 64 && value <= 512 && (serverMaxMemory === Infinity || value <= serverMaxMemory);
1088 - highlightInvalidField($(this), isValid);
1089 - });
1090 -
1091 - checkServerLimits();
1092 -
1093 - $form.on('submit', function(e) {
1094 - e.preventDefault();
1095 -
1096 - var formData = {
1097 - action: 'metasync_save_execution_settings',
1098 - execution_settings_nonce: $('#execution_settings_nonce').val(),
1099 - max_execution_time: $('#max_execution_time').val(),
1100 - max_memory_limit: $('#max_memory_limit').val(),
1101 - log_batch_size: $('#log_batch_size').val(),
1102 - action_scheduler_batches: $('#action_scheduler_batches').val(),
1103 - otto_rate_limit: $('#otto_rate_limit').val(),
1104 - queue_cleanup_days: $('#queue_cleanup_days').val()
1105 - };
1106 -
1107 - $('input[type="number"]').css({
1108 - 'border-color': 'var(--dashboard-border)',
1109 - 'box-shadow': 'none'
1110 - });
1111 -
1112 - var hasError = false;
1113 - var errorField = null;
1114 -
1115 - if (formData.max_execution_time < 1 || formData.max_execution_time > 300) {
1116 - showMessage('Max Execution Time must be between 1 and 300 seconds.', 'error');
1117 - highlightInvalidField($('#max_execution_time'), false);
1118 - errorField = $('#max_execution_time');
1119 - hasError = true;
1120 - } else if (serverMaxExecTime !== Infinity && formData.max_execution_time > serverMaxExecTime) {
1121 - showMessage('Max Execution Time exceeds server limit of ' + serverMaxExecTime + ' seconds. Please reduce the value.', 'error');
1122 - highlightInvalidField($('#max_execution_time'), false);
1123 - errorField = $('#max_execution_time');
1124 - hasError = true;
1125 - }
1126 -
1127 - if (canChangeMemory) {
1128 - if (formData.max_memory_limit < 64 || formData.max_memory_limit > 512) {
1129 - showMessage('Max Memory Limit must be between 64 and 512 MB.', 'error');
1130 - highlightInvalidField($('#max_memory_limit'), false);
1131 - if (!hasError) {
1132 - errorField = $('#max_memory_limit');
1133 - hasError = true;
1134 - }
1135 - } else if (serverMaxMemory !== Infinity && formData.max_memory_limit > serverMaxMemory) {
1136 - showMessage('Max Memory Limit exceeds server limit of ' + serverMaxMemory + ' MB. Please reduce the value.', 'error');
1137 - highlightInvalidField($('#max_memory_limit'), false);
1138 - if (!hasError) {
1139 - errorField = $('#max_memory_limit');
1140 - hasError = true;
1141 - }
1142 - }
1143 - }
1144 -
1145 - if (formData.log_batch_size < 100 || formData.log_batch_size > 5000) {
1146 - showMessage('Log Batch Size must be between 100 and 5000 lines.', 'error');
1147 - highlightInvalidField($('#log_batch_size'), false);
1148 - if (!hasError) {
1149 - errorField = $('#log_batch_size');
1150 - hasError = true;
1151 - }
1152 - }
1153 - if (formData.action_scheduler_batches < 1 || formData.action_scheduler_batches > 10) {
1154 - showMessage('Action Scheduler Batches must be between 1 and 10.', 'error');
1155 - highlightInvalidField($('#action_scheduler_batches'), false);
1156 - if (!hasError) {
1157 - errorField = $('#action_scheduler_batches');
1158 - hasError = true;
1159 - }
1160 - }
1161 - if (formData.otto_rate_limit < 1 || formData.otto_rate_limit > 60) {
1162 - showMessage('OTTO Rate Limit must be between 1 and 60 calls per minute.', 'error');
1163 - highlightInvalidField($('#otto_rate_limit'), false);
1164 - if (!hasError) {
1165 - errorField = $('#otto_rate_limit');
1166 - hasError = true;
1167 - }
1168 - }
1169 - if (formData.queue_cleanup_days < 7 || formData.queue_cleanup_days > 90) {
1170 - showMessage('Queue Cleanup Days must be between 7 and 90 days.', 'error');
1171 - highlightInvalidField($('#queue_cleanup_days'), false);
1172 - if (!hasError) {
1173 - errorField = $('#queue_cleanup_days');
1174 - hasError = true;
1175 - }
1176 - }
1177 -
1178 - if (hasError) {
1179 - if (errorField) {
1180 - errorField.focus();
1181 - $('html, body').animate({
1182 - scrollTop: errorField.offset().top - 100
1183 - }, 300);
1184 - }
1185 - return;
1186 - }
1187 -
1188 - $saveBtn.prop('disabled', true);
1189 - $saveBtn.find('.save-text').text('Saving...');
1190 - $saveBtn.find('.save-spinner').show();
1191 - $message.hide();
1192 -
1193 - $.ajax({
1194 - url: ajaxurl,
1195 - type: 'POST',
1196 - data: formData,
1197 - success: function(response) {
1198 - if (response.success) {
1199 - showMessage(response.data.message || 'Settings saved successfully!', 'success');
1200 - $saveBtn.prop('disabled', false);
1201 - $saveBtn.find('.save-text').text('Save Settings');
1202 - $saveBtn.find('.save-spinner').hide();
1203 - setTimeout(function() {
1204 - checkServerLimits();
1205 - }, 100);
1206 - $('html, body').animate({
1207 - scrollTop: $form.offset().top - 100
1208 - }, 300);
1209 - } else {
1210 - showMessage(response.data.message || 'Error saving settings.', 'error');
1211 - $saveBtn.prop('disabled', false);
1212 - $saveBtn.find('.save-text').text('Save Settings');
1213 - $saveBtn.find('.save-spinner').hide();
1214 - $('html, body').animate({
1215 - scrollTop: $message.offset().top - 100
1216 - }, 300);
1217 - }
1218 - },
1219 - error: function() {
1220 - showMessage('An error occurred while saving settings. Please try again.', 'error');
1221 - $saveBtn.prop('disabled', false);
1222 - $saveBtn.find('.save-text').text('Save Settings');
1223 - $saveBtn.find('.save-spinner').hide();
1224 - $('html, body').animate({
1225 - scrollTop: $message.offset().top - 100
1226 - }, 300);
1227 - }
1228 - });
1229 - });
1230 -
1231 - function showMessage(text, type) {
1232 - $message.removeClass('notice-success notice-error')
1233 - .addClass('notice-' + type)
1234 - .css({
1235 - 'background': type === 'success' ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)',
1236 - 'border': '1px solid ' + (type === 'success' ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'),
1237 - 'color': type === 'success' ? '#22c55e' : '#ef4444',
1238 - 'padding': '12px 16px',
1239 - 'border-radius': '6px',
1240 - 'font-size': '14px',
1241 - 'line-height': '1.5',
1242 - 'display': 'block'
1243 - })
1244 - .html('<strong style="margin-right: 8px;">' + (type === 'success' ? '✓' : '✗') + '</strong>' + text)
1245 - .show();
1246 -
1247 - if (type === 'success') {
1248 - setTimeout(function() {
1249 - $message.fadeOut(300);
1250 - }, 5000);
1251 - }
1252 - }
1253 - });
1254 - </script>
1255 1115 <?php
1116 + // Real-time validation and AJAX save for this form live in
1117 + // admin/js/metasync-execution-settings.js (enqueued on this page).
1118 + // The previous inline duplicate was removed to avoid a double-bound
1119 + // submit handler that fired the save request twice.
1120 + ?>
1121 + <?php
1256 1122 }
1257 1123
1258 1124 public function get_field_tooltips() {
1259 1125 $plugin_name = Metasync::get_effective_plugin_name();
@@ -1259,33 +1125,43 @@
1259 1125 $plugin_name = Metasync::get_effective_plugin_name();
1260 1126 $otto_name = Metasync::get_whitelabel_otto_name();
1261 1127
1262 1128 return array(
1263 - '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),
1129 + '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),
1264 1130 '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),
1265 1131 '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),
1266 1132 '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),
1267 1133 '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),
1134 + 'seo_priority' => sprintf('Choose which value appears first on a page when both your custom SEO value and an approved %s suggestion are available. This changes what visitors see; it does not delete or overwrite saved values.', $otto_name),
1268 1135 '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),
1136 + '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),
1137 + '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.',
1269 1138 '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),
1270 1139 '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),
1271 1140 '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),
1272 1141 '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.',
1273 - '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.',
1274 - '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.',
1275 - '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.',
1276 - '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.',
1277 - '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.',
1278 - '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.',
1279 - 'open_external_links' => 'Automatically add target="_blank" attribute to external links appearing in your posts, pages, and other post types when rendered by Otto.',
1142 + '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')),
1143 + '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')),
1144 + '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.',
1145 + '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')),
1146 + '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')),
1147 + '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')),
1148 + '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')),
1149 + '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).',
1150 + '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),
1280 1151 '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.',
1281 1152 '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),
1282 1153 '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),
1283 1154 '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),
1284 - '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),
1285 1155 '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),
1286 - '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.',
1287 - '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.'
1156 + '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),
1157 + '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.',
1158 + '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.',
1159 + '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.',
1160 + '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.',
1161 + '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.',
1162 + '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.',
1163 + '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.'
1288 1164 );
1289 1165 }
1290 1166
1291 1167 public function get_field_section($field_id) {
@@ -1322,9 +1198,9 @@
1322 1198 $content_state = $is_open ? 'open' : 'closed';
1323 1199
1324 1200 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1325 1201
1326 - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
1202 + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">';
1327 1203 echo '<div class="metasync-accordion-title">';
1328 1204 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1329 1205 echo '<div class="metasync-accordion-text">';
1330 1206 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -1335,9 +1211,9 @@
1335 1211 echo '<span class="toggle-icon">▼</span>';
1336 1212 echo '</button>';
1337 1213 echo '</div>';
1338 1214
1339 - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
1215 + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">';
1340 1216
1341 1217 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1342 1218 call_user_func($section_data['render_callback']);
1343 1219 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
@@ -1352,9 +1228,9 @@
1352 1228 echo '<tr>';
1353 1229 echo '<th scope="row">';
1354 1230 if (!empty($field['title'])) {
1355 1231 echo '<div class="metasync-field-label-wrapper">';
1356 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1232 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1357 1233
1358 1234 if (isset($tooltips[$field_id])) {
1359 1235 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1360 1236 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">';
@@ -1400,13 +1276,22 @@
1400 1276 return;
1401 1277 }
1402 1278
1403 1279 echo '<table class="form-table" role="presentation">';
1280 + $llms_tooltips = array(
1281 + '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.',
1282 + '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.',
1283 + );
1404 1284 foreach ($wp_settings_fields[$page][$section] as $field) {
1405 1285 echo '<tr>';
1406 1286 echo '<th scope="row">';
1407 1287 if (!empty($field['title'])) {
1408 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1288 + echo '<div class="metasync-field-label-wrapper">';
1289 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1290 + if (isset($llms_tooltips[$field['id']])) {
1291 + Metasync::render_tooltip_icon($field['id'], $llms_tooltips[$field['id']]);
1292 + }
1293 + echo '</div>';
1409 1294 }
1410 1295 echo '</th>';
1411 1296 echo '<td>';
1412 1297 call_user_func($field['callback'], $field['args']);
@@ -1456,31 +1341,32 @@
1456 1341 $display_value = $current_token;
1457 1342 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1458 1343 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1459 1344 } else {
1460 - $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name());
1461 - $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . esc_html(Metasync::get_effective_plugin_name()) . '".';
1345 + $display_value = 'Auto-generated when connecting to ' . Metasync::get_effective_plugin_name();
1346 + $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . Metasync::get_effective_plugin_name() . '".';
1462 1347 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1463 1348 }
1464 1349
1350 + printf('<div class="metasync-auth-token-row">');
1465 1351 printf(
1466 - '<input type="text" id="apikey" name="' . Metasync_Admin::option_key . '[general][apikey]" value="%s" size="40" readonly="readonly" /> ',
1352 + '<input type="text" id="apikey" name="' . esc_attr(Metasync_Admin::option_key) . '[general][apikey]" value="%s" size="40" class="regular-text" readonly="readonly" />',
1467 1353 esc_attr($display_value)
1468 1354 );
1469 -
1470 - 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>');
1471 - printf('<p class="description">%s %s</p>', $status_message, $refresh_help);
1355 + 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>');
1356 + printf('</div>');
1357 + printf('<p class="description">%s %s</p>', esc_html($status_message), esc_html($refresh_help));
1472 1358 }
1473 1359
1474 1360 public function linkgraph_token_callback()
1475 1361 {
1476 1362 printf(
1477 - '<input type="text" id="linkgraph_token" name="' . Metasync_Admin::option_key . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1363 + '<input type="text" id="linkgraph_token" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1478 1364 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1479 1365 );
1480 1366
1481 1367 printf(
1482 - '<input type="text" id="linkgraph_customer_id" name="' . Metasync_Admin::option_key . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1368 + '<input type="text" id="linkgraph_customer_id" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1483 1369 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1484 1370 );
1485 1371
1486 1372 ?>
@@ -1496,10 +1382,16 @@
1496 1382 {
1497 1383 if(empty($datetime)){
1498 1384 return "";
1499 1385 }
1500 - $now = new DateTime;
1501 - $ago = new DateTime($datetime);
1386 + # Stored timestamps use current_time('mysql') which is the WP site's
1387 + # local timezone (no offset marker on the string). Constructing
1388 + # DateTime without a timezone would default to UTC and produce a
1389 + # bogus diff equal to the WP timezone offset (QA: "4 hours ago"
1390 + # after a 4-minute sync on non-UTC sites).
1391 + $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
1392 + $now = new DateTime('now', $wp_tz);
1393 + $ago = new DateTime($datetime, $wp_tz);
1502 1394
1503 1395 $diff = $now->diff($ago);
1504 1396
1505 1397 $string = [
@@ -1524,12 +1416,26 @@
1524 1416 }
1525 1417
1526 1418 public function searchatlas_api_key_callback()
1527 1419 {
1528 - $current_api_key = isset(Metasync::get_option('general')['searchatlas_api_key']) ? esc_attr(Metasync::get_option('general')['searchatlas_api_key']) : '';
1420 + // Whether a key is stored at rest (encrypted or legacy plaintext).
1421 + $stored_api_key = Metasync::get_option('general')['searchatlas_api_key'] ?? '';
1422 + $has_api_key = ($stored_api_key !== '');
1423 +
1424 + // Decrypted key for masking only. Never rendered in cleartext.
1425 + // false => an encrypted key exists but cannot be decrypted (salts changed).
1426 + $decrypted_api_key = Metasync::get_searchatlas_api_key();
1427 + $decrypt_failed = ($decrypted_api_key === false);
1428 +
1429 + // Build a masked representation: bullets + last 4 chars. The cleartext
1430 + // key is never emitted into the page.
1431 + $masked_api_key = '';
1432 + if (!$decrypt_failed && $decrypted_api_key !== '') {
1433 + $masked_api_key = str_repeat('*', 8) . substr($decrypted_api_key, -4);
1434 + }
1435 +
1529 1436 $otto_uuid = isset(Metasync::get_option('general')['otto_pixel_uuid']) ? Metasync::get_option('general')['otto_pixel_uuid'] : '';
1530 -
1531 - $has_api_key = !empty($current_api_key);
1437 +
1532 1438 $has_otto_uuid = !empty($otto_uuid);
1533 1439
1534 1440 $is_fully_connected = $this->admin_instance->is_heartbeat_connected();
1535 1441
@@ -1583,11 +1489,11 @@
1583 1489 printf('<div style="padding: 10px 0; color: #666; font-size: 13px; line-height: 1.5;">');
1584 1490 printf('• Make sure you have a %s account before connecting<br/>', esc_html(Metasync::get_effective_plugin_name()));
1585 1491 printf('• The authentication window will open in a popup - please allow popups<br/>');
1586 1492 printf('• The process typically takes 15-30 seconds to complete<br/>');
1587 - printf('• Your API key will be automatically filled in the field below<br/>');
1493 + printf('• Your API key will be stored securely and shown masked below<br/>');
1588 1494 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1589 - printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', Metasync::SUPPORT_EMAIL, Metasync::SUPPORT_EMAIL);
1495 + printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', esc_attr(Metasync::SUPPORT_EMAIL), esc_html(Metasync::SUPPORT_EMAIL));
1590 1496 printf('</div>');
1591 1497 printf('</details>');
1592 1498 printf('</div>');
1593 1499
@@ -1602,29 +1508,38 @@
1602 1508 printf('<span style="color: #ff8c00; margin-left: 10px; font-weight: normal;">Partial Connection (Missing %s UUID)</span>', esc_html(Metasync::get_whitelabel_otto_name()));
1603 1509 }
1604 1510 printf('</label>');
1605 1511
1512 + // Read-only masked display. The API key is managed via one-click
1513 + // authentication only; the cleartext key is never rendered into the
1514 + // page HTML (View Source / DOM). No form input is emitted, so saving
1515 + // settings cannot overwrite or expose the stored key.
1516 + $display_has_key = (!$decrypt_failed && $masked_api_key !== '') ? '1' : '0';
1606 1517 printf(
1607 - '<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" />',
1608 - $current_api_key
1518 + '<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;">',
1519 + esc_attr($display_has_key)
1609 1520 );
1610 -
1521 + if ($decrypt_failed) {
1522 + 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>');
1523 + } elseif ($masked_api_key !== '') {
1524 + echo esc_html($masked_api_key);
1525 + } else {
1526 + printf('<em style="color:#646970;">Not configured</em>');
1527 + }
1528 + printf('</div>');
1529 +
1611 1530 printf('<p class="description" style="margin-top: 8px;">');
1612 - if ($is_fully_connected) {
1613 - 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()));
1531 + if ($decrypt_failed) {
1532 + printf('Your stored API key could not be decrypted (your site security keys may have changed). Please re-authenticate above to reconnect.');
1533 + } elseif ($is_fully_connected) {
1534 + 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()));
1614 1535 } elseif ($has_api_key && !$has_otto_uuid) {
1615 1536 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()));
1616 1537 } else {
1617 - 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.');
1538 + printf('Use the authentication button above to securely connect your account. Your API key is stored encrypted and shown masked here.');
1618 1539 }
1619 1540 printf('</p>');
1620 1541
1621 - ?>
1622 - <p class="description" style="margin-top: 10px; padding: 8px 12px; background: #fff9e6; border-left: 3px solid #f0b849; border-radius: 4px; font-size: 12px;">
1623 - <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.
1624 - </p>
1625 - <?php
1626 -
1627 1542 printf('</div>');
1628 1543
1629 1544 if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){
1630 1545 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
@@ -1630,9 +1545,9 @@
1630 1545 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1631 1546 printf(
1632 1547 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1633 1548 esc_attr($timestamp),
1634 - $this->time_elapsed_string($timestamp)
1549 + esc_html($this->time_elapsed_string($timestamp))
1635 1550 );
1636 1551
1637 1552
1638 1553 }
@@ -1640,9 +1555,9 @@
1640 1555
1641 1556 public function bing_site_verification_callback()
1642 1557 {
1643 1558 printf(
1644 - '<input type="text" id="bing_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1559 + '<input type="text" id="bing_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1645 1560 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1646 1561 );
1647 1562
1648 1563 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
@@ -1651,9 +1566,9 @@
1651 1566
1652 1567 public function yandex_site_verification_callback()
1653 1568 {
1654 1569 printf(
1655 - '<input type="text" id="yandex_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1570 + '<input type="text" id="yandex_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1656 1571 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1657 1572 );
1658 1573
1659 1574 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
@@ -1662,9 +1577,9 @@
1662 1577
1663 1578 public function google_site_verification_callback()
1664 1579 {
1665 1580 printf(
1666 - '<input type="text" id="google_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][google_site_verification]" value="%s" size="50" />',
1581 + '<input type="text" id="google_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][google_site_verification]" value="%s" size="50" />',
1667 1582 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1668 1583 );
1669 1584
1670 1585 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
@@ -1673,9 +1588,9 @@
1673 1588
1674 1589 public function pinterest_site_verification_callback()
1675 1590 {
1676 1591 printf(
1677 - '<input type="text" id="pinterest_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1592 + '<input type="text" id="pinterest_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1678 1593 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1679 1594 );
1680 1595
1681 1596 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
@@ -1684,15 +1599,21 @@
1684 1599
1685 1600 public function local_seo_person_organization_callback()
1686 1601 {
1687 1602 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1603 + $valid_person_organizations = array('Person', 'Organization');
1688 1604 ?>
1689 - <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1690 - <?php
1691 - printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1692 - printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1693 - ?>
1694 - </select>
1605 + <div class="metasync-type-select-wrap">
1606 + <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1607 + <?php
1608 + if (!in_array($person_organization, $valid_person_organizations, true)) {
1609 + printf('<option value="0" selected="selected">Select Type</option>');
1610 + }
1611 + printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1612 + printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1613 + ?>
1614 + </select>
1615 + </div>
1695 1616 <?php
1696 1617 printf(' <br> <span class="description"> Choose whether the site represents a person or an organization. </span>');
1697 1618 }
1698 1619
@@ -1698,10 +1619,10 @@
1698 1619
1699 1620 public function local_seo_name_callback()
1700 1621 {
1701 1622 printf(
1702 - '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1703 - isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : get_bloginfo()
1623 + '<input type="text" id="local_seo_name" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_name]" value="%s" size="50" />',
1624 + isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : esc_attr(get_bloginfo())
1704 1625 );
1705 1626
1706 1627 printf(' <br> <span class="description"> Your name or company name </span>');
1707 1628 }
@@ -1709,31 +1630,82 @@
1709 1630 public function local_seo_logo_callback()
1710 1631 {
1711 1632 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1712 1633
1634 + // Rows saved by the legacy sanitizer hold the corrupted form of
1635 + // an attachment ID ("http://45589"). Repair it before anything reads
1636 + // the value — both so the preview resolves and so the hidden input
1637 + // re-submits the recovered ID instead of the corrupted string, which
1638 + // would otherwise survive every subsequent Save Settings.
1639 + $local_seo_logo = metasync_repair_scheme_prefixed_media_id($local_seo_logo);
1640 +
1713 1641 printf(
1714 - '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1715 - isset(Metasync::get_option('localseo')['local_seo_logo']) ? esc_attr(Metasync::get_option('localseo')['local_seo_logo']) : ''
1642 + '<input type="hidden" id="local_seo_logo" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_logo]" value="%s" size="50" />',
1643 + esc_attr((string) $local_seo_logo)
1716 1644 );
1717 1645
1718 - printf(' <br> <input class="button-secondary" type="button" id="logo_upload_button" value="Add or Upload File">');
1646 + printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">');
1719 1647
1720 1648 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> ');
1721 1649
1722 - printf('<img src="%s" id="local_seo_business_logo" width="300">', wp_get_attachment_image_src($local_seo_logo, 'medium')[0] ?? '');
1650 + printf('<span class="metasync-logo-preview-wrap">');
1723 1651
1724 - $button_type = 'hidden';
1725 - if ($local_seo_logo) {
1726 - $button_type = 'button';
1652 + // The stored value may be an attachment ID or a raw URL, so resolve
1653 + // both the way the front-end schema output already does. Anything that
1654 + // resolves to nothing — a deleted attachment, a legacy value mangled by
1655 + // an earlier save — must render no preview at all, rather than an empty
1656 + // src next to an orphaned remove button.
1657 + $logo_url = $this->resolve_media_url($local_seo_logo, 'medium');
1658 +
1659 + printf(
1660 + '<img src="%s" id="local_seo_business_logo" width="300"%s>',
1661 + esc_url($logo_url),
1662 + $logo_url ? '' : ' style="display:none"'
1663 + );
1664 +
1665 + $button_type = $logo_url ? 'button' : 'hidden';
1666 + 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));
1667 + printf('</span>');
1668 + }
1669 +
1670 + /**
1671 + * Resolve a stored media setting to a displayable URL.
1672 + *
1673 + * These settings accept either a media-library attachment ID or a raw URL;
1674 + * returns an empty string when the value resolves to neither, so callers
1675 + * can distinguish "no image" from "image we could not resolve".
1676 + *
1677 + * @param mixed $value Attachment ID or URL as stored in the options.
1678 + * @param string $size Registered image size to request for an attachment.
1679 + * @return string Resolved URL, or '' when there is nothing to show.
1680 + */
1681 + private function resolve_media_url($value, $size = 'medium')
1682 + {
1683 + if (empty($value)) {
1684 + return '';
1727 1685 }
1728 - printf('<input type="%s" class="button-secondary" id="local_seo_logo_close_btn" value="X">', $button_type);
1686 +
1687 + // A row saved by the legacy sanitizer stores a corrupted attachment ID ("http://45589"),
1688 + // which is neither resolvable as an ID nor a usable image URL.
1689 + $value = metasync_repair_scheme_prefixed_media_id($value);
1690 +
1691 + if (empty($value)) {
1692 + return '';
1693 + }
1694 +
1695 + if (is_numeric($value)) {
1696 + $url = wp_get_attachment_image_url((int) $value, $size);
1697 + return $url ? $url : '';
1698 + }
1699 +
1700 + return (string) $value;
1729 1701 }
1730 1702
1731 1703 public function local_seo_url_callback()
1732 1704 {
1733 1705 printf(
1734 - '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1735 - isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : home_url()
1706 + '<input type="text" id="local_seo_url" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_url]" value="%s" size="50" />',
1707 + isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : esc_attr(home_url())
1736 1708 );
1737 1709
1738 1710 printf(' <br> <span class="description"> URL of the item. </span>');
1739 1711 }
@@ -1740,9 +1712,9 @@
1740 1712
1741 1713 public function local_seo_email_callback()
1742 1714 {
1743 1715 printf(
1744 - '<input type="text" id="local_seo_email" name="' . Metasync_Admin::option_key . '[localseo][local_seo_email]" value="%s" size="50" />',
1716 + '<input type="text" id="local_seo_email" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_email]" value="%s" size="50" />',
1745 1717 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1746 1718 );
1747 1719
1748 1720 printf(' <br> <span class="description"> Search engines display your email address. </span>');
@@ -1750,9 +1722,9 @@
1750 1722
1751 1723 public function local_seo_phone_callback()
1752 1724 {
1753 1725 printf(
1754 - '<input type="text" id="local_seo_phone" name="' . Metasync_Admin::option_key . '[localseo][local_seo_phone]" value="%s" size="50" />',
1726 + '<input type="text" id="local_seo_phone" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_phone]" value="%s" size="50" />',
1755 1727 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1756 1728 );
1757 1729
1758 1730 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
@@ -1760,29 +1732,29 @@
1760 1732
1761 1733 public function local_seo_address_callback()
1762 1734 {
1763 1735 printf(
1764 - '<input type="text" id="local_seo_address_street" name="' . Metasync_Admin::option_key . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>',
1736 + '<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>',
1765 1737 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1766 1738 );
1767 1739
1768 1740 printf(
1769 - '<input type="text" id="local_seo_address_locality" name="' . Metasync_Admin::option_key . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>',
1741 + '<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>',
1770 1742 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1771 1743 );
1772 1744
1773 1745 printf(
1774 - '<input type="text" id="local_seo_address_region" name="' . Metasync_Admin::option_key . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>',
1746 + '<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>',
1775 1747 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1776 1748 );
1777 1749
1778 1750 printf(
1779 - '<input type="text" id="local_seo_address_postalcode" name="' . Metasync_Admin::option_key . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>',
1751 + '<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>',
1780 1752 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1781 1753 );
1782 1754
1783 1755 printf(
1784 - '<input type="text" id="local_seo_address_country" name="' . Metasync_Admin::option_key . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>',
1756 + '<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>',
1785 1757 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1786 1758 );
1787 1759 }
1788 1760
@@ -1797,9 +1769,9 @@
1797 1769 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1798 1770 <option value='0'>Select Business Type</option>
1799 1771 <?php
1800 1772 foreach ($types as $type) {
1801 - printf('<option value="%s" %s >%s</option>', $type, selected($type, esc_attr($business_type)), $type);
1773 + printf('<option value="%s" %s >%s</option>', esc_attr($type), selected($type, $business_type, false), esc_html($type));
1802 1774 }
1803 1775 ?>
1804 1776 </select>
1805 1777 <?php
@@ -1823,9 +1795,9 @@
1823 1795 <li>
1824 1796 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1825 1797 <?php
1826 1798 foreach ($days_name as $name) {
1827 - printf('<option value="%s" %s >%s</option>', $name, selected(esc_attr($name), esc_attr($day_name)), esc_attr($name));
1799 + printf('<option value="%s" %s >%s</option>', esc_attr($name), selected($name, $day_name, false), esc_html($name));
1828 1800 }
1829 1801 ?>
1830 1802 </select>
1831 1803 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
@@ -1904,9 +1876,9 @@
1904 1876
1905 1877 public function local_seo_price_range_callback()
1906 1878 {
1907 1879 printf(
1908 - '<input type="text" id="local_seo_price_range" name="' . Metasync_Admin::option_key . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1880 + '<input type="text" id="local_seo_price_range" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1909 1881 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1910 1882 );
1911 1883 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1912 1884 }
@@ -1919,9 +1891,9 @@
1919 1891 <?php
1920 1892 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1921 1893 $pages = get_pages();
1922 1894 foreach ($pages as $page) {
1923 - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($about_page)), $page->post_title);
1895 + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $about_page, false), esc_html($page->post_title));
1924 1896 }
1925 1897 ?>
1926 1898 </select>
1927 1899 <?php
@@ -1936,9 +1908,9 @@
1936 1908 <?php
1937 1909 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1938 1910 $pages = get_pages();
1939 1911 foreach ($pages as $page) {
1940 - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($contact_page)), $page->post_title);
1912 + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $contact_page, false), esc_html($page->post_title));
1941 1913 }
1942 1914 ?>
1943 1915 </select>
1944 1916 <?php
@@ -1947,9 +1919,9 @@
1947 1919
1948 1920 public function local_seo_map_key_callback()
1949 1921 {
1950 1922 printf(
1951 - '<input type="text" id="local_seo_map_key" name="' . Metasync_Admin::option_key . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1923 + '<input type="text" id="local_seo_map_key" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1952 1924 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1953 1925 );
1954 1926
1955 1927 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
@@ -1957,9 +1929,9 @@
1957 1929
1958 1930 public function local_seo_geo_coordinates_callback()
1959 1931 {
1960 1932 printf(
1961 - '<input type="text" id="local_seo_geo_coordinates" name="' . Metasync_Admin::option_key . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1933 + '<input type="text" id="local_seo_geo_coordinates" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1962 1934 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1963 1935 );
1964 1936
1965 1937 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
@@ -1967,9 +1939,9 @@
1967 1939
1968 1940 public function header_snippets_callback()
1969 1941 {
1970 1942 printf(
1971 - '<textarea class="wide-text" id="header_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][header_snippet]" >%s</textarea>',
1943 + '<textarea class="wide-text" id="header_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][header_snippet]" >%s</textarea>',
1972 1944 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1973 1945 );
1974 1946 }
1975 1947
@@ -1975,163 +1947,13 @@
1975 1947
1976 1948 public function footer_snippets_callback()
1977 1949 {
1978 1950 printf(
1979 - '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][footer_snippet]" >%s</textarea>',
1951 + '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][footer_snippet]" >%s</textarea>',
1980 1952 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1981 1953 );
1982 1954 }
1983 1955
1984 - public function no_index_posts_callback()
1985 - {
1986 - printf(
1987 - '<input type="checkbox" id="no_index_posts" name="' . Metasync_Admin::option_key . '[optimal_settings][no_index_posts]" value="true" %s />',
1988 - isset(Metasync::get_option('optimal_settings')['no_index_posts']) && Metasync::get_option('optimal_settings')['no_index_posts'] == 'true' ? 'checked' : ''
1989 - );
1990 -
1991 - 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>');
1992 - }
1993 -
1994 - public function no_follow_links_callback()
1995 - {
1996 - printf(
1997 - '<input type="checkbox" id="no_follow_links" name="' . Metasync_Admin::option_key . '[optimal_settings][no_follow_links]" value="true" %s />',
1998 - isset(Metasync::get_option('optimal_settings')['no_follow_links']) && Metasync::get_option('optimal_settings')['no_follow_links'] == 'true' ? 'checked' : ''
1999 - );
2000 -
2001 - 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>');
2002 - }
2003 -
2004 - public function open_external_links_callback()
2005 - {
2006 - printf(
2007 - '<input type="checkbox" id="open_external_links" name="' . Metasync_Admin::option_key . '[optimal_settings][open_external_links]" value="true" %s />',
2008 - isset(Metasync::get_option('optimal_settings')['open_external_links']) && Metasync::get_option('optimal_settings')['open_external_links'] == 'true' ? 'checked' : ''
2009 - );
2010 -
2011 - 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>');
2012 - }
2013 -
2014 - public function add_alt_image_tags_callback()
2015 - {
2016 - printf(
2017 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_alt_image_tags]" value="true" %s />',
2018 - isset(Metasync::get_option('optimal_settings')['add_alt_image_tags']) && Metasync::get_option('optimal_settings')['add_alt_image_tags'] == 'true' ? 'checked' : ''
2019 - );
2020 -
2021 - 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>');
2022 - }
2023 -
2024 - public function add_title_image_tags_callback()
2025 - {
2026 - printf(
2027 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_title_image_tags]" value="true" %s />',
2028 - isset(Metasync::get_option('optimal_settings')['add_title_image_tags']) && Metasync::get_option('optimal_settings')['add_title_image_tags'] == 'true' ? 'checked' : ''
2029 - );
2030 -
2031 - 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>');
2032 - }
2033 -
2034 - public function site_type_callback()
2035 - {
2036 -
2037 - $site_type = Metasync::get_option('optimal_settings')['site_info']['type'] ?? '';
2038 -
2039 - $types = [
2040 - ['name' => 'Personal Blog', 'value' => 'blog'],
2041 - ['name' => 'Community Blog/News Site', 'value' => 'news'],
2042 - ['name' => 'Personal Portfolio', 'value' => 'portfolio'],
2043 - ['name' => 'Small Business Site', 'value' => 'business'],
2044 - ['name' => 'Webshop', 'value' => 'webshop'],
2045 - ['name' => 'Other Personal Website', 'value' => 'otherpersonal'],
2046 - ['name' => 'Other Business Website', 'value' => 'otherbusiness'],
2047 - ];
2048 -
2049 - ?>
2050 - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][type]') ?>" id="site_info_type">
2051 - <?php
2052 - foreach ($types as $type) {
2053 - printf('<option value="%s" %s >%s</option>', esc_attr($type['value']), selected(esc_attr($type['value']), esc_attr($site_type)), ($type['name']));
2054 - }
2055 - ?>
2056 - </select>
2057 - <?php
2058 -
2059 - }
2060 -
2061 - public function site_business_type_callback()
2062 - {
2063 -
2064 - $business_type = Metasync::get_option('optimal_settings')['site_info']['business_type'] ?? '';
2065 -
2066 - $types = self::get_business_types();
2067 - sort($types);
2068 -
2069 - ?>
2070 - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][business_type]') ?>">
2071 - <option value='0'>Select Business Type</option>
2072 - <?php
2073 - foreach ($types as $type) {
2074 - printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($business_type)), esc_attr($type));
2075 - }
2076 - ?>
2077 - </select>
2078 - <?php
2079 - }
2080 -
2081 - public function site_company_name_callback()
2082 - {
2083 -
2084 - $company_name = Metasync::get_option('optimal_settings')['site_info']['company_name'] ?? get_bloginfo('name');
2085 -
2086 - printf(
2087 - '<input type="text" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][company_name]" value="%s" size="50" />',
2088 - $company_name ? $company_name : get_bloginfo('name')
2089 - );
2090 - }
2091 -
2092 - public function site_google_logo_callback()
2093 - {
2094 -
2095 - $google_logo = Metasync::get_option('optimal_settings')['site_info']['google_logo'] ?? '';
2096 -
2097 - printf(
2098 - '<input type="hidden" id="site_google_logo" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][google_logo]" value="%s" size="50" />',
2099 - $google_logo
2100 - );
2101 -
2102 - printf(' <br> <input class="button-secondary" type="button" id="google_logo_btn" value="Add or Upload File">');
2103 - 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> ');
2104 - printf('<img src="%s" id="site_google_logo_img" width="300">', wp_get_attachment_image_src($google_logo, 'medium')[0] ?? '');
2105 -
2106 - $button_type = 'hidden';
2107 - if ($google_logo) {
2108 - $button_type = 'button';
2109 - }
2110 - printf('<input type="%s" class="button-secondary" id="site_google_logo_close_btn" value="X">', $button_type);
2111 - }
2112 -
2113 - public function site_social_share_image_callback()
2114 - {
2115 -
2116 - $social_share_image = Metasync::get_option('optimal_settings')['site_info']['social_share_image'] ?? '';
2117 -
2118 - printf(
2119 - '<input type="hidden" id="site_social_share_image" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][social_share_image]" value="%s" size="50" />',
2120 - $social_share_image
2121 - );
2122 -
2123 - printf(' <br> <input class="button-secondary" type="button" id="social_share_image_btn" value="Add or Upload File">');
2124 - 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> ');
2125 - printf('<img src="%s" id="site_social_share_img" width="300">', wp_get_attachment_image_src($social_share_image, 'medium')[0] ?? '');
2126 -
2127 - $button_type = 'hidden';
2128 - if ($social_share_image) {
2129 - $button_type = 'button';
2130 - }
2131 - printf('<input type="%s" class="button-secondary" id="site_social_image_close_btn" value="X">', $button_type);
2132 - }
2133 -
2134 1956 public function common_robot_meta_tags_callback()
2135 1957 {
2136 1958 $common_robots_meta = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
2137 1959
@@ -2137,16 +1959,8 @@
2137 1959
2138 1960 ?>
2139 1961 <ul class="checkbox-list">
2140 1962 <li>
2141 - <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']) : '' ?>>
2142 - <label for="robots_common1">Index </br>
2143 - <span class="description">
2144 - <span>Search engines to index and show these pages in the search results.</span>
2145 - </span>
2146 - </label>
2147 - </li>
2148 - <li>
2149 1963 <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']) : '' ?>>
2150 1964 <label for="robots_common2">No Index </br>
2151 1965 <span class="description">
2152 1966 <span>Search engines not indexed and displayed this pages in search engine results</span>
@@ -2272,9 +2086,9 @@
2272 2086
2273 2087 public function global_open_graph_meta_callback()
2274 2088 {
2275 2089 printf(
2276 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2090 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2277 2091 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
2278 2092 );
2279 2093 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
2280 2094 }
@@ -2281,9 +2095,9 @@
2281 2095
2282 2096 public function global_facebook_meta_callback()
2283 2097 {
2284 2098 printf(
2285 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2099 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2286 2100 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
2287 2101 );
2288 2102 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
2289 2103 }
@@ -2290,9 +2104,9 @@
2290 2104
2291 2105 public function global_twitter_meta_callback()
2292 2106 {
2293 2107 printf(
2294 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2108 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2295 2109 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2296 2110 );
2297 2111 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2298 2112 }
@@ -2300,9 +2114,9 @@
2300 2114 public function og_image_dimensions_callback()
2301 2115 {
2302 2116 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2303 2117 printf(
2304 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2118 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2305 2119 $val === 'true' ? 'checked' : ''
2306 2120 );
2307 2121 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2308 2122 }
@@ -2310,9 +2124,9 @@
2310 2124 public function article_timestamps_callback()
2311 2125 {
2312 2126 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2313 2127 printf(
2314 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_timestamps]" value="true" %s />',
2128 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_timestamps]" value="true" %s />',
2315 2129 $val === 'true' ? 'checked' : ''
2316 2130 );
2317 2131 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2318 2132 }
@@ -2320,9 +2134,9 @@
2320 2134 public function article_author_callback()
2321 2135 {
2322 2136 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2323 2137 printf(
2324 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_author]" value="true" %s />',
2138 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_author]" value="true" %s />',
2325 2139 $val === 'true' ? 'checked' : ''
2326 2140 );
2327 2141 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>');
2328 2142 }
@@ -2330,9 +2144,9 @@
2330 2144 public function article_section_callback()
2331 2145 {
2332 2146 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2333 2147 printf(
2334 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_section]" value="true" %s />',
2148 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_section]" value="true" %s />',
2335 2149 $val === 'true' ? 'checked' : ''
2336 2150 );
2337 2151 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2338 2152 }
@@ -2340,9 +2154,9 @@
2340 2154 public function article_tags_callback()
2341 2155 {
2342 2156 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2343 2157 printf(
2344 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_tags]" value="true" %s />',
2158 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_tags]" value="true" %s />',
2345 2159 $val === 'true' ? 'checked' : ''
2346 2160 );
2347 2161 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2348 2162 }
@@ -2350,9 +2164,9 @@
2350 2164 public function twitter_image_alt_callback()
2351 2165 {
2352 2166 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2353 2167 printf(
2354 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2168 + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2355 2169 $val === 'true' ? 'checked' : ''
2356 2170 );
2357 2171 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2358 2172 }
@@ -2359,9 +2173,9 @@
2359 2173
2360 2174 public function facebook_page_url_callback()
2361 2175 {
2362 2176 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2363 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url));
2177 + 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));
2364 2178 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2365 2179 }
2366 2180
2367 2181 public function facebook_authorship_callback()
@@ -2366,9 +2180,9 @@
2366 2180
2367 2181 public function facebook_authorship_callback()
2368 2182 {
2369 2183 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2370 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2184 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2371 2185 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>');
2372 2186 }
2373 2187
2374 2188 public function facebook_admin_callback()
@@ -2373,9 +2187,9 @@
2373 2187
2374 2188 public function facebook_admin_callback()
2375 2189 {
2376 2190 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2377 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2191 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2378 2192 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2379 2193 }
2380 2194
2381 2195 public function facebook_app_callback()
@@ -2380,9 +2194,9 @@
2380 2194
2381 2195 public function facebook_app_callback()
2382 2196 {
2383 2197 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2384 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2198 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2385 2199 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2386 2200 }
2387 2201
2388 2202 public function facebook_secret_callback()
@@ -2387,9 +2201,9 @@
2387 2201
2388 2202 public function facebook_secret_callback()
2389 2203 {
2390 2204 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2391 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2205 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2392 2206 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2393 2207 }
2394 2208
2395 2209 public function twitter_username_callback()
@@ -2394,9 +2208,9 @@
2394 2208
2395 2209 public function twitter_username_callback()
2396 2210 {
2397 2211 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2398 - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2212 + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2399 2213 printf(' <br> <span class="description"> Twitter username of the author to add <code>twitter:creator</code> tag to post. eg: <code>MetaSync</code> </span>');
2400 2214 }
2401 2215
2402 2216 public static function get_business_types()
@@ -2571,8 +2385,14 @@
2571 2385 }
2572 2386
2573 2387 /**
2574 2388 * Render breadcrumbs settings section
2389 + *
2390 + * Rendered by the dedicated Breadcrumbs admin page. The three checkbox
2391 + * fields each ship a hidden companion input so their keys are always
2392 + * present in the submitted payload, which lets the save handlers treat an
2393 + * absent key as "this form does not manage that field" instead of
2394 + * "unchecked".
2575 2395 */
2576 2396 public function render_breadcrumbs_section() {
2577 2397 $options = Metasync::get_option('breadcrumbs', array());
2578 2398
@@ -2577,9 +2397,9 @@
2577 2397 $options = Metasync::get_option('breadcrumbs', array());
2578 2398
2579 2399 $defaults = array(
2580 2400 'enabled' => true,
2581 - 'separator' => '&raquo;',
2401 + 'separator' => '»',
2582 2402 'home_label' => 'Home',
2583 2403 'home_url' => '',
2584 2404 'show_current_page' => true,
2585 2405 'prefix_text' => '',
@@ -2587,8 +2407,19 @@
2587 2407 'disable_schema' => false,
2588 2408 );
2589 2409
2590 2410 $options = wp_parse_args($options, $defaults);
2411 +
2412 + # Older builds stored the separator as an HTML entity (e.g. "&raquo;").
2413 + # Decode before matching so the saved choice still lights up its radio.
2414 + $separator_presets = array('»', '/', '>', '·');
2415 + $separator_current = html_entity_decode((string) $options['separator'], ENT_QUOTES, 'UTF-8');
2416 + if ($separator_current === '') {
2417 + $separator_current = '»';
2418 + }
2419 + # A separator that predates the radio picker (or came from the old free
2420 + # text field) gets its own option so it stays selected and round-trips.
2421 + $separator_custom = in_array($separator_current, $separator_presets, true) ? '' : $separator_current;
2591 2422 ?>
2592 2423 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2593 2424
2594 2425 <!-- Enable/Disable -->
@@ -2593,8 +2424,9 @@
2593 2424
2594 2425 <!-- Enable/Disable -->
2595 2426 <div style="margin-bottom: 24px;">
2596 2427 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2428 + <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0">
2597 2429 <input type="checkbox"
2598 2430 name="metasync_options[breadcrumbs][enabled]"
2599 2431 value="1"
2600 2432 <?php checked($options['enabled'], 1); ?>
@@ -2599,8 +2431,9 @@
2599 2431 value="1"
2600 2432 <?php checked($options['enabled'], 1); ?>
2601 2433 style="width: 18px; height: 18px; cursor: pointer;">
2602 2434 <span style="font-weight: 500; color: var(--dashboard-text);">Enable breadcrumbs globally</span>
2435 + <?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.'); ?>
2603 2436 </label>
2604 2437 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2605 2438 Enable or disable breadcrumb navigation on your site
2606 2439 </p>
@@ -2608,8 +2441,9 @@
2608 2441
2609 2442 <!-- Disable Schema Markup -->
2610 2443 <div style="margin-bottom: 24px;">
2611 2444 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2445 + <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0">
2612 2446 <input type="checkbox"
2613 2447 name="metasync_options[breadcrumbs][disable_schema]"
2614 2448 value="1"
2615 2449 <?php checked($options['disable_schema'], 1); ?>
@@ -2614,8 +2448,9 @@
2614 2448 value="1"
2615 2449 <?php checked($options['disable_schema'], 1); ?>
2616 2450 style="width: 18px; height: 18px; cursor: pointer;">
2617 2451 <span style="font-weight: 500; color: var(--dashboard-text);">Disable breadcrumb schema markup (JSON-LD)</span>
2452 + <?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.'); ?>
2618 2453 </label>
2619 2454 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2620 2455 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.
2621 2456 </p>
@@ -2626,24 +2461,21 @@
2626 2461 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2627 2462 Separator Character
2628 2463 </label>
2629 2464 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2465 + <?php foreach ($separator_presets as $separator_option) : ?>
2630 2466 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2631 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2632 - <span style="color: var(--dashboard-text);">»</span>
2467 + <input type="radio" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_option); ?>" <?php checked($separator_current, $separator_option); ?>>
2468 + <span style="color: var(--dashboard-text);"><?php echo esc_html($separator_option); ?></span>
2633 2469 </label>
2634 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2635 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2636 - <span style="color: var(--dashboard-text);">/</span>
2637 - </label>
2638 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2639 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2640 - <span style="color: var(--dashboard-text);">></span>
2641 - </label>
2642 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2643 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2644 - <span style="color: var(--dashboard-text);">·</span>
2645 - </label>
2470 + <?php endforeach; ?>
2471 + <div style="display: flex; align-items: center; gap: 8px;">
2472 + <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2473 + <input type="radio" id="metasync-breadcrumb-separator-custom" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_custom); ?>" <?php checked($separator_custom !== ''); ?>>
2474 + <span style="color: var(--dashboard-text);">Custom</span>
2475 + </label>
2476 + <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;">
2477 + </div>
2646 2478 </div>
2647 2479 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2648 2480 Choose how to separate breadcrumb items
2649 2481 </p>
@@ -2683,8 +2515,9 @@
2683 2515
2684 2516 <!-- Show Current Page -->
2685 2517 <div style="margin-bottom: 24px;">
2686 2518 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2519 + <input type="hidden" name="metasync_options[breadcrumbs][show_current_page]" value="0">
2687 2520 <input type="checkbox"
2688 2521 name="metasync_options[breadcrumbs][show_current_page]"
2689 2522 value="1"
2690 2523 <?php checked($options['show_current_page'], 1); ?>
@@ -2715,8 +2548,9 @@
2715 2548 <!-- Archive Label Format -->
2716 2549 <div style="margin-bottom: 24px;">
2717 2550 <label for="breadcrumb_archive_format" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2718 2551 Archive Label Format
2552 + <?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}".'); ?>
2719 2553 </label>
2720 2554 <input type="text"
2721 2555 id="breadcrumb_archive_format"
2722 2556 name="metasync_options[breadcrumbs][archive_label_format]"