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 +226 -104 2.6.20 → 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() : '';
@@ -647,9 +702,9 @@
647 702 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
648 703 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
649 704 </p>
650 705
651 - <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">
652 707 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
653 708 <input type="hidden" name="save_plugin_access_roles" value="yes" />
654 709
655 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;">
@@ -767,9 +822,9 @@
767 822
768 823 public function can_change_memory_limit() {
769 824 $current_limit = ini_get('memory_limit');
770 825
771 - $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
772 827
773 828 return $test_result !== false;
774 829 }
775 830
@@ -821,9 +876,9 @@
821 876 }
822 877
823 878 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
824 879
825 - $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
826 881
827 882 return $result !== false;
828 883 }
829 884
@@ -1075,19 +1130,23 @@
1075 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),
1076 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),
1077 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),
1078 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),
1079 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.',
1080 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),
1081 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),
1082 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),
1083 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.',
1084 - '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.',
1085 - '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.',
1086 - '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.',
1087 - '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.',
1088 - '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.',
1089 - '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')),
1090 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).',
1091 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),
1092 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.',
1093 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),
@@ -1092,9 +1151,8 @@
1092 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.',
1093 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),
1094 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),
1095 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),
1096 - '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),
1097 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),
1098 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),
1099 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.',
1100 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.',
@@ -1140,9 +1198,9 @@
1140 1198 $content_state = $is_open ? 'open' : 'closed';
1141 1199
1142 1200 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1143 1201
1144 - 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) . '">';
1145 1203 echo '<div class="metasync-accordion-title">';
1146 1204 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1147 1205 echo '<div class="metasync-accordion-text">';
1148 1206 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -1153,9 +1211,9 @@
1153 1211 echo '<span class="toggle-icon">▼</span>';
1154 1212 echo '</button>';
1155 1213 echo '</div>';
1156 1214
1157 - 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) . '">';
1158 1216
1159 1217 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1160 1218 call_user_func($section_data['render_callback']);
1161 1219 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
@@ -1170,9 +1228,9 @@
1170 1228 echo '<tr>';
1171 1229 echo '<th scope="row">';
1172 1230 if (!empty($field['title'])) {
1173 1231 echo '<div class="metasync-field-label-wrapper">';
1174 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1232 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1175 1233
1176 1234 if (isset($tooltips[$field_id])) {
1177 1235 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1178 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">';
@@ -1227,9 +1285,9 @@
1227 1285 echo '<tr>';
1228 1286 echo '<th scope="row">';
1229 1287 if (!empty($field['title'])) {
1230 1288 echo '<div class="metasync-field-label-wrapper">';
1231 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1289 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1232 1290 if (isset($llms_tooltips[$field['id']])) {
1233 1291 Metasync::render_tooltip_icon($field['id'], $llms_tooltips[$field['id']]);
1234 1292 }
1235 1293 echo '</div>';
@@ -1283,32 +1341,32 @@
1283 1341 $display_value = $current_token;
1284 1342 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1285 1343 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1286 1344 } else {
1287 - $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name());
1288 - $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() . '".';
1289 1347 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1290 1348 }
1291 1349
1292 1350 printf('<div class="metasync-auth-token-row">');
1293 1351 printf(
1294 - '<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" />',
1295 1353 esc_attr($display_value)
1296 1354 );
1297 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>');
1298 1356 printf('</div>');
1299 - 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));
1300 1358 }
1301 1359
1302 1360 public function linkgraph_token_callback()
1303 1361 {
1304 1362 printf(
1305 - '<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" />',
1306 1364 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1307 1365 );
1308 1366
1309 1367 printf(
1310 - '<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" />',
1311 1369 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1312 1370 );
1313 1371
1314 1372 ?>
@@ -1433,9 +1491,9 @@
1433 1491 printf('• The authentication window will open in a popup - please allow popups<br/>');
1434 1492 printf('• The process typically takes 15-30 seconds to complete<br/>');
1435 1493 printf('• Your API key will be stored securely and shown masked below<br/>');
1436 1494 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1437 - 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));
1438 1496 printf('</div>');
1439 1497 printf('</details>');
1440 1498 printf('</div>');
1441 1499
@@ -1487,9 +1545,9 @@
1487 1545 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1488 1546 printf(
1489 1547 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1490 1548 esc_attr($timestamp),
1491 - $this->time_elapsed_string($timestamp)
1549 + esc_html($this->time_elapsed_string($timestamp))
1492 1550 );
1493 1551
1494 1552
1495 1553 }
@@ -1497,9 +1555,9 @@
1497 1555
1498 1556 public function bing_site_verification_callback()
1499 1557 {
1500 1558 printf(
1501 - '<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" />',
1502 1560 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1503 1561 );
1504 1562
1505 1563 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
@@ -1508,9 +1566,9 @@
1508 1566
1509 1567 public function yandex_site_verification_callback()
1510 1568 {
1511 1569 printf(
1512 - '<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" />',
1513 1571 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1514 1572 );
1515 1573
1516 1574 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
@@ -1519,9 +1577,9 @@
1519 1577
1520 1578 public function google_site_verification_callback()
1521 1579 {
1522 1580 printf(
1523 - '<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" />',
1524 1582 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1525 1583 );
1526 1584
1527 1585 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
@@ -1530,9 +1588,9 @@
1530 1588
1531 1589 public function pinterest_site_verification_callback()
1532 1590 {
1533 1591 printf(
1534 - '<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" />',
1535 1593 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1536 1594 );
1537 1595
1538 1596 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
@@ -1541,12 +1599,16 @@
1541 1599
1542 1600 public function local_seo_person_organization_callback()
1543 1601 {
1544 1602 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1603 + $valid_person_organizations = array('Person', 'Organization');
1545 1604 ?>
1546 1605 <div class="metasync-type-select-wrap">
1547 1606 <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1548 1607 <?php
1608 + if (!in_array($person_organization, $valid_person_organizations, true)) {
1609 + printf('<option value="0" selected="selected">Select Type</option>');
1610 + }
1549 1611 printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1550 1612 printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1551 1613 ?>
1552 1614 </select>
@@ -1557,10 +1619,10 @@
1557 1619
1558 1620 public function local_seo_name_callback()
1559 1621 {
1560 1622 printf(
1561 - '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1562 - 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())
1563 1625 );
1564 1626
1565 1627 printf(' <br> <span class="description"> Your name or company name </span>');
1566 1628 }
@@ -1568,11 +1630,18 @@
1568 1630 public function local_seo_logo_callback()
1569 1631 {
1570 1632 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1571 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 +
1572 1641 printf(
1573 - '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1574 - 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)
1575 1644 );
1576 1645
1577 1646 printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">');
1578 1647
@@ -1577,22 +1646,66 @@
1577 1646 printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">');
1578 1647
1579 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> ');
1580 1649
1581 - 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">');
1582 1651
1583 - $button_type = 'hidden';
1584 - if ($local_seo_logo) {
1585 - $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 '';
1586 1685 }
1587 - printf('<input type="%s" class="button-secondary no-loading" 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;
1588 1701 }
1589 1702
1590 1703 public function local_seo_url_callback()
1591 1704 {
1592 1705 printf(
1593 - '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1594 - 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())
1595 1708 );
1596 1709
1597 1710 printf(' <br> <span class="description"> URL of the item. </span>');
1598 1711 }
@@ -1599,9 +1712,9 @@
1599 1712
1600 1713 public function local_seo_email_callback()
1601 1714 {
1602 1715 printf(
1603 - '<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" />',
1604 1717 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1605 1718 );
1606 1719
1607 1720 printf(' <br> <span class="description"> Search engines display your email address. </span>');
@@ -1609,9 +1722,9 @@
1609 1722
1610 1723 public function local_seo_phone_callback()
1611 1724 {
1612 1725 printf(
1613 - '<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" />',
1614 1727 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1615 1728 );
1616 1729
1617 1730 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
@@ -1619,29 +1732,29 @@
1619 1732
1620 1733 public function local_seo_address_callback()
1621 1734 {
1622 1735 printf(
1623 - '<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>',
1624 1737 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1625 1738 );
1626 1739
1627 1740 printf(
1628 - '<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>',
1629 1742 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1630 1743 );
1631 1744
1632 1745 printf(
1633 - '<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>',
1634 1747 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1635 1748 );
1636 1749
1637 1750 printf(
1638 - '<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>',
1639 1752 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1640 1753 );
1641 1754
1642 1755 printf(
1643 - '<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>',
1644 1757 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1645 1758 );
1646 1759 }
1647 1760
@@ -1656,9 +1769,9 @@
1656 1769 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1657 1770 <option value='0'>Select Business Type</option>
1658 1771 <?php
1659 1772 foreach ($types as $type) {
1660 - 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));
1661 1774 }
1662 1775 ?>
1663 1776 </select>
1664 1777 <?php
@@ -1682,9 +1795,9 @@
1682 1795 <li>
1683 1796 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1684 1797 <?php
1685 1798 foreach ($days_name as $name) {
1686 - 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));
1687 1800 }
1688 1801 ?>
1689 1802 </select>
1690 1803 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
@@ -1763,9 +1876,9 @@
1763 1876
1764 1877 public function local_seo_price_range_callback()
1765 1878 {
1766 1879 printf(
1767 - '<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" />',
1768 1881 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1769 1882 );
1770 1883 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1771 1884 }
@@ -1778,9 +1891,9 @@
1778 1891 <?php
1779 1892 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1780 1893 $pages = get_pages();
1781 1894 foreach ($pages as $page) {
1782 - 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));
1783 1896 }
1784 1897 ?>
1785 1898 </select>
1786 1899 <?php
@@ -1795,9 +1908,9 @@
1795 1908 <?php
1796 1909 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1797 1910 $pages = get_pages();
1798 1911 foreach ($pages as $page) {
1799 - 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));
1800 1913 }
1801 1914 ?>
1802 1915 </select>
1803 1916 <?php
@@ -1806,9 +1919,9 @@
1806 1919
1807 1920 public function local_seo_map_key_callback()
1808 1921 {
1809 1922 printf(
1810 - '<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" />',
1811 1924 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1812 1925 );
1813 1926
1814 1927 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
@@ -1816,9 +1929,9 @@
1816 1929
1817 1930 public function local_seo_geo_coordinates_callback()
1818 1931 {
1819 1932 printf(
1820 - '<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" />',
1821 1934 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1822 1935 );
1823 1936
1824 1937 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
@@ -1826,9 +1939,9 @@
1826 1939
1827 1940 public function header_snippets_callback()
1828 1941 {
1829 1942 printf(
1830 - '<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>',
1831 1944 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1832 1945 );
1833 1946 }
1834 1947
@@ -1834,9 +1947,9 @@
1834 1947
1835 1948 public function footer_snippets_callback()
1836 1949 {
1837 1950 printf(
1838 - '<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>',
1839 1952 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1840 1953 );
1841 1954 }
1842 1955
@@ -1846,16 +1959,8 @@
1846 1959
1847 1960 ?>
1848 1961 <ul class="checkbox-list">
1849 1962 <li>
1850 - <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']) : '' ?>>
1851 - <label for="robots_common1">Index </br>
1852 - <span class="description">
1853 - <span>Search engines to index and show these pages in the search results.</span>
1854 - </span>
1855 - </label>
1856 - </li>
1857 - <li>
1858 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']) : '' ?>>
1859 1964 <label for="robots_common2">No Index </br>
1860 1965 <span class="description">
1861 1966 <span>Search engines not indexed and displayed this pages in search engine results</span>
@@ -1981,9 +2086,9 @@
1981 2086
1982 2087 public function global_open_graph_meta_callback()
1983 2088 {
1984 2089 printf(
1985 - '<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 />',
1986 2091 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
1987 2092 );
1988 2093 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
1989 2094 }
@@ -1990,9 +2095,9 @@
1990 2095
1991 2096 public function global_facebook_meta_callback()
1992 2097 {
1993 2098 printf(
1994 - '<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 />',
1995 2100 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
1996 2101 );
1997 2102 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
1998 2103 }
@@ -1999,9 +2104,9 @@
1999 2104
2000 2105 public function global_twitter_meta_callback()
2001 2106 {
2002 2107 printf(
2003 - '<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 />',
2004 2109 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2005 2110 );
2006 2111 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2007 2112 }
@@ -2009,9 +2114,9 @@
2009 2114 public function og_image_dimensions_callback()
2010 2115 {
2011 2116 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2012 2117 printf(
2013 - '<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 />',
2014 2119 $val === 'true' ? 'checked' : ''
2015 2120 );
2016 2121 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2017 2122 }
@@ -2019,9 +2124,9 @@
2019 2124 public function article_timestamps_callback()
2020 2125 {
2021 2126 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2022 2127 printf(
2023 - '<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 />',
2024 2129 $val === 'true' ? 'checked' : ''
2025 2130 );
2026 2131 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2027 2132 }
@@ -2029,9 +2134,9 @@
2029 2134 public function article_author_callback()
2030 2135 {
2031 2136 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2032 2137 printf(
2033 - '<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 />',
2034 2139 $val === 'true' ? 'checked' : ''
2035 2140 );
2036 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>');
2037 2142 }
@@ -2039,9 +2144,9 @@
2039 2144 public function article_section_callback()
2040 2145 {
2041 2146 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2042 2147 printf(
2043 - '<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 />',
2044 2149 $val === 'true' ? 'checked' : ''
2045 2150 );
2046 2151 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2047 2152 }
@@ -2049,9 +2154,9 @@
2049 2154 public function article_tags_callback()
2050 2155 {
2051 2156 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2052 2157 printf(
2053 - '<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 />',
2054 2159 $val === 'true' ? 'checked' : ''
2055 2160 );
2056 2161 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2057 2162 }
@@ -2059,9 +2164,9 @@
2059 2164 public function twitter_image_alt_callback()
2060 2165 {
2061 2166 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2062 2167 printf(
2063 - '<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 />',
2064 2169 $val === 'true' ? 'checked' : ''
2065 2170 );
2066 2171 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2067 2172 }
@@ -2068,9 +2173,9 @@
2068 2173
2069 2174 public function facebook_page_url_callback()
2070 2175 {
2071 2176 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2072 - 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));
2073 2178 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2074 2179 }
2075 2180
2076 2181 public function facebook_authorship_callback()
@@ -2075,9 +2180,9 @@
2075 2180
2076 2181 public function facebook_authorship_callback()
2077 2182 {
2078 2183 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2079 - 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));
2080 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>');
2081 2186 }
2082 2187
2083 2188 public function facebook_admin_callback()
@@ -2082,9 +2187,9 @@
2082 2187
2083 2188 public function facebook_admin_callback()
2084 2189 {
2085 2190 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2086 - 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));
2087 2192 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2088 2193 }
2089 2194
2090 2195 public function facebook_app_callback()
@@ -2089,9 +2194,9 @@
2089 2194
2090 2195 public function facebook_app_callback()
2091 2196 {
2092 2197 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2093 - 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));
2094 2199 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2095 2200 }
2096 2201
2097 2202 public function facebook_secret_callback()
@@ -2096,9 +2201,9 @@
2096 2201
2097 2202 public function facebook_secret_callback()
2098 2203 {
2099 2204 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2100 - 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));
2101 2206 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2102 2207 }
2103 2208
2104 2209 public function twitter_username_callback()
@@ -2103,9 +2208,9 @@
2103 2208
2104 2209 public function twitter_username_callback()
2105 2210 {
2106 2211 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2107 - 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));
2108 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>');
2109 2214 }
2110 2215
2111 2216 public static function get_business_types()
@@ -2280,8 +2385,14 @@
2280 2385 }
2281 2386
2282 2387 /**
2283 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".
2284 2395 */
2285 2396 public function render_breadcrumbs_section() {
2286 2397 $options = Metasync::get_option('breadcrumbs', array());
2287 2398
@@ -2286,9 +2397,9 @@
2286 2397 $options = Metasync::get_option('breadcrumbs', array());
2287 2398
2288 2399 $defaults = array(
2289 2400 'enabled' => true,
2290 - 'separator' => '&raquo;',
2401 + 'separator' => '»',
2291 2402 'home_label' => 'Home',
2292 2403 'home_url' => '',
2293 2404 'show_current_page' => true,
2294 2405 'prefix_text' => '',
@@ -2296,8 +2407,19 @@
2296 2407 'disable_schema' => false,
2297 2408 );
2298 2409
2299 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;
2300 2422 ?>
2301 2423 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2302 2424
2303 2425 <!-- Enable/Disable -->
@@ -2302,8 +2424,9 @@
2302 2424
2303 2425 <!-- Enable/Disable -->
2304 2426 <div style="margin-bottom: 24px;">
2305 2427 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2428 + <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0">
2306 2429 <input type="checkbox"
2307 2430 name="metasync_options[breadcrumbs][enabled]"
2308 2431 value="1"
2309 2432 <?php checked($options['enabled'], 1); ?>
@@ -2318,8 +2441,9 @@
2318 2441
2319 2442 <!-- Disable Schema Markup -->
2320 2443 <div style="margin-bottom: 24px;">
2321 2444 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2445 + <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0">
2322 2446 <input type="checkbox"
2323 2447 name="metasync_options[breadcrumbs][disable_schema]"
2324 2448 value="1"
2325 2449 <?php checked($options['disable_schema'], 1); ?>
@@ -2337,24 +2461,21 @@
2337 2461 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2338 2462 Separator Character
2339 2463 </label>
2340 2464 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2465 + <?php foreach ($separator_presets as $separator_option) : ?>
2341 2466 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2342 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2343 - <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>
2344 2469 </label>
2345 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2346 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2347 - <span style="color: var(--dashboard-text);">/</span>
2348 - </label>
2349 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2350 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2351 - <span style="color: var(--dashboard-text);">></span>
2352 - </label>
2353 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2354 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2355 - <span style="color: var(--dashboard-text);">·</span>
2356 - </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>
2357 2478 </div>
2358 2479 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2359 2480 Choose how to separate breadcrumb items
2360 2481 </p>
@@ -2394,8 +2515,9 @@
2394 2515
2395 2516 <!-- Show Current Page -->
2396 2517 <div style="margin-bottom: 24px;">
2397 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">
2398 2520 <input type="checkbox"
2399 2521 name="metasync_options[breadcrumbs][show_current_page]"
2400 2522 value="1"
2401 2523 <?php checked($options['show_current_page'], 1); ?>