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 +155 -92 2.6.24 → 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,8 +74,9 @@
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',
73 80 'otto_sg_optimizer_compat',
74 81 'bookingpress_session_compat',
75 82 )
@@ -81,8 +88,16 @@
81 88 'priority' => 22,
82 89 'default_open' => false,
83 90 'render_callback' => array('Metasync_Edge_Cache_Settings', 'render'),
84 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 + ),
85 100 'bot_detection' => array(
86 101 'title' => 'Bot Detection & Filtering',
87 102 'description' => 'Manage bot traffic and reduce unnecessary API calls',
88 103 'icon' => 'filter',
@@ -94,16 +109,8 @@
94 109 'otto_bot_blacklist',
95 110 'otto_bot_statistics_link'
96 111 )
97 112 ),
98 - 'breadcrumbs' => array(
99 - 'title' => 'Breadcrumbs',
100 - 'description' => 'Configure breadcrumb navigation display',
101 - 'icon' => 'format-links',
102 - 'priority' => 35,
103 - 'default_open' => false,
104 - 'render_callback' => array($this, 'render_breadcrumbs_section')
105 - ),
106 113 'open_graph' => array(
107 114 'title' => 'Open Graph',
108 115 'description' => 'Control which Open Graph and article meta tags are output',
109 116 'icon' => 'share',
@@ -130,8 +137,9 @@
130 137 'disable_redirection_metabox',
131 138 'disable_canonical_metabox',
132 139 'disable_social_opengraph_metabox',
133 140 'disable_schema_markup_metabox',
141 + 'disable_language_alternates_metabox',
134 142 'disable_seo_metabox',
135 143 'open_external_links'
136 144 )
137 145 ),
@@ -172,13 +180,22 @@
172 180 'fields' => array(
173 181 'permalink_structure',
174 182 'hide_dashboard_framework',
175 183 'show_admin_bar_status',
176 - 'enable_auto_updates',
177 184 'import_external_data'
178 185 )
179 186 )
180 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;
181 198 }
182 199
183 200 public function get_advanced_accordion_config() {
184 201 $config = array();
@@ -203,8 +220,17 @@
203 220 'default_open' => false,
204 221 'render_callback' => array($this->admin_instance, 'render_debug_mode_section')
205 222 );
206 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 +
207 233 $config['error_logs'] = array(
208 234 'title' => 'Error Logs',
209 235 'description' => 'View and manage error logs to troubleshoot issues',
210 236 'icon' => 'warning',
@@ -277,9 +303,9 @@
277 303 $content_state = $is_open ? 'open' : 'closed';
278 304
279 305 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
280 306
281 - 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) . '">';
282 308 echo '<div class="metasync-accordion-title">';
283 309 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
284 310 echo '<div class="metasync-accordion-text">';
285 311 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -290,9 +316,9 @@
290 316 echo '<span class="toggle-icon">▼</span>';
291 317 echo '</button>';
292 318 echo '</div>';
293 319
294 - 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) . '">';
295 321
296 322 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
297 323 call_user_func($section_data['render_callback']);
298 324 }
@@ -386,14 +412,18 @@
386 412 if (!function_exists('google_index_direct')) {
387 413 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
388 414 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
389 415 } else {
390 - 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
391 417 return;
392 418 }
393 419 }
394 420
395 421 $google_index = google_index_direct();
422 + if (!$google_index) {
423 + // Nothing to render against, and calling through would be fatal.
424 + return;
425 + }
396 426 $service_info = $google_index->get_service_account_info();
397 427 $is_configured = !isset($service_info['error']);
398 428
399 429 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
@@ -672,9 +702,9 @@
672 702 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
673 703 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
674 704 </p>
675 705
676 - <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">
677 707 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
678 708 <input type="hidden" name="save_plugin_access_roles" value="yes" />
679 709
680 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;">
@@ -792,9 +822,9 @@
792 822
793 823 public function can_change_memory_limit() {
794 824 $current_limit = ini_get('memory_limit');
795 825
796 - $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
797 827
798 828 return $test_result !== false;
799 829 }
800 830
@@ -846,9 +876,9 @@
846 876 }
847 877
848 878 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
849 879
850 - $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
851 881
852 882 return $result !== false;
853 883 }
854 884
@@ -1100,8 +1130,9 @@
1100 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),
1101 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),
1102 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),
1103 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),
1104 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),
1105 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),
1106 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.',
1107 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),
@@ -1107,14 +1138,15 @@
1107 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),
1108 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),
1109 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),
1110 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.',
1111 - '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.',
1112 - '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.',
1113 - '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.',
1114 - '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.',
1115 - '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.',
1116 - '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')),
1117 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).',
1118 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),
1119 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.',
1120 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),
@@ -1119,9 +1151,8 @@
1119 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.',
1120 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),
1121 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),
1122 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),
1123 - '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),
1124 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),
1125 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),
1126 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.',
1127 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.',
@@ -1167,9 +1198,9 @@
1167 1198 $content_state = $is_open ? 'open' : 'closed';
1168 1199
1169 1200 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1170 1201
1171 - 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) . '">';
1172 1203 echo '<div class="metasync-accordion-title">';
1173 1204 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1174 1205 echo '<div class="metasync-accordion-text">';
1175 1206 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
@@ -1180,9 +1211,9 @@
1180 1211 echo '<span class="toggle-icon">▼</span>';
1181 1212 echo '</button>';
1182 1213 echo '</div>';
1183 1214
1184 - 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) . '">';
1185 1216
1186 1217 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1187 1218 call_user_func($section_data['render_callback']);
1188 1219 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
@@ -1197,9 +1228,9 @@
1197 1228 echo '<tr>';
1198 1229 echo '<th scope="row">';
1199 1230 if (!empty($field['title'])) {
1200 1231 echo '<div class="metasync-field-label-wrapper">';
1201 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1232 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1202 1233
1203 1234 if (isset($tooltips[$field_id])) {
1204 1235 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1205 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">';
@@ -1254,9 +1285,9 @@
1254 1285 echo '<tr>';
1255 1286 echo '<th scope="row">';
1256 1287 if (!empty($field['title'])) {
1257 1288 echo '<div class="metasync-field-label-wrapper">';
1258 - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1289 + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
1259 1290 if (isset($llms_tooltips[$field['id']])) {
1260 1291 Metasync::render_tooltip_icon($field['id'], $llms_tooltips[$field['id']]);
1261 1292 }
1262 1293 echo '</div>';
@@ -1310,32 +1341,32 @@
1310 1341 $display_value = $current_token;
1311 1342 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1312 1343 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1313 1344 } else {
1314 - $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name());
1315 - $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() . '".';
1316 1347 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1317 1348 }
1318 1349
1319 1350 printf('<div class="metasync-auth-token-row">');
1320 1351 printf(
1321 - '<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" />',
1322 1353 esc_attr($display_value)
1323 1354 );
1324 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>');
1325 1356 printf('</div>');
1326 - 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));
1327 1358 }
1328 1359
1329 1360 public function linkgraph_token_callback()
1330 1361 {
1331 1362 printf(
1332 - '<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" />',
1333 1364 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1334 1365 );
1335 1366
1336 1367 printf(
1337 - '<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" />',
1338 1369 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1339 1370 );
1340 1371
1341 1372 ?>
@@ -1460,9 +1491,9 @@
1460 1491 printf('• The authentication window will open in a popup - please allow popups<br/>');
1461 1492 printf('• The process typically takes 15-30 seconds to complete<br/>');
1462 1493 printf('• Your API key will be stored securely and shown masked below<br/>');
1463 1494 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1464 - 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));
1465 1496 printf('</div>');
1466 1497 printf('</details>');
1467 1498 printf('</div>');
1468 1499
@@ -1514,9 +1545,9 @@
1514 1545 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1515 1546 printf(
1516 1547 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1517 1548 esc_attr($timestamp),
1518 - $this->time_elapsed_string($timestamp)
1549 + esc_html($this->time_elapsed_string($timestamp))
1519 1550 );
1520 1551
1521 1552
1522 1553 }
@@ -1524,9 +1555,9 @@
1524 1555
1525 1556 public function bing_site_verification_callback()
1526 1557 {
1527 1558 printf(
1528 - '<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" />',
1529 1560 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1530 1561 );
1531 1562
1532 1563 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
@@ -1535,9 +1566,9 @@
1535 1566
1536 1567 public function yandex_site_verification_callback()
1537 1568 {
1538 1569 printf(
1539 - '<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" />',
1540 1571 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1541 1572 );
1542 1573
1543 1574 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
@@ -1546,9 +1577,9 @@
1546 1577
1547 1578 public function google_site_verification_callback()
1548 1579 {
1549 1580 printf(
1550 - '<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" />',
1551 1582 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1552 1583 );
1553 1584
1554 1585 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
@@ -1557,9 +1588,9 @@
1557 1588
1558 1589 public function pinterest_site_verification_callback()
1559 1590 {
1560 1591 printf(
1561 - '<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" />',
1562 1593 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1563 1594 );
1564 1595
1565 1596 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
@@ -1588,10 +1619,10 @@
1588 1619
1589 1620 public function local_seo_name_callback()
1590 1621 {
1591 1622 printf(
1592 - '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1593 - 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())
1594 1625 );
1595 1626
1596 1627 printf(' <br> <span class="description"> Your name or company name </span>');
1597 1628 }
@@ -1599,11 +1630,18 @@
1599 1630 public function local_seo_logo_callback()
1600 1631 {
1601 1632 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1602 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 +
1603 1641 printf(
1604 - '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1605 - 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)
1606 1644 );
1607 1645
1608 1646 printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">');
1609 1647
@@ -1624,9 +1662,9 @@
1624 1662 $logo_url ? '' : ' style="display:none"'
1625 1663 );
1626 1664
1627 1665 $button_type = $logo_url ? 'button' : 'hidden';
1628 - printf('<input type="%s" class="button-secondary no-loading metasync-logo-remove-btn" id="local_seo_logo_close_btn" value="X">', $button_type);
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));
1629 1667 printf('</span>');
1630 1668 }
1631 1669
1632 1670 /**
@@ -1645,8 +1683,16 @@
1645 1683 if (empty($value)) {
1646 1684 return '';
1647 1685 }
1648 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 +
1649 1695 if (is_numeric($value)) {
1650 1696 $url = wp_get_attachment_image_url((int) $value, $size);
1651 1697 return $url ? $url : '';
1652 1698 }
@@ -1656,10 +1702,10 @@
1656 1702
1657 1703 public function local_seo_url_callback()
1658 1704 {
1659 1705 printf(
1660 - '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1661 - 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())
1662 1708 );
1663 1709
1664 1710 printf(' <br> <span class="description"> URL of the item. </span>');
1665 1711 }
@@ -1666,9 +1712,9 @@
1666 1712
1667 1713 public function local_seo_email_callback()
1668 1714 {
1669 1715 printf(
1670 - '<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" />',
1671 1717 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1672 1718 );
1673 1719
1674 1720 printf(' <br> <span class="description"> Search engines display your email address. </span>');
@@ -1676,9 +1722,9 @@
1676 1722
1677 1723 public function local_seo_phone_callback()
1678 1724 {
1679 1725 printf(
1680 - '<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" />',
1681 1727 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1682 1728 );
1683 1729
1684 1730 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
@@ -1686,29 +1732,29 @@
1686 1732
1687 1733 public function local_seo_address_callback()
1688 1734 {
1689 1735 printf(
1690 - '<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>',
1691 1737 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1692 1738 );
1693 1739
1694 1740 printf(
1695 - '<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>',
1696 1742 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1697 1743 );
1698 1744
1699 1745 printf(
1700 - '<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>',
1701 1747 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1702 1748 );
1703 1749
1704 1750 printf(
1705 - '<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>',
1706 1752 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1707 1753 );
1708 1754
1709 1755 printf(
1710 - '<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>',
1711 1757 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1712 1758 );
1713 1759 }
1714 1760
@@ -1723,9 +1769,9 @@
1723 1769 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1724 1770 <option value='0'>Select Business Type</option>
1725 1771 <?php
1726 1772 foreach ($types as $type) {
1727 - 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));
1728 1774 }
1729 1775 ?>
1730 1776 </select>
1731 1777 <?php
@@ -1749,9 +1795,9 @@
1749 1795 <li>
1750 1796 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1751 1797 <?php
1752 1798 foreach ($days_name as $name) {
1753 - 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));
1754 1800 }
1755 1801 ?>
1756 1802 </select>
1757 1803 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
@@ -1830,9 +1876,9 @@
1830 1876
1831 1877 public function local_seo_price_range_callback()
1832 1878 {
1833 1879 printf(
1834 - '<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" />',
1835 1881 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1836 1882 );
1837 1883 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1838 1884 }
@@ -1845,9 +1891,9 @@
1845 1891 <?php
1846 1892 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1847 1893 $pages = get_pages();
1848 1894 foreach ($pages as $page) {
1849 - 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));
1850 1896 }
1851 1897 ?>
1852 1898 </select>
1853 1899 <?php
@@ -1862,9 +1908,9 @@
1862 1908 <?php
1863 1909 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1864 1910 $pages = get_pages();
1865 1911 foreach ($pages as $page) {
1866 - 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));
1867 1913 }
1868 1914 ?>
1869 1915 </select>
1870 1916 <?php
@@ -1873,9 +1919,9 @@
1873 1919
1874 1920 public function local_seo_map_key_callback()
1875 1921 {
1876 1922 printf(
1877 - '<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" />',
1878 1924 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1879 1925 );
1880 1926
1881 1927 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
@@ -1883,9 +1929,9 @@
1883 1929
1884 1930 public function local_seo_geo_coordinates_callback()
1885 1931 {
1886 1932 printf(
1887 - '<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" />',
1888 1934 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1889 1935 );
1890 1936
1891 1937 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
@@ -1893,9 +1939,9 @@
1893 1939
1894 1940 public function header_snippets_callback()
1895 1941 {
1896 1942 printf(
1897 - '<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>',
1898 1944 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1899 1945 );
1900 1946 }
1901 1947
@@ -1901,9 +1947,9 @@
1901 1947
1902 1948 public function footer_snippets_callback()
1903 1949 {
1904 1950 printf(
1905 - '<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>',
1906 1952 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1907 1953 );
1908 1954 }
1909 1955
@@ -2040,9 +2086,9 @@
2040 2086
2041 2087 public function global_open_graph_meta_callback()
2042 2088 {
2043 2089 printf(
2044 - '<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 />',
2045 2091 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
2046 2092 );
2047 2093 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
2048 2094 }
@@ -2049,9 +2095,9 @@
2049 2095
2050 2096 public function global_facebook_meta_callback()
2051 2097 {
2052 2098 printf(
2053 - '<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 />',
2054 2100 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
2055 2101 );
2056 2102 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
2057 2103 }
@@ -2058,9 +2104,9 @@
2058 2104
2059 2105 public function global_twitter_meta_callback()
2060 2106 {
2061 2107 printf(
2062 - '<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 />',
2063 2109 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2064 2110 );
2065 2111 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2066 2112 }
@@ -2068,9 +2114,9 @@
2068 2114 public function og_image_dimensions_callback()
2069 2115 {
2070 2116 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2071 2117 printf(
2072 - '<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 />',
2073 2119 $val === 'true' ? 'checked' : ''
2074 2120 );
2075 2121 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2076 2122 }
@@ -2078,9 +2124,9 @@
2078 2124 public function article_timestamps_callback()
2079 2125 {
2080 2126 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2081 2127 printf(
2082 - '<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 />',
2083 2129 $val === 'true' ? 'checked' : ''
2084 2130 );
2085 2131 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2086 2132 }
@@ -2088,9 +2134,9 @@
2088 2134 public function article_author_callback()
2089 2135 {
2090 2136 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2091 2137 printf(
2092 - '<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 />',
2093 2139 $val === 'true' ? 'checked' : ''
2094 2140 );
2095 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>');
2096 2142 }
@@ -2098,9 +2144,9 @@
2098 2144 public function article_section_callback()
2099 2145 {
2100 2146 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2101 2147 printf(
2102 - '<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 />',
2103 2149 $val === 'true' ? 'checked' : ''
2104 2150 );
2105 2151 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2106 2152 }
@@ -2108,9 +2154,9 @@
2108 2154 public function article_tags_callback()
2109 2155 {
2110 2156 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2111 2157 printf(
2112 - '<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 />',
2113 2159 $val === 'true' ? 'checked' : ''
2114 2160 );
2115 2161 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2116 2162 }
@@ -2118,9 +2164,9 @@
2118 2164 public function twitter_image_alt_callback()
2119 2165 {
2120 2166 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2121 2167 printf(
2122 - '<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 />',
2123 2169 $val === 'true' ? 'checked' : ''
2124 2170 );
2125 2171 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2126 2172 }
@@ -2127,9 +2173,9 @@
2127 2173
2128 2174 public function facebook_page_url_callback()
2129 2175 {
2130 2176 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2131 - 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));
2132 2178 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2133 2179 }
2134 2180
2135 2181 public function facebook_authorship_callback()
@@ -2134,9 +2180,9 @@
2134 2180
2135 2181 public function facebook_authorship_callback()
2136 2182 {
2137 2183 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2138 - 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));
2139 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>');
2140 2186 }
2141 2187
2142 2188 public function facebook_admin_callback()
@@ -2141,9 +2187,9 @@
2141 2187
2142 2188 public function facebook_admin_callback()
2143 2189 {
2144 2190 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2145 - 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));
2146 2192 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2147 2193 }
2148 2194
2149 2195 public function facebook_app_callback()
@@ -2148,9 +2194,9 @@
2148 2194
2149 2195 public function facebook_app_callback()
2150 2196 {
2151 2197 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2152 - 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));
2153 2199 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2154 2200 }
2155 2201
2156 2202 public function facebook_secret_callback()
@@ -2155,9 +2201,9 @@
2155 2201
2156 2202 public function facebook_secret_callback()
2157 2203 {
2158 2204 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2159 - 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));
2160 2206 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2161 2207 }
2162 2208
2163 2209 public function twitter_username_callback()
@@ -2162,9 +2208,9 @@
2162 2208
2163 2209 public function twitter_username_callback()
2164 2210 {
2165 2211 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2166 - 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));
2167 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>');
2168 2214 }
2169 2215
2170 2216 public static function get_business_types()
@@ -2339,8 +2385,14 @@
2339 2385 }
2340 2386
2341 2387 /**
2342 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".
2343 2395 */
2344 2396 public function render_breadcrumbs_section() {
2345 2397 $options = Metasync::get_option('breadcrumbs', array());
2346 2398
@@ -2345,9 +2397,9 @@
2345 2397 $options = Metasync::get_option('breadcrumbs', array());
2346 2398
2347 2399 $defaults = array(
2348 2400 'enabled' => true,
2349 - 'separator' => '&raquo;',
2401 + 'separator' => '»',
2350 2402 'home_label' => 'Home',
2351 2403 'home_url' => '',
2352 2404 'show_current_page' => true,
2353 2405 'prefix_text' => '',
@@ -2355,8 +2407,19 @@
2355 2407 'disable_schema' => false,
2356 2408 );
2357 2409
2358 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;
2359 2422 ?>
2360 2423 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2361 2424
2362 2425 <!-- Enable/Disable -->
@@ -2361,8 +2424,9 @@
2361 2424
2362 2425 <!-- Enable/Disable -->
2363 2426 <div style="margin-bottom: 24px;">
2364 2427 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2428 + <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0">
2365 2429 <input type="checkbox"
2366 2430 name="metasync_options[breadcrumbs][enabled]"
2367 2431 value="1"
2368 2432 <?php checked($options['enabled'], 1); ?>
@@ -2377,8 +2441,9 @@
2377 2441
2378 2442 <!-- Disable Schema Markup -->
2379 2443 <div style="margin-bottom: 24px;">
2380 2444 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2445 + <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0">
2381 2446 <input type="checkbox"
2382 2447 name="metasync_options[breadcrumbs][disable_schema]"
2383 2448 value="1"
2384 2449 <?php checked($options['disable_schema'], 1); ?>
@@ -2396,24 +2461,21 @@
2396 2461 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2397 2462 Separator Character
2398 2463 </label>
2399 2464 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2465 + <?php foreach ($separator_presets as $separator_option) : ?>
2400 2466 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2401 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2402 - <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>
2403 2469 </label>
2404 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2405 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2406 - <span style="color: var(--dashboard-text);">/</span>
2407 - </label>
2408 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2409 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2410 - <span style="color: var(--dashboard-text);">></span>
2411 - </label>
2412 - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2413 - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2414 - <span style="color: var(--dashboard-text);">·</span>
2415 - </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>
2416 2478 </div>
2417 2479 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2418 2480 Choose how to separate breadcrumb items
2419 2481 </p>
@@ -2453,8 +2515,9 @@
2453 2515
2454 2516 <!-- Show Current Page -->
2455 2517 <div style="margin-bottom: 24px;">
2456 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">
2457 2520 <input type="checkbox"
2458 2521 name="metasync_options[breadcrumbs][show_current_page]"
2459 2522 value="1"
2460 2523 <?php checked($options['show_current_page'], 1); ?>