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 +298 -278 2.6.17 → 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,9 @@
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',
132 142 'disable_seo_metabox',
133 143 'open_external_links'
134 144 )
135 145 ),
@@ -170,13 +180,22 @@
170 180 'fields' => array(
171 181 'permalink_structure',
172 182 'hide_dashboard_framework',
173 183 'show_admin_bar_status',
174 - 'enable_auto_updates',
175 184 'import_external_data'
176 185 )
177 186 )
178 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;
179 198 }
180 199
181 200 public function get_advanced_accordion_config() {
182 201 $config = array();
@@ -201,8 +220,17 @@
201 220 'default_open' => false,
202 221 'render_callback' => array($this->admin_instance, 'render_debug_mode_section')
203 222 );
204 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 +
205 233 $config['error_logs'] = array(
206 234 'title' => 'Error Logs',
207 235 'description' => 'View and manage error logs to troubleshoot issues',
208 236 'icon' => 'warning',
@@ -275,9 +303,9 @@
275 303 $content_state = $is_open ? 'open' : 'closed';
276 304
277 305 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
278 306
279 - 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) . '">';
280 308 echo '<div class="metasync-accordion-title">';
281 309 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
282 310 echo '<div class="metasync-accordion-text">';
283 311 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -288,9 +316,9 @@
288 316 echo '<span class="toggle-icon">▼</span>';
289 317 echo '</button>';
290 318 echo '</div>';
291 319
292 - 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) . '">';
293 321
294 322 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
295 323 call_user_func($section_data['render_callback']);
296 324 }
@@ -329,8 +357,31 @@
329 357 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear All Settings
330 358 </button>
331 359 </form>
332 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>
333 384 <script>
334 385 function confirmClearSettings(event) {
335 386 event.preventDefault();
336 387
@@ -361,14 +412,18 @@
361 412 if (!function_exists('google_index_direct')) {
362 413 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
363 414 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
364 415 } else {
365 - 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
366 417 return;
367 418 }
368 419 }
369 420
370 421 $google_index = google_index_direct();
422 + if (!$google_index) {
423 + // Nothing to render against, and calling through would be fatal.
424 + return;
425 + }
371 426 $service_info = $google_index->get_service_account_info();
372 427 $is_configured = !isset($service_info['error']);
373 428
374 429 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
@@ -445,8 +500,9 @@
445 500 <table class="form-table" style="margin-top: 0;">
446 501 <tr>
447 502 <th scope="row" style="width: 200px; padding-top: 15px;">
448 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.'); ?>
449 505 </th>
450 506 <td style="padding-top: 15px;">
451 507 <input type="text"
452 508 name="metasync_bing_api_key_inline"
@@ -646,9 +702,9 @@
646 702 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
647 703 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
648 704 </p>
649 705
650 - <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">
651 707 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
652 708 <input type="hidden" name="save_plugin_access_roles" value="yes" />
653 709
654 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;">
@@ -766,9 +822,9 @@
766 822
767 823 public function can_change_memory_limit() {
768 824 $current_limit = ini_get('memory_limit');
769 825
770 - $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
771 827
772 828 return $test_result !== false;
773 829 }
774 830
@@ -820,9 +876,9 @@
820 876 }
821 877
822 878 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
823 879
824 - $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
825 881
826 882 return $result !== false;
827 883 }
828 884
@@ -877,12 +933,13 @@
877 933 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
878 934 <div>
879 935 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
880 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.'); ?>
881 938 </label>
882 939 <div style="display: flex; align-items: center; gap: 10px;">
883 - <input type="number"
884 - id="max_execution_time"
940 + <input type="number"
941 + id="max_execution_time"
885 942 name="max_execution_time"
886 943 value="<?php echo esc_attr($settings['max_execution_time']); ?>"
887 944 min="1"
888 945 max="300"
@@ -908,12 +965,13 @@
908 965
909 966 <div>
910 967 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
911 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.'); ?>
912 970 </label>
913 971 <div style="display: flex; align-items: center; gap: 10px;">
914 - <input type="number"
915 - id="max_memory_limit"
972 + <input type="number"
973 + id="max_memory_limit"
916 974 name="max_memory_limit"
917 975 value="<?php echo esc_attr($settings['max_memory_limit']); ?>"
918 976 min="<?php echo esc_attr($memory_range['min']); ?>"
919 977 <?php if ($memory_range['max'] !== null): ?>max="<?php echo esc_attr($memory_range['max']); ?>"<?php endif; ?>
@@ -957,12 +1015,13 @@
957 1015 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
958 1016 <div>
959 1017 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
960 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.'); ?>
961 1020 </label>
962 1021 <div style="display: flex; align-items: center; gap: 10px;">
963 - <input type="number"
964 - id="log_batch_size"
1022 + <input type="number"
1023 + id="log_batch_size"
965 1024 name="log_batch_size"
966 1025 value="<?php echo esc_attr($settings['log_batch_size']); ?>"
967 1026 min="100"
968 1027 max="5000"
@@ -974,8 +1033,9 @@
974 1033
975 1034 <div>
976 1035 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
977 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.'); ?>
978 1038 </label>
979 1039 <div style="display: flex; align-items: center; gap: 10px;">
980 1040 <input type="number"
981 1041 id="action_scheduler_batches"
@@ -1001,8 +1061,9 @@
1001 1061 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1002 1062 <div>
1003 1063 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1004 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.'); ?>
1005 1066 </label>
1006 1067 <div style="display: flex; align-items: center; gap: 10px;">
1007 1068 <input type="number"
1008 1069 id="otto_rate_limit"
@@ -1017,8 +1078,9 @@
1017 1078
1018 1079 <div>
1019 1080 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1020 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.'); ?>
1021 1083 </label>
1022 1084 <div style="display: flex; align-items: center; gap: 10px;">
1023 1085 <input type="number"
1024 1086 id="queue_cleanup_days"
@@ -1068,19 +1130,23 @@
1068 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),
1069 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),
1070 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),
1071 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),
1072 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.',
1073 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),
1074 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),
1075 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),
1076 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.',
1077 - '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.',
1078 - '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.',
1079 - '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.',
1080 - '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.',
1081 - '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.',
1082 - '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.',
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')),
1083 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).',
1084 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),
1085 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.',
1086 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),
@@ -1085,12 +1151,17 @@
1085 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.',
1086 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),
1087 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),
1088 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),
1089 - '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),
1090 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),
1091 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),
1092 - '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.'
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.'
1093 1164 );
1094 1165 }
1095 1166
1096 1167 public function get_field_section($field_id) {
@@ -1127,9 +1198,9 @@
1127 1198 $content_state = $is_open ? 'open' : 'closed';
1128 1199
1129 1200 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1130 1201
1131 - 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) . '">';
1132 1203 echo '<div class="metasync-accordion-title">';
1133 1204 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1134 1205 echo '<div class="metasync-accordion-text">';
1135 1206 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -1140,9 +1211,9 @@
1140 1211 echo '<span class="toggle-icon">▼</span>';
1141 1212 echo '</button>';
1142 1213 echo '</div>';
1143 1214
1144 - 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) . '">';
1145 1216
1146 1217 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1147 1218 call_user_func($section_data['render_callback']);
1148 1219 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
@@ -1157,9 +1228,9 @@
1157 1228 echo '<tr>';
1158 1229 echo '<th scope="row">';
1159 1230 if (!empty($field['title'])) {
1160 1231 echo '<div class="metasync-field-label-wrapper">';
1161 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1232 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1162 1233
1163 1234 if (isset($tooltips[$field_id])) {
1164 1235 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1165 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">';
@@ -1205,13 +1276,22 @@
1205 1276 return;
1206 1277 }
1207 1278
1208 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 + );
1209 1284 foreach ($wp_settings_fields[$page][$section] as $field) {
1210 1285 echo '<tr>';
1211 1286 echo '<th scope="row">';
1212 1287 if (!empty($field['title'])) {
1213 - 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>';
1214 1294 }
1215 1295 echo '</th>';
1216 1296 echo '<td>';
1217 1297 call_user_func($field['callback'], $field['args']);
@@ -1261,32 +1341,32 @@
1261 1341 $display_value = $current_token;
1262 1342 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1263 1343 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1264 1344 } else {
1265 - $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name());
1266 - $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() . '".';
1267 1347 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1268 1348 }
1269 1349
1270 1350 printf('<div class="metasync-auth-token-row">');
1271 1351 printf(
1272 - '<input type="text" id="apikey" name="' . Metasync_Admin::option_key . '[general][apikey]" value="%s" size="40" class="regular-text" 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" />',
1273 1353 esc_attr($display_value)
1274 1354 );
1275 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>');
1276 1356 printf('</div>');
1277 - printf('<p class="description">%s %s</p>', $status_message, $refresh_help);
1357 + printf('<p class="description">%s %s</p>', esc_html($status_message), esc_html($refresh_help));
1278 1358 }
1279 1359
1280 1360 public function linkgraph_token_callback()
1281 1361 {
1282 1362 printf(
1283 - '<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" />',
1284 1364 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1285 1365 );
1286 1366
1287 1367 printf(
1288 - '<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" />',
1289 1369 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1290 1370 );
1291 1371
1292 1372 ?>
@@ -1336,12 +1416,26 @@
1336 1416 }
1337 1417
1338 1418 public function searchatlas_api_key_callback()
1339 1419 {
1340 - $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 +
1341 1436 $otto_uuid = isset(Metasync::get_option('general')['otto_pixel_uuid']) ? Metasync::get_option('general')['otto_pixel_uuid'] : '';
1342 -
1343 - $has_api_key = !empty($current_api_key);
1437 +
1344 1438 $has_otto_uuid = !empty($otto_uuid);
1345 1439
1346 1440 $is_fully_connected = $this->admin_instance->is_heartbeat_connected();
1347 1441
@@ -1395,11 +1489,11 @@
1395 1489 printf('<div style="padding: 10px 0; color: #666; font-size: 13px; line-height: 1.5;">');
1396 1490 printf('• Make sure you have a %s account before connecting<br/>', esc_html(Metasync::get_effective_plugin_name()));
1397 1491 printf('• The authentication window will open in a popup - please allow popups<br/>');
1398 1492 printf('• The process typically takes 15-30 seconds to complete<br/>');
1399 - 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/>');
1400 1494 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1401 - 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));
1402 1496 printf('</div>');
1403 1497 printf('</details>');
1404 1498 printf('</div>');
1405 1499
@@ -1414,29 +1508,38 @@
1414 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()));
1415 1509 }
1416 1510 printf('</label>');
1417 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';
1418 1517 printf(
1419 - '<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" />',
1420 - $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)
1421 1520 );
1422 -
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 +
1423 1530 printf('<p class="description" style="margin-top: 8px;">');
1424 - if ($is_fully_connected) {
1425 - 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()));
1426 1535 } elseif ($has_api_key && !$has_otto_uuid) {
1427 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()));
1428 1537 } else {
1429 - 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.');
1430 1539 }
1431 1540 printf('</p>');
1432 1541
1433 - ?>
1434 - <p class="description" style="margin-top: 10px; padding: 8px 12px; background: #fff9e6; border-left: 3px solid #f0b849; border-radius: 4px; font-size: 12px;">
1435 - <strong>Manual Authentication:</strong> If you manually enter your <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> API Key and <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> UUID, you also consent to the same AI-powered automation permissions described above.
1436 - </p>
1437 - <?php
1438 -
1439 1542 printf('</div>');
1440 1543
1441 1544 if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){
1442 1545 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
@@ -1442,9 +1545,9 @@
1442 1545 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1443 1546 printf(
1444 1547 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1445 1548 esc_attr($timestamp),
1446 - $this->time_elapsed_string($timestamp)
1549 + esc_html($this->time_elapsed_string($timestamp))
1447 1550 );
1448 1551
1449 1552
1450 1553 }
@@ -1452,9 +1555,9 @@
1452 1555
1453 1556 public function bing_site_verification_callback()
1454 1557 {
1455 1558 printf(
1456 - '<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" />',
1457 1560 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1458 1561 );
1459 1562
1460 1563 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
@@ -1463,9 +1566,9 @@
1463 1566
1464 1567 public function yandex_site_verification_callback()
1465 1568 {
1466 1569 printf(
1467 - '<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" />',
1468 1571 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1469 1572 );
1470 1573
1471 1574 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
@@ -1474,9 +1577,9 @@
1474 1577
1475 1578 public function google_site_verification_callback()
1476 1579 {
1477 1580 printf(
1478 - '<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" />',
1479 1582 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1480 1583 );
1481 1584
1482 1585 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
@@ -1485,9 +1588,9 @@
1485 1588
1486 1589 public function pinterest_site_verification_callback()
1487 1590 {
1488 1591 printf(
1489 - '<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" />',
1490 1593 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1491 1594 );
1492 1595
1493 1596 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
@@ -1496,12 +1599,16 @@
1496 1599
1497 1600 public function local_seo_person_organization_callback()
1498 1601 {
1499 1602 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1603 + $valid_person_organizations = array('Person', 'Organization');
1500 1604 ?>
1501 1605 <div class="metasync-type-select-wrap">
1502 1606 <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1503 1607 <?php
1608 + if (!in_array($person_organization, $valid_person_organizations, true)) {
1609 + printf('<option value="0" selected="selected">Select Type</option>');
1610 + }
1504 1611 printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1505 1612 printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1506 1613 ?>
1507 1614 </select>
@@ -1512,10 +1619,10 @@
1512 1619
1513 1620 public function local_seo_name_callback()
1514 1621 {
1515 1622 printf(
1516 - '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1517 - 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())
1518 1625 );
1519 1626
1520 1627 printf(' <br> <span class="description"> Your name or company name </span>');
1521 1628 }
@@ -1523,31 +1630,82 @@
1523 1630 public function local_seo_logo_callback()
1524 1631 {
1525 1632 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1526 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 +
1527 1641 printf(
1528 - '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1529 - 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)
1530 1644 );
1531 1645
1532 - 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">');
1533 1647
1534 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> ');
1535 1649
1536 - 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">');
1537 1651
1538 - $button_type = 'hidden';
1539 - if ($local_seo_logo) {
1540 - $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 '';
1541 1685 }
1542 - 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;
1543 1701 }
1544 1702
1545 1703 public function local_seo_url_callback()
1546 1704 {
1547 1705 printf(
1548 - '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1549 - 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())
1550 1708 );
1551 1709
1552 1710 printf(' <br> <span class="description"> URL of the item. </span>');
1553 1711 }
@@ -1554,9 +1712,9 @@
1554 1712
1555 1713 public function local_seo_email_callback()
1556 1714 {
1557 1715 printf(
1558 - '<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" />',
1559 1717 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1560 1718 );
1561 1719
1562 1720 printf(' <br> <span class="description"> Search engines display your email address. </span>');
@@ -1564,9 +1722,9 @@
1564 1722
1565 1723 public function local_seo_phone_callback()
1566 1724 {
1567 1725 printf(
1568 - '<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" />',
1569 1727 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1570 1728 );
1571 1729
1572 1730 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
@@ -1574,29 +1732,29 @@
1574 1732
1575 1733 public function local_seo_address_callback()
1576 1734 {
1577 1735 printf(
1578 - '<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>',
1579 1737 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1580 1738 );
1581 1739
1582 1740 printf(
1583 - '<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>',
1584 1742 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1585 1743 );
1586 1744
1587 1745 printf(
1588 - '<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>',
1589 1747 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1590 1748 );
1591 1749
1592 1750 printf(
1593 - '<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>',
1594 1752 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1595 1753 );
1596 1754
1597 1755 printf(
1598 - '<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>',
1599 1757 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1600 1758 );
1601 1759 }
1602 1760
@@ -1611,9 +1769,9 @@
1611 1769 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1612 1770 <option value='0'>Select Business Type</option>
1613 1771 <?php
1614 1772 foreach ($types as $type) {
1615 - 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));
1616 1774 }
1617 1775 ?>
1618 1776 </select>
1619 1777 <?php
@@ -1637,9 +1795,9 @@
1637 1795 <li>
1638 1796 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1639 1797 <?php
1640 1798 foreach ($days_name as $name) {
1641 - 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));
1642 1800 }
1643 1801 ?>
1644 1802 </select>
1645 1803 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
@@ -1718,9 +1876,9 @@
1718 1876
1719 1877 public function local_seo_price_range_callback()
1720 1878 {
1721 1879 printf(
1722 - '<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" />',
1723 1881 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1724 1882 );
1725 1883 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1726 1884 }
@@ -1733,9 +1891,9 @@
1733 1891 <?php
1734 1892 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1735 1893 $pages = get_pages();
1736 1894 foreach ($pages as $page) {
1737 - 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));
1738 1896 }
1739 1897 ?>
1740 1898 </select>
1741 1899 <?php
@@ -1750,9 +1908,9 @@
1750 1908 <?php
1751 1909 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1752 1910 $pages = get_pages();
1753 1911 foreach ($pages as $page) {
1754 - 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));
1755 1913 }
1756 1914 ?>
1757 1915 </select>
1758 1916 <?php
@@ -1761,9 +1919,9 @@
1761 1919
1762 1920 public function local_seo_map_key_callback()
1763 1921 {
1764 1922 printf(
1765 - '<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" />',
1766 1924 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1767 1925 );
1768 1926
1769 1927 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
@@ -1771,9 +1929,9 @@
1771 1929
1772 1930 public function local_seo_geo_coordinates_callback()
1773 1931 {
1774 1932 printf(
1775 - '<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" />',
1776 1934 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1777 1935 );
1778 1936
1779 1937 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
@@ -1781,9 +1939,9 @@
1781 1939
1782 1940 public function header_snippets_callback()
1783 1941 {
1784 1942 printf(
1785 - '<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>',
1786 1944 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1787 1945 );
1788 1946 }
1789 1947
@@ -1789,163 +1947,13 @@
1789 1947
1790 1948 public function footer_snippets_callback()
1791 1949 {
1792 1950 printf(
1793 - '<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>',
1794 1952 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1795 1953 );
1796 1954 }
1797 1955
1798 - public function no_index_posts_callback()
1799 - {
1800 - printf(
1801 - '<input type="checkbox" id="no_index_posts" name="' . Metasync_Admin::option_key . '[optimal_settings][no_index_posts]" value="true" %s />',
1802 - isset(Metasync::get_option('optimal_settings')['no_index_posts']) && Metasync::get_option('optimal_settings')['no_index_posts'] == 'true' ? 'checked' : ''
1803 - );
1804 -
1805 - 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>');
1806 - }
1807 -
1808 - public function no_follow_links_callback()
1809 - {
1810 - printf(
1811 - '<input type="checkbox" id="no_follow_links" name="' . Metasync_Admin::option_key . '[optimal_settings][no_follow_links]" value="true" %s />',
1812 - isset(Metasync::get_option('optimal_settings')['no_follow_links']) && Metasync::get_option('optimal_settings')['no_follow_links'] == 'true' ? 'checked' : ''
1813 - );
1814 -
1815 - 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>');
1816 - }
1817 -
1818 - public function open_external_links_callback()
1819 - {
1820 - printf(
1821 - '<input type="checkbox" id="open_external_links" name="' . Metasync_Admin::option_key . '[optimal_settings][open_external_links]" value="true" %s />',
1822 - isset(Metasync::get_option('optimal_settings')['open_external_links']) && Metasync::get_option('optimal_settings')['open_external_links'] == 'true' ? 'checked' : ''
1823 - );
1824 -
1825 - 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>');
1826 - }
1827 -
1828 - public function add_alt_image_tags_callback()
1829 - {
1830 - printf(
1831 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_alt_image_tags]" value="true" %s />',
1832 - isset(Metasync::get_option('optimal_settings')['add_alt_image_tags']) && Metasync::get_option('optimal_settings')['add_alt_image_tags'] == 'true' ? 'checked' : ''
1833 - );
1834 -
1835 - 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>');
1836 - }
1837 -
1838 - public function add_title_image_tags_callback()
1839 - {
1840 - printf(
1841 - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_title_image_tags]" value="true" %s />',
1842 - isset(Metasync::get_option('optimal_settings')['add_title_image_tags']) && Metasync::get_option('optimal_settings')['add_title_image_tags'] == 'true' ? 'checked' : ''
1843 - );
1844 -
1845 - 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>');
1846 - }
1847 -
1848 - public function site_type_callback()
1849 - {
1850 -
1851 - $site_type = Metasync::get_option('optimal_settings')['site_info']['type'] ?? '';
1852 -
1853 - $types = [
1854 - ['name' => 'Personal Blog', 'value' => 'blog'],
1855 - ['name' => 'Community Blog/News Site', 'value' => 'news'],
1856 - ['name' => 'Personal Portfolio', 'value' => 'portfolio'],
1857 - ['name' => 'Small Business Site', 'value' => 'business'],
1858 - ['name' => 'Webshop', 'value' => 'webshop'],
1859 - ['name' => 'Other Personal Website', 'value' => 'otherpersonal'],
1860 - ['name' => 'Other Business Website', 'value' => 'otherbusiness'],
1861 - ];
1862 -
1863 - ?>
1864 - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][type]') ?>" id="site_info_type">
1865 - <?php
1866 - foreach ($types as $type) {
1867 - printf('<option value="%s" %s >%s</option>', esc_attr($type['value']), selected(esc_attr($type['value']), esc_attr($site_type)), ($type['name']));
1868 - }
1869 - ?>
1870 - </select>
1871 - <?php
1872 -
1873 - }
1874 -
1875 - public function site_business_type_callback()
1876 - {
1877 -
1878 - $business_type = Metasync::get_option('optimal_settings')['site_info']['business_type'] ?? '';
1879 -
1880 - $types = self::get_business_types();
1881 - sort($types);
1882 -
1883 - ?>
1884 - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][business_type]') ?>">
1885 - <option value='0'>Select Business Type</option>
1886 - <?php
1887 - foreach ($types as $type) {
1888 - printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($business_type)), esc_attr($type));
1889 - }
1890 - ?>
1891 - </select>
1892 - <?php
1893 - }
1894 -
1895 - public function site_company_name_callback()
1896 - {
1897 -
1898 - $company_name = Metasync::get_option('optimal_settings')['site_info']['company_name'] ?? get_bloginfo('name');
1899 -
1900 - printf(
1901 - '<input type="text" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][company_name]" value="%s" size="50" />',
1902 - $company_name ? $company_name : get_bloginfo('name')
1903 - );
1904 - }
1905 -
1906 - public function site_google_logo_callback()
1907 - {
1908 -
1909 - $google_logo = Metasync::get_option('optimal_settings')['site_info']['google_logo'] ?? '';
1910 -
1911 - printf(
1912 - '<input type="hidden" id="site_google_logo" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][google_logo]" value="%s" size="50" />',
1913 - $google_logo
1914 - );
1915 -
1916 - printf(' <br> <input class="button-secondary" type="button" id="google_logo_btn" value="Add or Upload File">');
1917 - 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> ');
1918 - printf('<img src="%s" id="site_google_logo_img" width="300">', wp_get_attachment_image_src($google_logo, 'medium')[0] ?? '');
1919 -
1920 - $button_type = 'hidden';
1921 - if ($google_logo) {
1922 - $button_type = 'button';
1923 - }
1924 - printf('<input type="%s" class="button-secondary" id="site_google_logo_close_btn" value="X">', $button_type);
1925 - }
1926 -
1927 - public function site_social_share_image_callback()
1928 - {
1929 -
1930 - $social_share_image = Metasync::get_option('optimal_settings')['site_info']['social_share_image'] ?? '';
1931 -
1932 - printf(
1933 - '<input type="hidden" id="site_social_share_image" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][social_share_image]" value="%s" size="50" />',
1934 - $social_share_image
1935 - );
1936 -
1937 - printf(' <br> <input class="button-secondary" type="button" id="social_share_image_btn" value="Add or Upload File">');
1938 - 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> ');
1939 - printf('<img src="%s" id="site_social_share_img" width="300">', wp_get_attachment_image_src($social_share_image, 'medium')[0] ?? '');
1940 -
1941 - $button_type = 'hidden';
1942 - if ($social_share_image) {
1943 - $button_type = 'button';
1944 - }
1945 - printf('<input type="%s" class="button-secondary" id="site_social_image_close_btn" value="X">', $button_type);
1946 - }
1947 -
1948 1956 public function common_robot_meta_tags_callback()
1949 1957 {
1950 1958 $common_robots_meta = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
1951 1959
@@ -1951,16 +1959,8 @@
1951 1959
1952 1960 ?>
1953 1961 <ul class="checkbox-list">
1954 1962 <li>
1955 - <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']) : '' ?>>
1956 - <label for="robots_common1">Index </br>
1957 - <span class="description">
1958 - <span>Search engines to index and show these pages in the search results.</span>
1959 - </span>
1960 - </label>
1961 - </li>
1962 - <li>
1963 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']) : '' ?>>
1964 1964 <label for="robots_common2">No Index </br>
1965 1965 <span class="description">
1966 1966 <span>Search engines not indexed and displayed this pages in search engine results</span>
@@ -2086,9 +2086,9 @@
2086 2086
2087 2087 public function global_open_graph_meta_callback()
2088 2088 {
2089 2089 printf(
2090 - '<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 />',
2091 2091 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
2092 2092 );
2093 2093 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
2094 2094 }
@@ -2095,9 +2095,9 @@
2095 2095
2096 2096 public function global_facebook_meta_callback()
2097 2097 {
2098 2098 printf(
2099 - '<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 />',
2100 2100 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
2101 2101 );
2102 2102 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
2103 2103 }
@@ -2104,9 +2104,9 @@
2104 2104
2105 2105 public function global_twitter_meta_callback()
2106 2106 {
2107 2107 printf(
2108 - '<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 />',
2109 2109 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2110 2110 );
2111 2111 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2112 2112 }
@@ -2114,9 +2114,9 @@
2114 2114 public function og_image_dimensions_callback()
2115 2115 {
2116 2116 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2117 2117 printf(
2118 - '<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 />',
2119 2119 $val === 'true' ? 'checked' : ''
2120 2120 );
2121 2121 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2122 2122 }
@@ -2124,9 +2124,9 @@
2124 2124 public function article_timestamps_callback()
2125 2125 {
2126 2126 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2127 2127 printf(
2128 - '<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 />',
2129 2129 $val === 'true' ? 'checked' : ''
2130 2130 );
2131 2131 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2132 2132 }
@@ -2134,9 +2134,9 @@
2134 2134 public function article_author_callback()
2135 2135 {
2136 2136 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2137 2137 printf(
2138 - '<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 />',
2139 2139 $val === 'true' ? 'checked' : ''
2140 2140 );
2141 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>');
2142 2142 }
@@ -2144,9 +2144,9 @@
2144 2144 public function article_section_callback()
2145 2145 {
2146 2146 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2147 2147 printf(
2148 - '<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 />',
2149 2149 $val === 'true' ? 'checked' : ''
2150 2150 );
2151 2151 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2152 2152 }
@@ -2154,9 +2154,9 @@
2154 2154 public function article_tags_callback()
2155 2155 {
2156 2156 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2157 2157 printf(
2158 - '<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 />',
2159 2159 $val === 'true' ? 'checked' : ''
2160 2160 );
2161 2161 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2162 2162 }
@@ -2164,9 +2164,9 @@
2164 2164 public function twitter_image_alt_callback()
2165 2165 {
2166 2166 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2167 2167 printf(
2168 - '<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 />',
2169 2169 $val === 'true' ? 'checked' : ''
2170 2170 );
2171 2171 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2172 2172 }
@@ -2173,9 +2173,9 @@
2173 2173
2174 2174 public function facebook_page_url_callback()
2175 2175 {
2176 2176 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2177 - 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));
2178 2178 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2179 2179 }
2180 2180
2181 2181 public function facebook_authorship_callback()
@@ -2180,9 +2180,9 @@
2180 2180
2181 2181 public function facebook_authorship_callback()
2182 2182 {
2183 2183 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2184 - 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));
2185 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>');
2186 2186 }
2187 2187
2188 2188 public function facebook_admin_callback()
@@ -2187,9 +2187,9 @@
2187 2187
2188 2188 public function facebook_admin_callback()
2189 2189 {
2190 2190 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2191 - 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));
2192 2192 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2193 2193 }
2194 2194
2195 2195 public function facebook_app_callback()
@@ -2194,9 +2194,9 @@
2194 2194
2195 2195 public function facebook_app_callback()
2196 2196 {
2197 2197 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2198 - 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));
2199 2199 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2200 2200 }
2201 2201
2202 2202 public function facebook_secret_callback()
@@ -2201,9 +2201,9 @@
2201 2201
2202 2202 public function facebook_secret_callback()
2203 2203 {
2204 2204 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2205 - 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));
2206 2206 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2207 2207 }
2208 2208
2209 2209 public function twitter_username_callback()
@@ -2208,9 +2208,9 @@
2208 2208
2209 2209 public function twitter_username_callback()
2210 2210 {
2211 2211 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2212 - 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));
2213 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>');
2214 2214 }
2215 2215
2216 2216 public static function get_business_types()
@@ -2385,8 +2385,14 @@
2385 2385 }
2386 2386
2387 2387 /**
2388 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".
2389 2395 */
2390 2396 public function render_breadcrumbs_section() {
2391 2397 $options = Metasync::get_option('breadcrumbs', array());
2392 2398
@@ -2391,9 +2397,9 @@
2391 2397 $options = Metasync::get_option('breadcrumbs', array());
2392 2398
2393 2399 $defaults = array(
2394 2400 'enabled' => true,
2395 - 'separator' => '&raquo;',
2401 + 'separator' => '»',
2396 2402 'home_label' => 'Home',
2397 2403 'home_url' => '',
2398 2404 'show_current_page' => true,
2399 2405 'prefix_text' => '',
@@ -2401,8 +2407,19 @@
2401 2407 'disable_schema' => false,
2402 2408 );
2403 2409
2404 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;
2405 2422 ?>
2406 2423 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2407 2424
2408 2425 <!-- Enable/Disable -->
@@ -2407,8 +2424,9 @@
2407 2424
2408 2425 <!-- Enable/Disable -->
2409 2426 <div style="margin-bottom: 24px;">
2410 2427 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2428 + <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0">
2411 2429 <input type="checkbox"
2412 2430 name="metasync_options[breadcrumbs][enabled]"
2413 2431 value="1"
2414 2432 <?php checked($options['enabled'], 1); ?>
@@ -2413,8 +2431,9 @@
2413 2431 value="1"
2414 2432 <?php checked($options['enabled'], 1); ?>
2415 2433 style="width: 18px; height: 18px; cursor: pointer;">
2416 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.'); ?>
2417 2436 </label>
2418 2437 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2419 2438 Enable or disable breadcrumb navigation on your site
2420 2439 </p>
@@ -2422,8 +2441,9 @@
2422 2441
2423 2442 <!-- Disable Schema Markup -->
2424 2443 <div style="margin-bottom: 24px;">
2425 2444 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2445 + <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0">
2426 2446 <input type="checkbox"
2427 2447 name="metasync_options[breadcrumbs][disable_schema]"
2428 2448 value="1"
2429 2449 <?php checked($options['disable_schema'], 1); ?>
@@ -2428,8 +2448,9 @@
2428 2448 value="1"
2429 2449 <?php checked($options['disable_schema'], 1); ?>
2430 2450 style="width: 18px; height: 18px; cursor: pointer;">
2431 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.'); ?>
2432 2453 </label>
2433 2454 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2434 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.
2435 2456 </p>
@@ -2440,24 +2461,21 @@
2440 2461 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2441 2462 Separator Character
2442 2463 </label>
2443 2464 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2465 + <?php foreach ($separator_presets as $separator_option) : ?>
2444 2466 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2445 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2446 - <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>
2447 2469 </label>
2448 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2449 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2450 - <span style="color: var(--dashboard-text);">/</span>
2451 - </label>
2452 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2453 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2454 - <span style="color: var(--dashboard-text);">></span>
2455 - </label>
2456 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2457 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2458 - <span style="color: var(--dashboard-text);">·</span>
2459 - </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>
2460 2478 </div>
2461 2479 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2462 2480 Choose how to separate breadcrumb items
2463 2481 </p>
@@ -2497,8 +2515,9 @@
2497 2515
2498 2516 <!-- Show Current Page -->
2499 2517 <div style="margin-bottom: 24px;">
2500 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">
2501 2520 <input type="checkbox"
2502 2521 name="metasync_options[breadcrumbs][show_current_page]"
2503 2522 value="1"
2504 2523 <?php checked($options['show_current_page'], 1); ?>
@@ -2529,8 +2548,9 @@
2529 2548 <!-- Archive Label Format -->
2530 2549 <div style="margin-bottom: 24px;">
2531 2550 <label for="breadcrumb_archive_format" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2532 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}".'); ?>
2533 2553 </label>
2534 2554 <input type="text"
2535 2555 id="breadcrumb_archive_format"
2536 2556 name="metasync_options[breadcrumbs][archive_label_format]"