PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.11
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.11
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
metasync / includes / class-metasync-settings-registration.php

class-metasync-settings-registration.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.11, at includes/class-metasync-settings-registration.php

3,257 lines 162.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * Settings Registration manager.
8 *
9 * Extracted from Metasync_Admin to keep the admin class focused on UI concerns.
10 * Handles register_setting / add_settings_section / add_settings_field calls
11 * (settings_page_init) and the sanitize callback for the main option group.
12 *
13 * @package Metasync
14 * @subpackage Metasync/includes
15 */
16 class Metasync_Settings_Registration
17 {
18 /** @var self|null */
19 private static $instance = null;
20
21 /**
22 * Get singleton instance.
23 *
24 * @return self
25 */
26 public static function instance()
27 {
28 if (self::$instance === null) {
29 self::$instance = new self();
30 }
31 return self::$instance;
32 }
33
34 private function __construct() {}
35
36 /**
37 * Helper – replicate Metasync_Admin::get_effective_menu_title() without
38 * needing an admin instance.
39 */
40 private function get_effective_menu_title()
41 {
42 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
43 if ($whitelabel_company_name) {
44 return $whitelabel_company_name . ' SEO';
45 }
46 return Metasync::get_effective_plugin_name();
47 }
48
49 /**
50 * Handle saving plugin access roles from Advanced Settings.
51 * Moved from Metasync_Admin – only called from settings_page_init().
52 */
53 private function handle_plugin_access_roles_save()
54 {
55 if (isset($_POST['save_plugin_access_roles']) && $_POST['save_plugin_access_roles'] === 'yes') {
56 if (isset($_POST['plugin_access_roles_nonce']) && wp_verify_nonce($_POST['plugin_access_roles_nonce'], 'metasync_plugin_access_roles_nonce')) {
57
58 if (!Metasync::current_user_has_plugin_access()) {
59 wp_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced&access_roles_error=1&message=' . urlencode('Insufficient permissions')));
60 exit;
61 }
62
63 $metasync_options = Metasync::get_option();
64 if (!is_array($metasync_options)) {
65 $metasync_options = array();
66 }
67 if (!isset($metasync_options['general']) || !is_array($metasync_options['general'])) {
68 $metasync_options['general'] = array();
69 }
70
71 if (isset($_POST['plugin_access_roles']) && is_array($_POST['plugin_access_roles'])) {
72 $metasync_options['general']['plugin_access_roles'] = array_map('sanitize_text_field', $_POST['plugin_access_roles']);
73 } else {
74 $metasync_options['general']['plugin_access_roles'] = array();
75 }
76
77 Metasync::set_option($metasync_options);
78
79 $redirect_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced&access_roles_saved=1');
80 wp_redirect($redirect_url);
81 exit;
82 } else {
83 $redirect_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced&access_roles_error=1&message=' . urlencode('Invalid security token'));
84 wp_redirect($redirect_url);
85 exit;
86 }
87 }
88 }
89
90 /**
91 * Register and add settings – formerly Metasync_Admin::settings_page_init().
92 */
93 public function settings_page_init()
94 {
95 Metasync_Connect_Manager::instance()->handle_session_management_early();
96 Metasync_Connect_Manager::instance()->handle_whitelabel_password_early();
97
98 Metasync_Debug_Manager::instance()->handle_debug_mode_operations();
99 Metasync_Debug_Manager::instance()->handle_error_log_operations();
100 Metasync_Debug_Manager::instance()->handle_clear_all_settings();
101
102 $this->handle_plugin_access_roles_save();
103
104 $SECTION_FEATURES = Metasync_Admin::SECTION_FEATURES;
105 $SECTION_METASYNC = Metasync_Admin::SECTION_METASYNC;
106 $SECTION_SEARCHENGINE = Metasync_Admin::SECTION_SEARCHENGINE;
107 $SECTION_LOCALSEO = Metasync_Admin::SECTION_LOCALSEO;
108 $SECTION_CODESNIPPETS = Metasync_Admin::SECTION_CODESNIPPETS;
109 $SECTION_OPTIMAL_SETTINGS = Metasync_Admin::SECTION_OPTIMAL_SETTINGS;
110 $SECTION_SITE_SETTINGS = Metasync_Admin::SECTION_SITE_SETTINGS;
111 $SECTION_COMMON_SETTINGS = Metasync_Admin::SECTION_COMMON_SETTINGS;
112 $SECTION_COMMON_META_SETTINGS = Metasync_Admin::SECTION_COMMON_META_SETTINGS;
113 $SECTION_SOCIAL_META = Metasync_Admin::SECTION_SOCIAL_META;
114 $SECTION_SEO_CONTROLS = Metasync_Admin::SECTION_SEO_CONTROLS;
115 $SECTION_SEO_CONTROLS_ADVANCED = Metasync_Admin::SECTION_SEO_CONTROLS_ADVANCED;
116 $SECTION_SEO_CONTROLS_INSTANT_INDEX = Metasync_Admin::SECTION_SEO_CONTROLS_INSTANT_INDEX;
117 $SECTION_PLUGIN_VISIBILITY = Metasync_Admin::SECTION_PLUGIN_VISIBILITY;
118 $SECTION_BREADCRUMBS = Metasync_Admin::SECTION_BREADCRUMBS;
119 $SECTION_LLMS_TXT = Metasync_Admin::SECTION_LLMS_TXT;
120
121 $option_key = Metasync_Admin::option_key;
122 $page_slug = Metasync_Admin::$page_slug;
123
124 # Use whitelabel OTTO name if configured, fallback to 'OTTO'
125 $whitelabel_otto_name = Metasync::get_whitelabel_otto_name();
126
127 register_setting(
128 Metasync_Admin::option_group,
129 Metasync_Admin::option_key,
130 array(self::instance(), 'sanitize')
131 );
132
133 // LLMs.txt settings are stored in a dedicated option so MCP tools and
134 // the admin UI share one source of truth.
135 register_setting(
136 Metasync_Admin::option_group,
137 'metasync_llms_txt_settings',
138 array(
139 'sanitize_callback' => array(self::instance(), 'sanitize_llms_txt_settings'),
140 'default' => array(),
141 )
142 );
143
144
145 add_settings_section(
146 $SECTION_METASYNC, // ID
147 '', // Title - removed to prevent duplication with dashboard card
148 function(){}, // Callback
149 $page_slug . '_general' // Page
150 );
151
152 add_settings_section(
153 $SECTION_LLMS_TXT,
154 esc_html__('LLMs.txt Settings', 'metasync'),
155 function () {
156 echo '<p>' . esc_html__('Publish a standards-compliant /llms.txt so AI search engines and assistants can discover your most important pages.', 'metasync') . '</p>';
157 },
158 $page_slug . '_general'
159 );
160
161 add_settings_field(
162 'llms_txt_enabled',
163 esc_html__('Enable /llms.txt', 'metasync'),
164 function () {
165 $settings = get_option('metasync_llms_txt_settings', array());
166 $checked = !empty($settings['enabled']);
167 printf(
168 '<input type="checkbox" id="llms_txt_enabled" name="metasync_llms_txt_settings[enabled]" value="1" %s />',
169 $checked ? 'checked' : ''
170 );
171 echo '<span class="description">' . esc_html__('Serve /llms.txt via a virtual route.', 'metasync') . '</span>';
172 },
173 $page_slug . '_general',
174 $SECTION_LLMS_TXT
175 );
176
177 add_settings_field(
178 'llms_txt_post_types',
179 esc_html__('Post types to include', 'metasync'),
180 function () {
181 $settings = get_option('metasync_llms_txt_settings', array());
182 $selected = isset($settings['post_types']) && is_array($settings['post_types'])
183 ? $settings['post_types']
184 : array('page', 'post');
185 $public_types = get_post_types(array('public' => true), 'objects');
186 echo '<div class="llms-txt-dependent">';
187 foreach ($public_types as $type) {
188 if (in_array($type->name, array('attachment'), true)) {
189 continue;
190 }
191 printf(
192 '<label style="display:inline-block;margin-right:12px;"><input type="checkbox" name="metasync_llms_txt_settings[post_types][]" value="%1$s" %2$s /> %3$s</label>',
193 esc_attr($type->name),
194 in_array($type->name, $selected, true) ? 'checked' : '',
195 esc_html($type->labels->singular_name ?: $type->name)
196 );
197 }
198 echo '</div>';
199 },
200 $page_slug . '_general',
201 $SECTION_LLMS_TXT
202 );
203
204 add_settings_field(
205 'llms_txt_max_posts',
206 esc_html__('Max posts to include', 'metasync'),
207 function () {
208 $settings = get_option('metasync_llms_txt_settings', array());
209 $value = isset($settings['max_posts']) ? (int) $settings['max_posts'] : 50;
210 echo '<div class="llms-txt-dependent">';
211 printf(
212 '<input type="number" id="llms_txt_max_posts" name="metasync_llms_txt_settings[max_posts]" min="1" max="500" value="%d" />',
213 $value
214 );
215 echo '</div>';
216 },
217 $page_slug . '_general',
218 $SECTION_LLMS_TXT
219 );
220
221 add_settings_field(
222 'llms_txt_excluded_ids',
223 esc_html__('Excluded post/page IDs', 'metasync'),
224 function () {
225 $settings = get_option('metasync_llms_txt_settings', array());
226 $ids = isset($settings['excluded_ids']) && is_array($settings['excluded_ids'])
227 ? implode(', ', array_map('absint', $settings['excluded_ids']))
228 : '';
229 echo '<div class="llms-txt-dependent">';
230 printf(
231 '<input type="text" id="llms_txt_excluded_ids" name="metasync_llms_txt_settings[excluded_ids]" value="%s" size="40" />',
232 esc_attr($ids)
233 );
234 echo '<p class="description">' . esc_html__('Comma-separated list of IDs to exclude from the listing.', 'metasync') . '</p>';
235 echo '</div>';
236 },
237 $page_slug . '_general',
238 $SECTION_LLMS_TXT
239 );
240
241 add_settings_field(
242 'llms_txt_custom_description',
243 esc_html__('Custom description', 'metasync'),
244 function () {
245 $settings = get_option('metasync_llms_txt_settings', array());
246 $value = isset($settings['custom_description']) ? (string) $settings['custom_description'] : '';
247 echo '<div class="llms-txt-dependent">';
248 printf(
249 '<input type="text" id="llms_txt_custom_description" name="metasync_llms_txt_settings[custom_description]" value="%s" size="60" />',
250 esc_attr($value)
251 );
252 echo '<p class="description">' . esc_html__('Overrides the site tagline in the /llms.txt header.', 'metasync') . '</p>';
253 echo '</div>';
254 },
255 $page_slug . '_general',
256 $SECTION_LLMS_TXT
257 );
258
259 add_settings_field(
260 'llms_full_enabled',
261 esc_html__('Enable /llms-full.txt', 'metasync'),
262 function () {
263 $settings = get_option('metasync_llms_txt_settings', array());
264 $checked = !empty($settings['llms_full_enabled']);
265 echo '<div class="llms-txt-dependent">';
266 printf(
267 '<input type="checkbox" id="llms_full_enabled" name="metasync_llms_txt_settings[llms_full_enabled]" value="1" %s />',
268 $checked ? 'checked' : ''
269 );
270 echo '<span class="description">' . esc_html__('Also serve /llms-full.txt with full post bodies converted to markdown.', 'metasync') . '</span>';
271 echo '</div>';
272 },
273 $page_slug . '_general',
274 $SECTION_LLMS_TXT
275 );
276
277 add_settings_field(
278 'llms_txt_max_posts_full',
279 esc_html__('Max posts for full-text', 'metasync'),
280 function () {
281 $settings = get_option('metasync_llms_txt_settings', array());
282 $value = isset($settings['max_posts_full']) ? (int) $settings['max_posts_full'] : 25;
283 echo '<div class="llms-full-dependent">';
284 printf(
285 '<input type="number" id="llms_txt_max_posts_full" name="metasync_llms_txt_settings[max_posts_full]" min="1" max="500" value="%d" />',
286 $value
287 );
288 echo '<p class="description">' . esc_html__('Maximum posts for /llms-full.txt (lower than summary to protect server performance).', 'metasync') . '</p>';
289 echo '</div>';
290 },
291 $page_slug . '_general',
292 $SECTION_LLMS_TXT
293 );
294
295
296 add_settings_section(
297 $SECTION_METASYNC, // ID
298 '', // Title - removed to prevent duplication with dashboard card
299 function(){}, // Callback
300 $page_slug . '_branding' // Page
301 );
302
303 add_settings_section(
304 $SECTION_METASYNC, // ID
305 $this->get_effective_menu_title() . ' Caching Settings:', // Title
306 function(){}, // Callback
307 $page_slug . '_otto_cache' // Page
308 );
309
310 add_settings_section(
311 $SECTION_SEO_CONTROLS, // ID
312 '', // Title - removed to prevent duplication with dashboard card
313 function(){}, // Callback
314 $page_slug . '_seo-controls' // Page
315 );
316
317 // Archive Indexation Control Fields
318 add_settings_field(
319 'noindex_empty_archives',
320 'Disallow Empty Archives',
321 function() use ($option_key) {
322 $noindex_empty_archives = Metasync::get_option('seo_controls')['noindex_empty_archives'] ?? 'false';
323 printf(
324 '<input type="checkbox" id="noindex_empty_archives" name="' . $option_key . '[seo_controls][noindex_empty_archives]" value="true" %s />',
325 isset($noindex_empty_archives) && $noindex_empty_archives == 'true' ? 'checked' : ''
326 );
327 printf('<span class="description"><strong>Empty Archives:</strong> When checked, automatically adds noindex to category, tag, author, and format archive pages that have no posts. Once posts are added to these archives, they will automatically be allowed for indexing. This prevents thin content pages from being indexed.</span>');
328 },
329 $page_slug . '_seo-controls',
330 $SECTION_SEO_CONTROLS
331 );
332
333 add_settings_field(
334 'index_date_archives',
335 'Disallow Date Archives',
336 function() use ($option_key) {
337 $index_date_archives = Metasync::get_option('seo_controls')['index_date_archives'] ?? 'false';
338 printf(
339 '<input type="checkbox" id="index_date_archives" name="' . $option_key . '[seo_controls][index_date_archives]" value="true" %s />',
340 isset($index_date_archives) && $index_date_archives == 'true' ? 'checked' : ''
341 );
342 printf('<span class="description"><strong>Date Archives:</strong> When checked, prevents search engines from indexing date-based archive pages (e.g., /2024/01/, /2024/01/15/). These pages often have thin content and can dilute your site\'s SEO value. Recommended for most sites.</span>');
343 },
344 $page_slug . '_seo-controls',
345 $SECTION_SEO_CONTROLS
346 );
347
348 add_settings_field(
349 'index_tag_archives',
350 'Disallow Tag Archives',
351 function() use ($option_key) {
352 $index_tag_archives = Metasync::get_option('seo_controls')['index_tag_archives'] ?? 'false';
353 printf(
354 '<input type="checkbox" id="index_tag_archives" name="' . $option_key . '[seo_controls][index_tag_archives]" value="true" %s />',
355 isset($index_tag_archives) && $index_tag_archives == 'true' ? 'checked' : ''
356 );
357 printf('<span class="description"><strong>Tag Archives:</strong> When checked, prevents search engines from indexing tag archive pages (e.g., /tag/technology/). Useful if you have many low-quality tag pages or want to focus on category-based organization instead.</span>');
358 },
359 $page_slug . '_seo-controls',
360 $SECTION_SEO_CONTROLS
361 );
362
363 add_settings_field(
364 'index_author_archives',
365 'Disallow Author Archives',
366 function() use ($option_key) {
367 $index_author_archives = Metasync::get_option('seo_controls')['index_author_archives'] ?? 'false';
368 printf(
369 '<input type="checkbox" id="index_author_archives" name="' . $option_key . '[seo_controls][index_author_archives]" value="true" %s />',
370 isset($index_author_archives) && $index_author_archives == 'true' ? 'checked' : ''
371 );
372 printf('<span class="description"><strong>Author Archives:</strong> When checked, prevents search engines from indexing author archive pages (e.g., /author/john-doe/). Recommended for single-author sites or when author pages don\'t provide unique value. Multi-author sites may want to keep this unchecked.</span>');
373 },
374 $page_slug . '_seo-controls',
375 $SECTION_SEO_CONTROLS
376 );
377
378 add_settings_field(
379 'index_format_archives',
380 'Disallow Format Archives',
381 function() use ($option_key) {
382 $index_format_archives = Metasync::get_option('seo_controls')['index_format_archives'] ?? 'false';
383 printf(
384 '<input type="checkbox" id="index_format_archives" name="' . $option_key . '[seo_controls][index_format_archives]" value="true" %s />',
385 isset($index_format_archives) && $index_format_archives == 'true' ? 'checked' : ''
386 );
387 printf('<span class="description"><strong>Format Archives:</strong> When checked, prevents search engines from indexing post format archive pages (e.g., /type/aside/, /type/gallery/). These are rarely useful for SEO and can create duplicate content issues. Recommended for most sites.</span>');
388 },
389 $page_slug . '_seo-controls',
390 $SECTION_SEO_CONTROLS
391 );
392
393 add_settings_field(
394 'index_category_archives',
395 'Disallow Category Archives',
396 function() use ($option_key) {
397 $index_category_archives = Metasync::get_option('seo_controls')['index_category_archives'] ?? 'false';
398 printf(
399 '<input type="checkbox" id="index_category_archives" name="' . $option_key . '[seo_controls][index_category_archives]" value="true" %s />',
400 isset($index_category_archives) && $index_category_archives == 'true' ? 'checked' : ''
401 );
402 printf('<span class="description"><strong>Category Archives:</strong> When checked, prevents search engines from indexing category archive pages (e.g., /category/news/). This may be useful if your category pages have thin content or if you want to consolidate SEO value on main pages instead. Use with caution as category pages can be valuable for site organization.</span>');
403 },
404 $page_slug . '_seo-controls',
405 $SECTION_SEO_CONTROLS
406 );
407
408 add_settings_field(
409 'add_nofollow_to_external_links',
410 'Add No-follow to External Links',
411 function() use ($option_key) {
412 $add_nofollow_to_external_links = Metasync::get_option('seo_controls')['add_nofollow_to_external_links'] ?? 'false';
413 printf(
414 '<input type="checkbox" id="add_nofollow_to_external_links" name="' . $option_key . '[seo_controls][add_nofollow_to_external_links]" value="true" %s />',
415 isset($add_nofollow_to_external_links) && $add_nofollow_to_external_links == 'true' ? 'checked' : ''
416 );
417 printf('<span class="description"><strong>No-follow External Links:</strong> When checked, automatically adds <code>rel="nofollow"</code> attribute to all external links appearing in posts, pages, and other post types when rendered by Otto. This tells search engines not to follow these links, which can help preserve your site\'s SEO value and prevent passing link juice to external sites.</span>');
418 },
419 $page_slug . '_seo-controls',
420 $SECTION_SEO_CONTROLS
421 );
422
423 // Advanced SEO Controls Section
424 add_settings_section(
425 $SECTION_SEO_CONTROLS_ADVANCED, // ID
426 '', // Title - will be handled by custom rendering
427 function(){
428 echo '</div>'; // Close main Indexation Control card
429 echo '<div class="dashboard-card" style="margin-top: 20px;">';
430 echo '<h2>Advanced Settings</h2>';
431 echo '<p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how ' . esc_html(Metasync_Settings_Registration::instance()->get_effective_menu_title()) . ' interacts with other SEO plugins.</p>';
432 },
433 $page_slug . '_seo-controls' // Page
434 );
435
436 add_settings_field(
437 'override_robots_tags',
438 'Override Other Plugins\' Robots Tags',
439 function() use ($option_key) {
440 $override_robots_tags = Metasync::get_option('seo_controls')['override_robots_tags'] ?? 'false';
441 printf(
442 '<input type="checkbox" id="override_robots_tags" name="' . $option_key . '[seo_controls][override_robots_tags]" value="true" %s />',
443 isset($override_robots_tags) && $override_robots_tags == 'true' ? 'checked' : ''
444 );
445 printf('<span class="description"><strong>Override Robots Tags:</strong> When checked, ' . Metasync_Settings_Registration::instance()->get_effective_menu_title() . ' will take precedence over robots meta tags from other SEO plugins (Yoast, Rank Math, All in One SEO, etc.). This removes their noindex tags when you want to allow indexing on archive pages. Only enable this if other plugins are conflicting with your indexation settings.</span>');
446 },
447 $page_slug . '_seo-controls',
448 $SECTION_SEO_CONTROLS_ADVANCED
449 );
450
451 // Google Instant Indexing Section
452 add_settings_section(
453 $SECTION_SEO_CONTROLS_INSTANT_INDEX, // ID
454 '', // Title - will be handled by custom rendering
455 function(){
456 echo '</div>'; // Close Advanced Settings card
457 echo '<div class="dashboard-card" style="margin-top: 20px;">';
458 echo '<h2>Google Instant Indexing</h2>';
459 echo '<p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure Google Indexing API for faster URL indexing. Enable this feature to access the Instant Indexing page.</p>';
460 },
461 $page_slug . '_seo-controls' // Page
462 );
463
464 add_settings_field(
465 'enable_googleinstantindex',
466 'Enable Google Instant Indexing',
467 function() use ($option_key) {
468 $enable_googleinstantindex = Metasync::get_option('seo_controls')['enable_googleinstantindex'] ?? 'false';
469 printf(
470 '<input type="checkbox" id="enable_googleinstantindex" name="' . $option_key . '[seo_controls][enable_googleinstantindex]" value="true" %s />',
471 isset($enable_googleinstantindex) && $enable_googleinstantindex == 'true' ? 'checked' : ''
472 );
473 printf('<span class="description"><strong>Enable Instant Indexing:</strong> When checked, enables the Google Instant Indexing feature which allows you to submit URLs directly to Google for faster indexing. A new "Instant Indexing" menu item will appear in the navigation.</span>');
474 },
475 $page_slug . '_seo-controls',
476 $SECTION_SEO_CONTROLS_INSTANT_INDEX
477 );
478
479 add_settings_field(
480 'google_index_api_config',
481 'Google Index API Configuration',
482 array(Metasync_Settings_Fields::instance(), 'render_google_index_section'),
483 $page_slug . '_seo-controls',
484 $SECTION_SEO_CONTROLS_INSTANT_INDEX
485 );
486
487 // Bing Instant Indexing Section
488 add_settings_section(
489 'seo_controls_bing_instant_index', // ID
490 '', // Title - will be handled by custom rendering
491 function(){
492 echo '</div>'; // Close Google Instant Indexing card
493 echo '<div class="dashboard-card" style="margin-top: 20px;">';
494 echo '<h2>Bing Instant Indexing (IndexNow)</h2>';
495 echo '<p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure IndexNow API for instant URL submission to Bing, Yandex, and other search engines that support the IndexNow protocol.</p>';
496 },
497 $page_slug . '_seo-controls' // Page
498 );
499
500 add_settings_field(
501 'enable_binginstantindex',
502 'Enable Bing Instant Indexing',
503 function() use ($option_key) {
504 // Auto-enable if API key is configured
505 $bing_settings = get_option('metasync_options_bing_instant_indexing', []);
506 $has_api_key = !empty($bing_settings['api_key']);
507
508 $enable_binginstantindex = Metasync::get_option('seo_controls')['enable_binginstantindex'] ?? 'false';
509
510 // Auto-check if API key exists
511 if ($has_api_key && $enable_binginstantindex !== 'true') {
512 $enable_binginstantindex = 'true';
513 }
514
515 printf(
516 '<input type="checkbox" id="enable_binginstantindex" name="' . $option_key . '[seo_controls][enable_binginstantindex]" value="true" %s %s />',
517 isset($enable_binginstantindex) && $enable_binginstantindex == 'true' ? 'checked' : '',
518 $has_api_key ? 'data-auto-enabled="true"' : ''
519 );
520 printf('<span class="description"><strong>Enable Bing Instant Indexing:</strong> Automatically enabled when you configure an API key below. When enabled, this activates the IndexNow protocol to instantly notify Bing, Yandex, and other search engines about URL changes.</span>');
521 },
522 $page_slug . '_seo-controls',
523 'seo_controls_bing_instant_index'
524 );
525
526 // Bing Index API configuration
527 add_settings_field(
528 'bing_index_api_config',
529 'IndexNow API Configuration',
530 array(Metasync_Settings_Fields::instance(), 'render_bing_index_section'),
531 $page_slug . '_seo-controls',
532 'seo_controls_bing_instant_index'
533 );
534
535 add_settings_field(
536 'searchatlas_api_key',
537 $this->get_effective_menu_title() . ' API Key',
538 array(Metasync_Settings_Fields::instance(), 'searchatlas_api_key_callback'),
539 $page_slug . '_general',
540 $SECTION_METASYNC
541 );
542
543 add_settings_field(
544 'apikey', // ID
545 'Plugin Auth Token', // Title
546 array(Metasync_Settings_Fields::instance(), 'metasync_settings_genkey_callback'), // Callback
547 $page_slug . '_general', // Page
548 $SECTION_METASYNC // Section
549 );
550
551 /**
552 * SERVER SIDE RENDERING SETTING OPTIONS
553 * @see SSR functionality
554 * Note: OTTO SSR is always enabled by default when connected
555 */
556
557 # field to accept the otto pixel
558 add_settings_field(
559 'otto_pixel_uuid',
560 $whitelabel_otto_name . ' Pixel UUID',
561 function() use ($option_key) {
562 $value = Metasync::get_option('general')['otto_pixel_uuid'] ?? '';
563 printf('<input type="text" size="40" value = "'.esc_attr($value).'" name="' . $option_key . '[general][otto_pixel_uuid]"/>');
564 },
565 $page_slug . '_general',
566 $SECTION_METASYNC
567 );
568
569
570 # check box to toggle on and off disabling OTTO for logged in users
571 add_settings_field(
572 'otto_disable_on_loggedin',
573 'Disable ' . $whitelabel_otto_name . ' for Logged in Users',
574 function() use ($whitelabel_otto_name, $option_key) {
575 $otto_disable_on_loggedin = Metasync::get_option('general')['otto_disable_on_loggedin'] ?? '';
576 printf(
577 '<input type="checkbox" id="otto_disable_on_loggedin" name="' . $option_key . '[general][otto_disable_on_loggedin]" value="true" %s />',
578 isset($otto_disable_on_loggedin) && $otto_disable_on_loggedin == 'true' ? 'checked' : ''
579 );
580 printf('<span class="description"> This disables '.$whitelabel_otto_name.' when logged in to allow editing original page contents</span>');
581 },
582 $page_slug . '_general',
583 $SECTION_METASYNC
584 );
585
586 # check box to disable OTTO frontend toolbar
587 add_settings_field(
588 'otto_disable_preview_button',
589 'Disable ' . $whitelabel_otto_name . ' Frontend Toolbar',
590 function() use ($whitelabel_otto_name, $option_key) {
591 $otto_disable_toolbar = Metasync::get_option('general')['otto_disable_preview_button'] ?? false;
592 printf(
593 '<input type="checkbox" id="otto_disable_preview_button" name="' . $option_key . '[general][otto_disable_preview_button]" value="true" %s />',
594 filter_var($otto_disable_toolbar, FILTER_VALIDATE_BOOLEAN) ? 'checked' : ''
595 );
596 printf('<span class="description"> Hide the entire frontend toolbar (status indicator, preview button, and debug button) on the frontend. %s functionality will still work, but the toolbar controls will be hidden.</span>', esc_html($whitelabel_otto_name));
597 },
598 $page_slug . '_general',
599 $SECTION_METASYNC
600 );
601
602 # WP Rocket compatibility mode setting
603 add_settings_field(
604 'otto_wp_rocket_compat',
605 'WP Rocket Compatibility',
606 function() use ($whitelabel_otto_name, $option_key) {
607 $value = Metasync::get_option('general')['otto_wp_rocket_compat'] ?? 'auto';
608 $wp_rocket_active = class_exists('WP_Rocket');
609
610 echo '<select id="otto_wp_rocket_compat" name="' . $option_key . '[general][otto_wp_rocket_compat]">';
611 echo '<option value="auto"' . selected($value, 'auto', false) . '>Auto (Recommended)</option>';
612 echo '<option value="buffer"' . selected($value, 'buffer', false) . '>Buffer Mode (Faster)</option>';
613 echo '<option value="http"' . selected($value, 'http', false) . '>HTTP Mode (Safer)</option>';
614 echo '<option value="disable_otto"' . selected($value, 'disable_otto', false) . '>Disable ' . esc_html($whitelabel_otto_name) . ' when WP Rocket is active</option>';
615 echo '</select>';
616
617 if ($wp_rocket_active) {
618 echo '<p class="description" style="color: #0073aa; margin-top: 8px;">✓ WP Rocket detected - compatibility mode active</p>';
619 }
620
621 echo '<p class="description" style="margin-top: 8px;">';
622 echo '<strong>Auto (Recommended):</strong> Allows both ' . esc_html($whitelabel_otto_name) . ' and WP Rocket to work together. Does not set DONOTCACHEPAGE unless required (Brizy pages, SG Optimizer conflicts).<br>';
623 echo '<strong>Buffer Mode:</strong> Forces output buffering method. Fastest but may conflict with some configurations.<br>';
624 echo '<strong>HTTP Mode:</strong> Forces internal HTTP fetch method. Slower but more compatible.<br>';
625 echo '<strong>Disable ' . esc_html($whitelabel_otto_name) . ':</strong> Completely disables ' . esc_html($whitelabel_otto_name) . ' when WP Rocket is detected. Use if issues persist.';
626 echo '</p>';
627 },
628 $page_slug . '_general',
629 $SECTION_METASYNC
630 );
631
632 /**
633 * BOT DETECTION SETTINGS FOR OTTO
634 * @since 1.0.0
635 */
636
637 # Checkbox to disable OTTO for bot traffic
638 add_settings_field(
639 'otto_disable_for_bots',
640 'Disable ' . $whitelabel_otto_name . ' for Bot Traffic',
641 function() use ($whitelabel_otto_name, $option_key) {
642 $otto_disable_for_bots = Metasync::get_option('general')['otto_disable_for_bots'] ?? false;
643 printf(
644 '<input type="checkbox" id="otto_disable_for_bots" name="' . $option_key . '[general][otto_disable_for_bots]" value="true" %s />',
645 filter_var($otto_disable_for_bots, FILTER_VALIDATE_BOOLEAN) ? 'checked' : ''
646 );
647 printf('<span class="description"> Skip %s processing for detected bots (search engines, crawlers, SEO tools) to reduce unnecessary API calls. View bot statistics in the Bot Statistics page under the SEO dropdown.</span>', esc_html($whitelabel_otto_name));
648 },
649 $page_slug . '_general',
650 $SECTION_METASYNC
651 );
652
653 # Textarea for bot whitelist
654 add_settings_field(
655 'otto_bot_whitelist',
656 'Bot Whitelist',
657 function() use ($whitelabel_otto_name, $option_key) {
658 $otto_bot_whitelist = Metasync::get_option('general')['otto_bot_whitelist'] ?? '';
659 printf(
660 '<textarea id="otto_bot_whitelist" name="' . $option_key . '[general][otto_bot_whitelist]" rows="5" cols="50" style="width: 100%%; max-width: 500px; font-family: monospace;">%s</textarea>',
661 esc_textarea($otto_bot_whitelist)
662 );
663 echo '<p class="description">';
664 echo 'Enter bot names or user-agent patterns (one per line) that should <strong>always</strong> be processed by ' . esc_html($whitelabel_otto_name) . ', even when "Disable for Bots" is enabled.<br>';
665 echo '<strong>Example:</strong><br>';
666 echo 'Googlebot<br>';
667 echo 'Bingbot';
668 echo '</p>';
669 },
670 $page_slug . '_general',
671 $SECTION_METASYNC
672 );
673
674 # Textarea for bot blacklist
675 add_settings_field(
676 'otto_bot_blacklist',
677 'Bot Blacklist',
678 function() use ($whitelabel_otto_name, $option_key) {
679 $otto_bot_blacklist = Metasync::get_option('general')['otto_bot_blacklist'] ?? '';
680 printf(
681 '<textarea id="otto_bot_blacklist" name="' . $option_key . '[general][otto_bot_blacklist]" rows="5" cols="50" style="width: 100%%; max-width: 500px; font-family: monospace;">%s</textarea>',
682 esc_textarea($otto_bot_blacklist)
683 );
684 echo '<p class="description">';
685 echo 'Enter bot names or user-agent patterns (one per line) that should <strong>always</strong> be blocked from ' . esc_html($whitelabel_otto_name) . ' processing, regardless of other settings.<br>';
686 echo '<strong>Example:</strong><br>';
687 echo 'BadBot<br>';
688 echo 'MaliciousCrawler';
689 echo '</p>';
690 },
691 $page_slug . '_general',
692 $SECTION_METASYNC
693 );
694
695
696 # Link button to Bot Statistics page
697 add_settings_field(
698 'otto_bot_statistics_link',
699 'Bot Detection Statistics',
700 function() use ($page_slug) {
701 printf(
702 '<a href="%s" class="button button-secondary"><span class="dashicons dashicons-chart-bar" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> View Bot Statistics</a>',
703 esc_url(admin_url('admin.php?page=' . $page_slug . '-bot-statistics'))
704 );
705 printf('<p class="description">View detailed bot detection statistics, breakdown by bot type, and unique bot entries with hit counts.</p>');
706 },
707 $page_slug . '_general',
708 $SECTION_METASYNC
709 );
710
711 # END BOT DETECTION SETTINGS
712
713 add_settings_field(
714 'periodic_clear_ottopage_cache',
715 'Clear Page Cache',
716 function() use ($option_key) {
717 $periodic_clear_ottopage_cache = Metasync::get_option('general')['periodic_clear_ottopage_cache'] ?? 'default';
718 printf('<select style = "width : 250px" name="' . $option_key . '[general][periodic_clear_ottopage_cache]" id="heading_style">');
719 printf('<option value="24" '. selected($periodic_clear_ottopage_cache, '24', false) . '>Clear Daily</option>');
720 printf('<option value="36" '. selected($periodic_clear_ottopage_cache, '36', false) . '>Clear Every 2 days</option>');
721 printf('<option value="40" '. selected($periodic_clear_ottopage_cache, '40', false) . '>Clear Weekly</option>');
722 printf('<option value="0"'.selected($periodic_clear_ottopage_cache, '0', false).'>Clear Monthly (Default)</option>');
723 printf('</select>');
724
725 # get last cleared timestamp
726 $timestamp = get_option('metasync_refresh_all_caches')['pages'] ?? false;
727
728 if($timestamp > 0){
729 printf(
730 '<p class="descriptionValue">last Cleared : '.date('y-m-d H:i:s', $timestamp).'</p>'
731 );
732 }
733
734 },
735 $page_slug . '_otto_cache',
736 $SECTION_METASYNC
737 );
738
739 add_settings_field(
740 'periodic_clear_ottopost_cache',
741 'Clear Post Cache',
742 function() use ($option_key) {
743 $periodic_clear_ottopost_cache = Metasync::get_option('general')['periodic_clear_ottopost_cache'] ?? 'default';
744 printf('<select style = "width : 250px" name="' . $option_key . '[general][periodic_clear_ottopost_cache]" id="heading_style">');
745 printf('<option value="24" '. selected($periodic_clear_ottopost_cache, '24', false) . '>Clear Daily</option>');
746 printf('<option value="36" '. selected($periodic_clear_ottopost_cache, '36', false) . '>Clear Every 2 days</option>');
747 printf('<option value="40" '. selected($periodic_clear_ottopost_cache, '40', false) . '>Clear Weekly</option>');
748 printf('<option value="0"'.selected($periodic_clear_ottopost_cache, '0', false).'>Clear Monthly (Default)</option>');
749 printf('</select>');
750
751 # get last cleared timestamp
752 $timestamp = get_option('metasync_refresh_all_caches')['posts'] ?? false;
753
754 if($timestamp > 0){
755 printf(
756 '<p class="descriptionValue">last Cleared : '.date('y-m-d H:i:s', $timestamp).'</p>'
757 );
758 }
759 },
760 $page_slug . '_otto_cache',
761 $SECTION_METASYNC
762 );
763
764 add_settings_field(
765 'periodic_clear_otto_cache',
766 'Clear all cache',
767 function() use ($option_key) {
768 $periodic_clear_otto_cache = Metasync::get_option('general')['periodic_clear_otto_cache'] ?? 'default';
769 printf('<select style = "width : 250px" name="' . $option_key . '[general][periodic_clear_otto_cache]" id="heading_style">');
770 printf('<option value="24" '. selected($periodic_clear_otto_cache, '24', false) . '>Clear Daily</option>');
771 printf('<option value="36" '. selected($periodic_clear_otto_cache, '36', false) . '>Clear Every 2 days</option>');
772 printf('<option value="40" '. selected($periodic_clear_otto_cache, '40', false) . '>Clear Weekly</option>');
773 printf('<option value="0"'.selected($periodic_clear_otto_cache, '0', false).'>Clear Monthly (Default)</option>');
774 printf('</select>');
775
776 # get last cleared timestamp
777 $timestamp = get_option('metasync_refresh_all_caches')['general'] ?? false;
778
779 if($timestamp > 0){
780 printf(
781 '<p class="descriptionValue">last Cleared : '.date('y-m-d H:i:s', $timestamp).'</p>'
782 );
783 }
784
785 },
786 $page_slug . '_otto_cache',
787 $SECTION_METASYNC
788 );
789
790 # END SERVER SIDE RENDERING SETTINGS
791
792 // Meta Box Visibility Controls
793 add_settings_field(
794 'disable_common_robots_metabox',
795 'Disable Common Robots Meta Box',
796 function() use ($option_key) {
797 $disabled = Metasync::get_option('general')['disable_common_robots_metabox'] ?? false;
798 printf(
799 '<input type="checkbox" id="disable_common_robots_metabox" name="' . $option_key . '[general][disable_common_robots_metabox]" value="1" %s />',
800 $disabled ? 'checked' : ''
801 );
802 printf('<span class="description"> Hide the Common Robots Meta box on post/page edit screens</span>');
803 },
804 $page_slug . '_general',
805 $SECTION_METASYNC
806 );
807
808 add_settings_field(
809 'disable_advance_robots_metabox',
810 'Disable Advance Robots Meta Box',
811 function() use ($option_key) {
812 $disabled = Metasync::get_option('general')['disable_advance_robots_metabox'] ?? false;
813 printf(
814 '<input type="checkbox" id="disable_advance_robots_metabox" name="' . $option_key . '[general][disable_advance_robots_metabox]" value="1" %s />',
815 $disabled ? 'checked' : ''
816 );
817 printf('<span class="description"> Hide the Advance Robots Meta box on post/page edit screens</span>');
818 },
819 $page_slug . '_general',
820 $SECTION_METASYNC
821 );
822
823 add_settings_field(
824 'disable_redirection_metabox',
825 'Disable Redirection Meta Box',
826 function() use ($option_key) {
827 $disabled = Metasync::get_option('general')['disable_redirection_metabox'] ?? false;
828 printf(
829 '<input type="checkbox" id="disable_redirection_metabox" name="' . $option_key . '[general][disable_redirection_metabox]" value="1" %s />',
830 $disabled ? 'checked' : ''
831 );
832 printf('<span class="description"> Hide the Redirection meta box on post/page edit screens</span>');
833 },
834 $page_slug . '_general',
835 $SECTION_METASYNC
836 );
837
838 add_settings_field(
839 'disable_canonical_metabox',
840 'Disable Canonical Meta Box',
841 function() use ($option_key) {
842 $disabled = Metasync::get_option('general')['disable_canonical_metabox'] ?? false;
843 printf(
844 '<input type="checkbox" id="disable_canonical_metabox" name="' . $option_key . '[general][disable_canonical_metabox]" value="1" %s />',
845 $disabled ? 'checked' : ''
846 );
847 printf('<span class="description"> Hide the Canonical meta box on post/page edit screens</span>');
848 },
849 $page_slug . '_general',
850 $SECTION_METASYNC
851 );
852
853 add_settings_field(
854 'disable_social_opengraph_metabox',
855 'Disable Social Media & Open Graph Meta Box',
856 function() use ($option_key) {
857 $disabled = Metasync::get_option('general')['disable_social_opengraph_metabox'] ?? false;
858 printf(
859 '<input type="checkbox" id="disable_social_opengraph_metabox" name="' . $option_key . '[general][disable_social_opengraph_metabox]" value="1" %s />',
860 $disabled ? 'checked' : ''
861 );
862 printf('<span class="description"> Hide the Social Media & Open Graph meta box on post/page edit screens</span>');
863 },
864 $page_slug . '_general',
865 $SECTION_METASYNC
866 );
867
868 add_settings_field(
869 'disable_schema_markup_metabox',
870 'Disable Schema Markup Meta Box',
871 function() use ($option_key) {
872 $disabled = Metasync::get_option('general')['disable_schema_markup_metabox'] ?? false;
873 printf(
874 '<input type="checkbox" id="disable_schema_markup_metabox" name="' . $option_key . '[general][disable_schema_markup_metabox]" value="1" %s />',
875 $disabled ? 'checked' : ''
876 );
877 printf('<span class="description"> Hide the Schema Markup meta box on post/page edit screens</span>');
878 },
879 $page_slug . '_general',
880 $SECTION_METASYNC
881 );
882
883 add_settings_field(
884 'disable_seo_metabox',
885 'Disable SEO Meta Box',
886 function() use ($option_key) {
887 $disabled = Metasync::get_option('general')['disable_seo_metabox'] ?? false;
888 printf(
889 '<input type="checkbox" id="disable_seo_metabox" name="' . $option_key . '[general][disable_seo_metabox]" value="1" %s />',
890 $disabled ? 'checked' : ''
891 );
892 printf('<span class="description"> Hide the SEO Title & Meta Description meta box on post/page edit screens</span>');
893 },
894 $page_slug . '_general',
895 $SECTION_METASYNC
896 );
897
898 add_settings_field(
899 'open_external_links',
900 'Open External Links in New Tab/Window',
901 function() use ($option_key) {
902 $enabled = Metasync::get_option('general')['open_external_links'] ?? false;
903 printf(
904 '<input type="checkbox" id="open_external_links" name="' . $option_key . '[general][open_external_links]" value="1" %s />',
905 $enabled ? 'checked' : ''
906 );
907 printf('<span class="description"> Automatically add <code>target="_blank"</code> attribute to external links appearing in your posts, pages, and other post types when rendered by Otto. The attribute is applied when the url is displayed.</span>');
908 },
909 $page_slug . '_general',
910 $SECTION_METASYNC
911 );
912
913 add_settings_field(
914 'permalink_structure',
915 'The Permalink setting of website',
916 function() {
917
918 $current_permalink_structure = get_option('permalink_structure');
919 $current_rewrite_rules = get_option('rewrite_rules');
920 // Check if the current permalink structure is set to "Plain"
921 if (($current_permalink_structure == '/%post_id%/' || $current_permalink_structure == '') && $current_rewrite_rules == '') {
922 printf('<span class="description" style="color:#ff0000;opacity:1;">To ensure compatibility, Please Update your Permalink structure to any option other than "plain. For any Inquiries contact support <a href="' . get_admin_url() . 'options-permalink.php">Check Setting</a> </span>');
923 } else {
924 printf('<span class="description" style="color:#008000;opacity:1;">Permalink is Okay </span>');
925 }
926 },
927 $page_slug . '_general',
928 $SECTION_METASYNC
929 );
930
931 # Add Hide Dashboard Framework setting
932 add_settings_field(
933 'hide_dashboard_framework',
934 'Hide Dashboard',
935 function() use ($option_key) {
936 $hide_dashboard = Metasync::get_option('general')['hide_dashboard_framework'] ?? '';
937 printf(
938 '<input type="checkbox" id="hide_dashboard_framework" name="' . $option_key . '[general][hide_dashboard_framework]" value="true" %s />',
939 isset($hide_dashboard) && $hide_dashboard == 'true' ? 'checked' : ''
940 );
941 printf('<span class="description"> Hide the %s dashboard</span>', esc_html(Metasync_Settings_Registration::instance()->get_effective_menu_title()));
942 },
943 $page_slug . '_general',
944 $SECTION_METASYNC
945 );
946
947 // REMOVED (CVE-2025-14386): disable_single_signup_login setting was removed
948
949 # Adding the "Show Admin Bar Status" setting
950 add_settings_field(
951 'show_admin_bar_status',
952 'Show ' . $this->get_effective_menu_title() . ' Status in Admin Bar',
953 function() use ($option_key) {
954 $show_admin_bar = Metasync::get_option('general')['show_admin_bar_status'] ?? true;
955 printf(
956 '<input type="checkbox" id="show_admin_bar_status" name="' . $option_key . '[general][show_admin_bar_status]" value="true" %s />',
957 $show_admin_bar ? 'checked' : ''
958 );
959 printf('<span class="description">Show the %s status indicator in the WordPress admin bar.</span>', esc_html(Metasync::get_effective_plugin_name()));
960 },
961 $page_slug . '_general',
962 $SECTION_METASYNC
963 );
964
965 # Adding the "Enable Auto-updates" setting
966 add_settings_field(
967 'enable_auto_updates',
968 'Enable Automatic Updates',
969 function() use ($option_key) {
970 $enable_auto_updates = Metasync::get_option('general')['enable_auto_updates'] ?? false;
971 printf(
972 '<input type="checkbox" id="enable_auto_updates" name="' . $option_key . '[general][enable_auto_updates]" value="true" %s />',
973 $enable_auto_updates ? 'checked' : ''
974 );
975 printf('<span class="description">Allow WordPress to automatically update this plugin when new versions are available.</span>');
976 },
977 $page_slug . '_general',
978 $SECTION_METASYNC
979 );
980
981 # Add User Role Sync Setting
982 add_settings_field(
983 'content_genius_sync_roles',
984 'Content Genius User Roles to Sync',
985 function() use ($option_key) {
986 $selected_roles = Metasync::get_option('general')['content_genius_sync_roles'] ?? array();
987
988 # If it's a string (single role from old version), convert to array
989 if (!is_array($selected_roles)) {
990 $selected_roles = array($selected_roles);
991 }
992
993 # Get all WordPress user roles safely
994 if (!function_exists('wp_roles')) {
995 require_once(ABSPATH . 'wp-admin/includes/user.php');
996 }
997 global $wp_roles;
998
999 # Safety check for $wp_roles
1000 if (!isset($wp_roles) || !is_object($wp_roles)) {
1001 $wp_roles = new WP_Roles();
1002 }
1003
1004 $all_roles = $wp_roles->roles;
1005
1006 # Start container
1007 ?>
1008 <div class="metasync-role-selector-container">
1009
1010 <!-- All Roles Option -->
1011 <label class="metasync-role-option-all">
1012 <input type="checkbox"
1013 name="<?php echo esc_attr($option_key); ?>[general][content_genius_sync_roles][]"
1014 value="all"
1015 <?php checked(empty($selected_roles) || in_array('all', $selected_roles), true); ?> />
1016 <strong>All Roles</strong>
1017 </label>
1018
1019 <!-- Divider -->
1020 <hr class="metasync-role-divider">
1021
1022 <?php
1023 # Display each role as a checkbox
1024 if (!empty($all_roles) && is_array($all_roles)) {
1025 foreach ($all_roles as $role_key => $role_details) {
1026 # Safety check for role data
1027 if (!isset($role_details['name'])) {
1028 continue;
1029 }
1030
1031 $is_checked = in_array($role_key, $selected_roles);
1032 $role_name = translate_user_role($role_details['name']);
1033 ?>
1034 <label class="metasync-role-option">
1035 <input type="checkbox"
1036 name="<?php echo esc_attr($option_key); ?>[general][content_genius_sync_roles][]"
1037 value="<?php echo esc_attr($role_key); ?>"
1038 <?php checked($is_checked, true); ?> />
1039 <span class="metasync-role-label"><?php echo esc_html($role_name); ?></span>
1040 </label>
1041 <?php
1042 }
1043 }
1044 ?>
1045
1046 </div>
1047
1048 <!-- Description -->
1049 <p class="description" style="margin-top: 10px;">
1050 <strong>How it works:</strong>
1051 Select which user roles should be synced with Content Genius.
1052 If <strong>"All Roles"</strong> is selected or none are selected, all users will be synced.
1053 Otherwise, only users with the selected roles will be included in the sync.
1054 </p>
1055 <?php
1056 },
1057 $page_slug . '_general',
1058 $SECTION_METASYNC
1059 );
1060
1061 # Default Page Builder setting
1062 add_settings_field(
1063 'default_page_builder',
1064 'Default Page Builder',
1065 function() use ($option_key) {
1066 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
1067 $current = Metasync::get_option('general')['default_page_builder'] ?? '';
1068 $builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
1069 $auto_detected = Metasync_HTML_To_Builder_Converter::auto_detect_builder();
1070
1071 echo '<div class="metasync-page-builder-setting">';
1072 echo '<style>#default_page_builder:focus { color: #fff; }</style>';
1073 echo '<select id="default_page_builder" name="' . esc_attr($option_key) . '[general][default_page_builder]" style="min-width: 300px;">';
1074
1075 foreach ($builders as $key => $builder) {
1076 $is_detected = $builder['detected'];
1077 $label = esc_html($builder['label']);
1078 if ($is_detected && $key !== 'gutenberg') {
1079 $label .= ' — Detected';
1080 } elseif (!$is_detected) {
1081 $label .= ' — Not Detected';
1082 }
1083 if ($key === 'gutenberg' && empty($current)) {
1084 $label .= ' — Default';
1085 }
1086 printf(
1087 '<option value="%s" %s%s>%s</option>',
1088 esc_attr($key),
1089 selected(empty($current) ? 'gutenberg' : $current, $key, false),
1090 $is_detected ? '' : ' disabled',
1091 $label
1092 );
1093 }
1094
1095 echo '</select>';
1096 echo '<p class="description" style="margin-top: 6px;">Only theme builders detected on your site can be selected. Undetected builders are shown as disabled.</p>';
1097
1098 # Show current auto-detection result
1099 if (!empty($builders[$auto_detected]) && $auto_detected !== 'gutenberg') {
1100 printf(
1101 '<p class="description" style="margin-top: 8px; color: #0073aa;">🔍 Auto-detected: <strong>%s</strong> is active on this site.</p>',
1102 esc_html($builders[$auto_detected]['label'])
1103 );
1104 }
1105
1106 # Show description for the selected builder
1107 echo '<div id="metasync-builder-descriptions" style="margin-top: 10px;">';
1108 foreach ($builders as $key => $builder) {
1109 $is_visible = (empty($current) ? 'gutenberg' : $current) === $key;
1110 printf(
1111 '<p class="description metasync-builder-desc" data-builder="%s" style="%s">%s</p>',
1112 esc_attr($key),
1113 $is_visible ? '' : 'display:none;',
1114 esc_html($builder['description'])
1115 );
1116 }
1117 echo '</div>';
1118
1119 # Inline JS to toggle description on select change
1120 ?>
1121 <script>
1122 document.getElementById('default_page_builder')?.addEventListener('change', function() {
1123 document.querySelectorAll('.metasync-builder-desc').forEach(function(el) {
1124 el.style.display = el.dataset.builder === this.value ? '' : 'none';
1125 }.bind(this));
1126 });
1127 </script>
1128 <?php
1129 },
1130 $page_slug . '_general',
1131 $SECTION_METASYNC
1132 );
1133
1134 add_settings_field(
1135 'import_external_data',
1136 'Import settings and data from SEO Plugins',
1137 function() {
1138 printf(
1139 '<a href="%s" class="button button-secondary"><span class="dashicons dashicons-download" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Import from SEO Plugins</a>',
1140 esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-import-external'))
1141 );
1142 printf('<p class="description">Import settings and data from other SEO plugins (Yoast, Rank Math, AIOSEO, etc).</p>');
1143 },
1144 $page_slug . '_general',
1145 $SECTION_METASYNC
1146 );
1147
1148
1149
1150 /*
1151 add field to save color for elementor and assign custom color to the heading
1152 */
1153 if(is_admin()){
1154 add_settings_field(
1155 'white_label_plugin_name',
1156 'Plugin Name',
1157 function() use ($option_key) {
1158 $value = Metasync::get_option('general')['white_label_plugin_name'] ?? '';
1159 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_name]" value="' . esc_attr($value) . '" maxlength="16" />');
1160 printf('<p class="description">This name will be used for general plugin branding (WordPress menus, page titles, and system messages). Maximum 16 characters.</p>');
1161 },
1162 $page_slug . '_branding',
1163 $SECTION_METASYNC
1164 );
1165
1166 add_settings_field(
1167 'whitelabel_otto_name',
1168 'OTTO Name',
1169 function() use ($option_key) {
1170 $value = Metasync::get_option('general')['whitelabel_otto_name'] ?? '';
1171 printf('<input type="text" name="' . $option_key . '[general][whitelabel_otto_name]" value="' . esc_attr($value) . '" />');
1172 $example_name = !empty($value) ? $value : 'OTTO';
1173 printf('<p class="description">This name will be used for OTTO feature references (e.g., "Enable %s Server Side Rendering").</p>', esc_html($example_name));
1174 },
1175 $page_slug . '_branding',
1176 $SECTION_METASYNC
1177 );
1178
1179 add_settings_field(
1180 'whitelabel_logo_light_url',
1181 'Logo (Light Theme)',
1182 function() use ($option_key) {
1183 $whitelabel_settings = Metasync::get_whitelabel_settings();
1184 $value = $whitelabel_settings['logo_light'] ?? '';
1185 printf('<input type="url" name="' . $option_key . '[whitelabel][logo_light]" value="' . esc_attr($value) . '" size="60" />');
1186 printf('<p class="description">Displayed when the admin UI is in light mode. Leave blank to use the default %s logo.</p>', esc_html(Metasync::get_effective_plugin_name()));
1187 },
1188 $page_slug . '_branding',
1189 $SECTION_METASYNC
1190 );
1191
1192 add_settings_field(
1193 'whitelabel_logo_dark_url',
1194 'Logo (Dark Theme)',
1195 function() use ($option_key) {
1196 $whitelabel_settings = Metasync::get_whitelabel_settings();
1197 $value = $whitelabel_settings['logo_dark'] ?? '';
1198 printf('<input type="url" name="' . $option_key . '[whitelabel][logo_dark]" value="' . esc_attr($value) . '" size="60" />');
1199 printf('<p class="description">Displayed when the admin UI is in dark mode. Leave blank to use the default %s logo.</p>', esc_html(Metasync::get_effective_plugin_name()));
1200 },
1201 $page_slug . '_branding',
1202 $SECTION_METASYNC
1203 );
1204
1205 add_settings_field(
1206 'whitelabel_domain_url',
1207 'Dashboard URL',
1208 function() use ($option_key) {
1209 $whitelabel_settings = Metasync::get_whitelabel_settings();
1210 $value = $whitelabel_settings['domain'] ?? '';
1211 printf('<input type="url" name="' . $option_key . '[whitelabel][domain]" value="' . esc_attr($value) . '" size="60" />');
1212 printf('<p class="description">Enter your whitelabel dashboard URL (e.g., https://yourdashboard.com). Used for branding purposes.</p>');
1213 },
1214 $page_slug . '_branding',
1215 $SECTION_METASYNC
1216 );
1217 add_settings_field(
1218 'white_label_plugin_description',
1219 'Plugin Description',
1220 function() use ($option_key) {
1221 $value = Metasync::get_option('general')['white_label_plugin_description'] ?? '';
1222 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_description]" value="' . esc_attr($value) . '" />');
1223 },
1224 $page_slug . '_branding',
1225 $SECTION_METASYNC
1226 );
1227
1228 add_settings_field(
1229 'white_label_plugin_author',
1230 'Author',
1231 function() use ($option_key) {
1232 $value = Metasync::get_option('general')['white_label_plugin_author'] ?? '';
1233 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_author]" value="' . esc_attr($value) . '" />');
1234 },
1235 $page_slug . '_branding',
1236 $SECTION_METASYNC
1237 );
1238
1239 add_settings_field(
1240 'white_label_plugin_author_uri',
1241 'Author URI',
1242 function() use ($option_key) {
1243 $value = Metasync::get_option('general')['white_label_plugin_author_uri'] ?? '';
1244 # printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_author_uri]" value="' . esc_attr($value) . '" />');
1245 # Fixed printf usage
1246 printf('<input type="text" name="%s" value="%s" />', esc_attr($option_key . '[general][white_label_plugin_author_uri]'), esc_attr($value) );
1247
1248 },
1249 $page_slug . '_branding',
1250 $SECTION_METASYNC
1251 );
1252 add_settings_field(
1253 'white_label_plugin_uri',
1254 'Plugin URI',
1255 function() use ($option_key) {
1256 $value = Metasync::get_option('general')['white_label_plugin_uri'] ?? ''; // New option for Plugin URI
1257 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_uri]" value="' . esc_attr($value) . '" />');
1258 },
1259 $page_slug . '_branding',
1260 $SECTION_METASYNC
1261 );
1262 register_setting(Metasync_Admin::option_group,
1263 Metasync_Admin::option_key,
1264 array(self::instance(), 'sanitize')
1265 );
1266
1267 // DEPRECATED: Menu Name and Menu Title fields removed
1268 add_settings_field(
1269 'white_label_plugin_menu_slug',
1270 'Menu Slug',
1271 function() use ($option_key) {
1272 $value = Metasync::get_option('general')['white_label_plugin_menu_slug'] ?? '';
1273 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_menu_slug]" value="' . esc_attr($value) . '" />');
1274 },
1275 $page_slug . '_branding',
1276 $SECTION_METASYNC
1277 );
1278 add_settings_field(
1279 'white_label_plugin_menu_icon',
1280 'Menu Icon',
1281 function() use ($option_key) {
1282 $value = Metasync::get_option('general')['white_label_plugin_menu_icon'] ?? '';
1283 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_menu_icon]" value="' . esc_attr($value) . '" />');
1284 },
1285 $page_slug . '_branding',
1286 $SECTION_METASYNC
1287 );
1288 }
1289 // HIDDEN: Choose Style Option setting
1290 /*
1291 add_settings_field(
1292 'enabled_plugin_css',
1293 'Choose Style Option',
1294 function() use ($option_key) {
1295 $enabled_plugin_css = Metasync::get_option('general')['enabled_plugin_css'] ?? '';
1296
1297 // Output radio button for Default Style.css active
1298
1299 printf(
1300 '<input type="radio" id="enable_default" name="' . $option_key . '[general][enabled_plugin_css]" value="default" %s />',
1301 ($enabled_plugin_css == 'default'||$enabled_plugin_css =='') ? 'checked' : ''
1302 );
1303 printf('<label for="enable_default">Default</label><br>');
1304
1305
1306 // Output radio button for Metasync Style
1307 printf(
1308 '<input type="radio" id="enable_metasync" name="' . $option_key . '[general][enabled_plugin_css]" value="metasync" %s />',
1309 ($enabled_plugin_css == 'metasync') ? 'checked' : ''
1310 );
1311 printf('<label for="enable_metasync">Metasync Style</label>');
1312
1313 printf('<p class="description"> Choose the default page Style Sheet: Default or MetaSync.</p>');
1314 },
1315 $page_slug . '_branding',
1316 $SECTION_METASYNC
1317 );
1318 */
1319
1320 add_settings_field(
1321 'enabled_elementor_plugin_css',
1322 'Choose Elementor Font Style',
1323 function() use ($option_key) {
1324 $enabled_elementor_plugin_css = Metasync::get_option('general')['enabled_elementor_plugin_css'] ?? 'default';
1325 printf('<select name="' . $option_key . '[general][enabled_elementor_plugin_css]" id="heading_style">');
1326 printf('<option value="default"'.selected($enabled_elementor_plugin_css, 'default', false).'>Default</option>');
1327 printf('<option value="custom" '. selected($enabled_elementor_plugin_css, 'custom', false) . '>Custom</option>');
1328 printf('</select>');
1329 },
1330 $page_slug . '_branding',
1331 $SECTION_METASYNC
1332 );
1333 add_settings_field(
1334 'enabled_elementor_plugin_css_color',
1335 'Choose Elementor Font Color',
1336 function() use ($option_key) {
1337 $enabled_elementor_plugin_css_color = Metasync::get_option('general')['enabled_elementor_plugin_css_color'] ?? '#000000';
1338 printf('<input type="color" id="elementor_default_color_metasync" name="' . $option_key . '[general][enabled_elementor_plugin_css_color]" value="'.$enabled_elementor_plugin_css_color.'">');
1339 },
1340 $page_slug . '_branding',
1341 $SECTION_METASYNC
1342 );
1343
1344 add_settings_field(
1345 'whitelabel_settings_password',
1346 'Settings Password',
1347 function() use ($option_key) {
1348 $whitelabel_settings = Metasync::get_whitelabel_settings();
1349 $value = $whitelabel_settings['settings_password'] ?? '';
1350 printf('<input type="password" name="' . $option_key . '[whitelabel][settings_password]" value="' . esc_attr($value) . '" size="30" autocomplete="new-password" />');
1351 printf('<p class="description">Set a custom password to protect the branding settings section.</p>');
1352 },
1353 $page_slug . '_branding',
1354 $SECTION_METASYNC
1355 );
1356
1357 add_settings_field(
1358 'whitelabel_recovery_email',
1359 'Recovery Email',
1360 function() use ($option_key) {
1361 $whitelabel_settings = Metasync::get_whitelabel_settings();
1362 $value = $whitelabel_settings['recovery_email'] ?? '';
1363 $has_password = !empty($whitelabel_settings['settings_password']);
1364 printf('<input type="email" name="' . $option_key . '[whitelabel][recovery_email]" value="' . esc_attr($value) . '" size="30" autocomplete="email" %s />', $has_password ? 'required' : '');
1365 printf('<p class="description">Email address to receive password recovery. <strong>Required when password is set.</strong></p>');
1366 },
1367 $page_slug . '_branding',
1368 $SECTION_METASYNC
1369 );
1370
1371 add_settings_field(
1372 'whitelabel_quick_links',
1373 'Quick Links',
1374 function() use ($option_key) {
1375 $whitelabel_settings = Metasync::get_whitelabel_settings();
1376 $links = isset($whitelabel_settings['quick_links']) && is_array($whitelabel_settings['quick_links'])
1377 ? $whitelabel_settings['quick_links']
1378 : [];
1379 // Ensure at least 5 rows
1380 while (count($links) < 5) {
1381 $links[] = ['label' => '', 'url' => '', 'external' => false];
1382 }
1383 echo '<p class="description" style="margin-bottom:8px;">Add up to 5 links to show in the sidebar Quick Links widget. Leave blank to hide the widget when whitelabeling is active.</p>';
1384 echo '<style>.metasync-quick-links-table.widefat, .metasync-quick-links-table.widefat th, .metasync-quick-links-table.widefat td { background:transparent; border-color:var(--dashboard-border, #374151); }</style>';
1385 echo '<table class="widefat metasync-quick-links-table" style="max-width:560px;">';
1386 echo '<thead><tr><th>Label</th><th>URL</th><th>Open in new tab</th></tr></thead><tbody>';
1387 foreach (array_slice($links, 0, 5) as $i => $link) {
1388 $label = esc_attr($link['label'] ?? '');
1389 $url = esc_attr($link['url'] ?? '');
1390 $external = !empty($link['external']);
1391 printf(
1392 '<tr>
1393 <td><input type="text" name="%s[whitelabel][quick_links][%d][label]" value="%s" placeholder="Link label" style="width:100%%" /></td>
1394 <td><input type="url" name="%s[whitelabel][quick_links][%d][url]" value="%s" placeholder="https://…" style="width:100%%" /></td>
1395 <td style="text-align:center"><input type="checkbox" name="%s[whitelabel][quick_links][%d][external]" value="1" %s /></td>
1396 </tr>',
1397 esc_attr($option_key), $i, $label,
1398 esc_attr($option_key), $i, $url,
1399 esc_attr($option_key), $i, checked($external, true, false)
1400 );
1401 }
1402 echo '</tbody></table>';
1403 },
1404 $page_slug . '_branding',
1405 $SECTION_METASYNC
1406 );
1407
1408 // ======================================================================
1409 // Schema Markup Settings — dedicated page (SEO → Schema Markup)
1410 // ======================================================================
1411 $schema_page = $page_slug . '_schema-markup';
1412
1413 add_settings_section(
1414 'metasync_schema_org',
1415 'Organization Schema (Site-Wide)',
1416 function() {
1417 echo '<p>This Organization schema is injected on every page of your site.</p>';
1418 },
1419 $schema_page
1420 );
1421
1422 add_settings_field(
1423 'schema_org_name',
1424 'Organization Name',
1425 function() use ($option_key) {
1426 $schema = Metasync::get_option('schema') ?? [];
1427 $value = $schema['org_name'] ?? '';
1428 printf('<input type="text" name="' . $option_key . '[schema][org_name]" value="%s" size="40" placeholder="Your organization name" />', esc_attr($value));
1429 },
1430 $schema_page,
1431 'metasync_schema_org'
1432 );
1433
1434 add_settings_field(
1435 'schema_org_url',
1436 'Organization URL',
1437 function() use ($option_key) {
1438 $schema = Metasync::get_option('schema') ?? [];
1439 $value = $schema['org_url'] ?? '';
1440 printf('<input type="url" name="' . $option_key . '[schema][org_url]" value="%s" size="40" placeholder="https://example.com" />', esc_attr($value));
1441 },
1442 $schema_page,
1443 'metasync_schema_org'
1444 );
1445
1446 add_settings_field(
1447 'schema_org_logo',
1448 'Organization Logo URL',
1449 function() use ($option_key) {
1450 $schema = Metasync::get_option('schema') ?? [];
1451 $value = $schema['org_logo'] ?? '';
1452 printf('<input type="url" name="' . $option_key . '[schema][org_logo]" value="%s" size="40" placeholder="https://example.com/logo.png" />', esc_attr($value));
1453 },
1454 $schema_page,
1455 'metasync_schema_org'
1456 );
1457
1458 add_settings_field(
1459 'schema_org_contact_telephone',
1460 'Contact Telephone',
1461 function() use ($option_key) {
1462 $schema = Metasync::get_option('schema') ?? [];
1463 $value = $schema['org_contact_telephone'] ?? '';
1464 printf('<input type="text" name="' . $option_key . '[schema][org_contact_telephone]" value="%s" size="30" placeholder="+1-555-1234" />', esc_attr($value));
1465 },
1466 $schema_page,
1467 'metasync_schema_org'
1468 );
1469
1470 add_settings_field(
1471 'schema_org_contact_type',
1472 'Contact Type',
1473 function() use ($option_key) {
1474 $schema = Metasync::get_option('schema') ?? [];
1475 $value = $schema['org_contact_type'] ?? '';
1476 printf('<input type="text" name="' . $option_key . '[schema][org_contact_type]" value="%s" size="30" placeholder="customer support" />', esc_attr($value));
1477 },
1478 $schema_page,
1479 'metasync_schema_org'
1480 );
1481
1482 add_settings_field(
1483 'schema_org_same_as',
1484 'Social Profile URLs',
1485 function() use ($option_key) {
1486 $schema = Metasync::get_option('schema') ?? [];
1487 $value = $schema['org_same_as'] ?? '';
1488 printf('<textarea name="' . $option_key . '[schema][org_same_as]" rows="4" cols="50" placeholder="One URL per line&#10;https://facebook.com/...&#10;https://twitter.com/...">%s</textarea>', esc_textarea($value));
1489 echo '<p class="description">Enter one social profile URL per line.</p>';
1490 },
1491 $schema_page,
1492 'metasync_schema_org'
1493 );
1494
1495 add_settings_section(
1496 'metasync_schema_website',
1497 'WebSite Schema (Homepage Only)',
1498 function() {
1499 echo '<p>This WebSite schema is injected on the homepage only. Enables the Google Sitelinks Searchbox.</p>';
1500 },
1501 $schema_page
1502 );
1503
1504 add_settings_field(
1505 'schema_website_name',
1506 'WebSite Name',
1507 function() use ($option_key) {
1508 $schema = Metasync::get_option('schema') ?? [];
1509 $value = $schema['website_name'] ?? '';
1510 printf('<input type="text" name="' . $option_key . '[schema][website_name]" value="%s" size="40" placeholder="Your website name" />', esc_attr($value));
1511 },
1512 $schema_page,
1513 'metasync_schema_website'
1514 );
1515
1516 add_settings_field(
1517 'schema_website_url',
1518 'WebSite URL',
1519 function() use ($option_key) {
1520 $schema = Metasync::get_option('schema') ?? [];
1521 $value = $schema['website_url'] ?? '';
1522 printf('<input type="url" name="' . $option_key . '[schema][website_url]" value="%s" size="40" placeholder="https://example.com" />', esc_attr($value));
1523 },
1524 $schema_page,
1525 'metasync_schema_website'
1526 );
1527
1528 add_settings_field(
1529 'schema_website_searchbox',
1530 'Enable Sitelinks Searchbox',
1531 function() use ($option_key) {
1532 $schema = Metasync::get_option('schema') ?? [];
1533 $value = $schema['website_searchbox'] ?? false;
1534 printf(
1535 '<input type="checkbox" name="' . $option_key . '[schema][website_searchbox]" value="1" %s />',
1536 checked($value, true, false)
1537 );
1538 echo '<span class="description">Adds SearchAction to WebSite schema for Google Sitelinks Searchbox.</span>';
1539 },
1540 $schema_page,
1541 'metasync_schema_website'
1542 );
1543
1544 add_settings_section(
1545 'metasync_schema_compat',
1546 'Compatibility',
1547 function() {
1548 echo '<p>Control how MetaSync interacts with other plugins that output structured data.</p>';
1549 },
1550 $schema_page
1551 );
1552
1553 add_settings_field(
1554 'schema_override_woocommerce',
1555 'Override WooCommerce Product Schema',
1556 function() use ($option_key) {
1557 $schema = Metasync::get_option('schema') ?? [];
1558 $value = $schema['override_woocommerce_schema'] ?? false;
1559 printf(
1560 '<input type="checkbox" name="' . $option_key . '[schema][override_woocommerce_schema]" value="1" %s />',
1561 checked($value, true, false)
1562 );
1563 echo '<span class="description">When unchecked (default), MetaSync suppresses its Product schema when WooCommerce is active to avoid duplication. Check to override WooCommerce\'s native Product schema.</span>';
1564 },
1565 $schema_page,
1566 'metasync_schema_compat'
1567 );
1568
1569 // ----------------------------------------------------------------
1570 // Site Verification page
1571 // ----------------------------------------------------------------
1572 add_settings_section(
1573 $SECTION_SEARCHENGINE,
1574 '',
1575 function() {},
1576 $page_slug . '_searchengines-verification'
1577 );
1578
1579 add_settings_field(
1580 'google_site_verification',
1581 'Google Search Console',
1582 array( Metasync_Settings_Fields::instance(), 'google_site_verification_callback' ),
1583 $page_slug . '_searchengines-verification',
1584 $SECTION_SEARCHENGINE
1585 );
1586
1587 add_settings_field(
1588 'bing_site_verification',
1589 'Bing Webmaster Tools',
1590 array( Metasync_Settings_Fields::instance(), 'bing_site_verification_callback' ),
1591 $page_slug . '_searchengines-verification',
1592 $SECTION_SEARCHENGINE
1593 );
1594
1595 add_settings_field(
1596 'yandex_site_verification',
1597 'Yandex Webmaster',
1598 array( Metasync_Settings_Fields::instance(), 'yandex_site_verification_callback' ),
1599 $page_slug . '_searchengines-verification',
1600 $SECTION_SEARCHENGINE
1601 );
1602
1603 add_settings_field(
1604 'pinterest_site_verification',
1605 'Pinterest',
1606 array( Metasync_Settings_Fields::instance(), 'pinterest_site_verification_callback' ),
1607 $page_slug . '_searchengines-verification',
1608 $SECTION_SEARCHENGINE
1609 );
1610
1611 add_settings_field(
1612 'baidu_site_verification',
1613 'Baidu',
1614 function() use ($option_key) {
1615 $value = Metasync::get_option('searchengines')['baidu_site_verification'] ?? '';
1616 printf( '<input type="text" id="baidu_site_verification" name="' . $option_key . '[searchengines][baidu_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1617 },
1618 $page_slug . '_searchengines-verification',
1619 $SECTION_SEARCHENGINE
1620 );
1621
1622 add_settings_field(
1623 'alexa_site_verification',
1624 'Alexa',
1625 function() use ($option_key) {
1626 $value = Metasync::get_option('searchengines')['alexa_site_verification'] ?? '';
1627 printf( '<input type="text" id="alexa_site_verification" name="' . $option_key . '[searchengines][alexa_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1628 },
1629 $page_slug . '_searchengines-verification',
1630 $SECTION_SEARCHENGINE
1631 );
1632
1633 add_settings_field(
1634 'norton_save_site_verification',
1635 'Norton Safe Web',
1636 function() use ($option_key) {
1637 $value = Metasync::get_option('searchengines')['norton_save_site_verification'] ?? '';
1638 printf( '<input type="text" id="norton_save_site_verification" name="' . $option_key . '[searchengines][norton_save_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1639 },
1640 $page_slug . '_searchengines-verification',
1641 $SECTION_SEARCHENGINE
1642 );
1643
1644 // ----------------------------------------------------------------
1645 // Code Snippets page
1646 // ----------------------------------------------------------------
1647 add_settings_section(
1648 $SECTION_CODESNIPPETS,
1649 '',
1650 function() {},
1651 $page_slug . '_code-snippets'
1652 );
1653
1654 add_settings_field(
1655 'header_snippet',
1656 'Header Code',
1657 array( Metasync_Settings_Fields::instance(), 'header_snippets_callback' ),
1658 $page_slug . '_code-snippets',
1659 $SECTION_CODESNIPPETS
1660 );
1661
1662 add_settings_field(
1663 'footer_snippet',
1664 'Footer Code',
1665 array( Metasync_Settings_Fields::instance(), 'footer_snippets_callback' ),
1666 $page_slug . '_code-snippets',
1667 $SECTION_CODESNIPPETS
1668 );
1669
1670 // ----------------------------------------------------------------
1671 // Local Business page
1672 // ----------------------------------------------------------------
1673 add_settings_section(
1674 $SECTION_LOCALSEO,
1675 '',
1676 function() {},
1677 $page_slug . '_local-seo'
1678 );
1679
1680 $fields_instance = Metasync_Settings_Fields::instance();
1681
1682 add_settings_field( 'local_seo_person_organization', 'Type', array( $fields_instance, 'local_seo_person_organization_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1683 add_settings_field( 'local_seo_name', 'Name', array( $fields_instance, 'local_seo_name_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1684 add_settings_field( 'local_seo_logo', 'Logo', array( $fields_instance, 'local_seo_logo_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1685 add_settings_field( 'local_seo_url', 'Website URL', array( $fields_instance, 'local_seo_url_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1686 add_settings_field( 'local_seo_email', 'Email', array( $fields_instance, 'local_seo_email_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1687 add_settings_field( 'local_seo_phone', 'Phone', array( $fields_instance, 'local_seo_phone_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1688 add_settings_field( 'local_seo_address', 'Address', array( $fields_instance, 'local_seo_address_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1689 add_settings_field( 'local_seo_business_type', 'Business Type', array( $fields_instance, 'local_seo_business_type_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1690 add_settings_field( 'local_seo_opening_hours', 'Opening Hours', array( $fields_instance, 'local_seo_opening_hours_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1691 add_settings_field( 'local_seo_phone_numbers', 'Phone Numbers', array( $fields_instance, 'local_seo_phone_numbers_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1692 add_settings_field( 'local_seo_price_range', 'Price Range', array( $fields_instance, 'local_seo_price_range_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1693 add_settings_field( 'local_seo_about_page', 'About Page', array( $fields_instance, 'local_seo_about_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1694 add_settings_field( 'local_seo_contact_page', 'Contact Page', array( $fields_instance, 'local_seo_contact_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1695 add_settings_field( 'local_seo_map_key', 'Google Maps Key', array( $fields_instance, 'local_seo_map_key_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1696 add_settings_field( 'local_seo_geo_coordinates', 'Geo Coordinates', array( $fields_instance, 'local_seo_geo_coordinates_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1697
1698 // ----------------------------------------------------------------
1699 // Breadcrumbs page
1700 // ----------------------------------------------------------------
1701 add_settings_section(
1702 $SECTION_BREADCRUMBS,
1703 '',
1704 function() {},
1705 $page_slug . '_breadcrumbs'
1706 );
1707
1708 add_settings_field(
1709 'breadcrumbs_enabled',
1710 'Enable Breadcrumbs',
1711 function() use ($option_key) {
1712 $value = Metasync::get_option('breadcrumbs')['enabled'] ?? true;
1713 printf(
1714 '<input type="checkbox" id="breadcrumbs_enabled" name="' . $option_key . '[breadcrumbs][enabled]" value="1" %s />',
1715 checked(1, $value, false)
1716 );
1717 echo '<p class="description">Output breadcrumb trail HTML on your site.</p>';
1718 },
1719 $page_slug . '_breadcrumbs',
1720 $SECTION_BREADCRUMBS
1721 );
1722
1723 add_settings_field(
1724 'breadcrumbs_separator',
1725 'Separator',
1726 function() use ($option_key) {
1727 $value = Metasync::get_option('breadcrumbs')['separator'] ?? '&raquo;';
1728 printf(
1729 '<input type="text" id="breadcrumbs_separator" name="' . $option_key . '[breadcrumbs][separator]" value="%s" size="10" />',
1730 esc_attr($value)
1731 );
1732 echo '<p class="description">Character or HTML entity shown between crumbs (e.g. &raquo; or /).</p>';
1733 },
1734 $page_slug . '_breadcrumbs',
1735 $SECTION_BREADCRUMBS
1736 );
1737
1738 add_settings_field(
1739 'breadcrumbs_home_label',
1740 'Home Label',
1741 function() use ($option_key) {
1742 $value = Metasync::get_option('breadcrumbs')['home_label'] ?? 'Home';
1743 printf(
1744 '<input type="text" id="breadcrumbs_home_label" name="' . $option_key . '[breadcrumbs][home_label]" value="%s" size="30" />',
1745 esc_attr($value)
1746 );
1747 },
1748 $page_slug . '_breadcrumbs',
1749 $SECTION_BREADCRUMBS
1750 );
1751
1752 add_settings_field(
1753 'breadcrumbs_home_url',
1754 'Home URL',
1755 function() use ($option_key) {
1756 $value = Metasync::get_option('breadcrumbs')['home_url'] ?? '';
1757 printf(
1758 '<input type="url" id="breadcrumbs_home_url" name="' . $option_key . '[breadcrumbs][home_url]" value="%s" size="50" />',
1759 esc_attr($value)
1760 );
1761 echo '<p class="description">Leave blank to use the site home URL.</p>';
1762 },
1763 $page_slug . '_breadcrumbs',
1764 $SECTION_BREADCRUMBS
1765 );
1766
1767 add_settings_field(
1768 'breadcrumbs_show_current_page',
1769 'Show Current Page',
1770 function() use ($option_key) {
1771 $value = Metasync::get_option('breadcrumbs')['show_current_page'] ?? true;
1772 printf(
1773 '<input type="checkbox" id="breadcrumbs_show_current_page" name="' . $option_key . '[breadcrumbs][show_current_page]" value="1" %s />',
1774 checked(1, $value, false)
1775 );
1776 echo '<p class="description">Include the current page as the last (non-linked) crumb.</p>';
1777 },
1778 $page_slug . '_breadcrumbs',
1779 $SECTION_BREADCRUMBS
1780 );
1781
1782 add_settings_field(
1783 'breadcrumbs_prefix_text',
1784 'Prefix Text',
1785 function() use ($option_key) {
1786 $value = Metasync::get_option('breadcrumbs')['prefix_text'] ?? '';
1787 printf(
1788 '<input type="text" id="breadcrumbs_prefix_text" name="' . $option_key . '[breadcrumbs][prefix_text]" value="%s" size="30" />',
1789 esc_attr($value)
1790 );
1791 echo '<p class="description">Optional text before the breadcrumb trail (e.g. "You are here:").</p>';
1792 },
1793 $page_slug . '_breadcrumbs',
1794 $SECTION_BREADCRUMBS
1795 );
1796
1797 add_settings_field(
1798 'breadcrumbs_archive_label_format',
1799 'Archive Label Format',
1800 function() use ($option_key) {
1801 $value = Metasync::get_option('breadcrumbs')['archive_label_format'] ?? '{name}';
1802 printf(
1803 '<input type="text" id="breadcrumbs_archive_label_format" name="' . $option_key . '[breadcrumbs][archive_label_format]" value="%s" size="30" />',
1804 esc_attr($value)
1805 );
1806 echo '<p class="description">Format for archive crumb labels. Use <code>{name}</code> as placeholder for the archive name.</p>';
1807 },
1808 $page_slug . '_breadcrumbs',
1809 $SECTION_BREADCRUMBS
1810 );
1811
1812 # Open Graph / Article meta toggles — surface each new tag family as its
1813 # own opt-out checkbox on the main Settings page (Open Graph accordion).
1814 add_settings_field(
1815 'og_image_dimensions',
1816 'OG Image Dimensions',
1817 array(Metasync_Settings_Fields::instance(), 'og_image_dimensions_callback'),
1818 $page_slug . '_general',
1819 $SECTION_METASYNC
1820 );
1821
1822 add_settings_field(
1823 'article_timestamps',
1824 'Article Timestamps',
1825 array(Metasync_Settings_Fields::instance(), 'article_timestamps_callback'),
1826 $page_slug . '_general',
1827 $SECTION_METASYNC
1828 );
1829
1830 add_settings_field(
1831 'article_author',
1832 'Article Author',
1833 array(Metasync_Settings_Fields::instance(), 'article_author_callback'),
1834 $page_slug . '_general',
1835 $SECTION_METASYNC
1836 );
1837
1838 add_settings_field(
1839 'article_section',
1840 'Article Section',
1841 array(Metasync_Settings_Fields::instance(), 'article_section_callback'),
1842 $page_slug . '_general',
1843 $SECTION_METASYNC
1844 );
1845
1846 add_settings_field(
1847 'article_tags',
1848 'Article Tags',
1849 array(Metasync_Settings_Fields::instance(), 'article_tags_callback'),
1850 $page_slug . '_general',
1851 $SECTION_METASYNC
1852 );
1853
1854 add_settings_field(
1855 'twitter_image_alt',
1856 'Twitter Image Alt',
1857 array(Metasync_Settings_Fields::instance(), 'twitter_image_alt_callback'),
1858 $page_slug . '_general',
1859 $SECTION_METASYNC
1860 );
1861
1862 }
1863
1864 /**
1865 * Sanitize each setting field as needed.
1866 *
1867 * Formerly Metasync_Admin::sanitize().
1868 *
1869 * @param array $input Contains all settings fields as array keys
1870 * @return array Sanitized settings
1871 */
1872 public function sanitize($input)
1873 {
1874 $new_input = Metasync::get_option();
1875 $whitelabel_validation_failed = false;
1876
1877 # Determine which tab is being submitted (same logic as AJAX handler)
1878 # Check POST first (from AJAX), then GET (from form action URL)
1879 $active_tab = 'general';
1880 if (isset($_POST['active_tab'])) {
1881 $active_tab = sanitize_text_field($_POST['active_tab']);
1882 } elseif (isset($_GET['tab'])) {
1883 $active_tab = sanitize_text_field($_GET['tab']);
1884 } elseif (isset($_POST['_wp_http_referer'])) {
1885 # Parse referer URL to get tab parameter
1886 $referer = wp_parse_url($_POST['_wp_http_referer']);
1887 if (isset($referer['query'])) {
1888 parse_str($referer['query'], $referer_params);
1889 if (isset($referer_params['tab'])) {
1890 $active_tab = sanitize_text_field($referer_params['tab']);
1891 }
1892 }
1893 }
1894 $general_tab_submitted = ($active_tab === 'general');
1895
1896 // General Settings
1897 if (isset($input['general']['apikey'])) {
1898 $new_input['general']['apikey'] = sanitize_text_field($input['general']['apikey']);
1899 }
1900
1901 // Meta Box Visibility Settings
1902 if (isset($input['general']) && $general_tab_submitted) {
1903 $checkbox_fields = [
1904 'disable_common_robots_metabox',
1905 'disable_advance_robots_metabox',
1906 'disable_redirection_metabox',
1907 'disable_canonical_metabox',
1908 'disable_social_opengraph_metabox',
1909 'disable_schema_markup_metabox',
1910 'open_external_links'
1911 ];
1912
1913 foreach ($checkbox_fields as $field) {
1914 if (isset($input['general'][$field])) {
1915 $new_input['general'][$field] = filter_var($input['general'][$field], FILTER_VALIDATE_BOOLEAN);
1916 } else {
1917 $new_input['general'][$field] = false;
1918 }
1919 // The final array_merge($new_input, $input) is shallow, so
1920 // $input['general'] replaces $new_input['general'] wholesale.
1921 // Write the sanitized boolean back into $input so it survives.
1922 $input['general'][$field] = $new_input['general'][$field];
1923 }
1924 }
1925
1926 // Site Verification Settings
1927 if (isset($input['searchengines']['bing_site_verification'])) {
1928 $new_input['searchengines']['bing_site_verification'] = sanitize_text_field($input['searchengines']['bing_site_verification']);
1929 }
1930 if (isset($input['searchengines']['baidu_site_verification'])) {
1931 $new_input['searchengines']['baidu_site_verification'] = sanitize_text_field($input['searchengines']['baidu_site_verification']);
1932 }
1933 if (isset($input['searchengines']['alexa_site_verification'])) {
1934 $new_input['searchengines']['alexa_site_verification'] = sanitize_text_field($input['searchengines']['alexa_site_verification']);
1935 }
1936 if (isset($input['searchengines']['yandex_site_verification'])) {
1937 $new_input['searchengines']['yandex_site_verification'] = sanitize_text_field($input['searchengines']['yandex_site_verification']);
1938 }
1939 if (isset($input['searchengines']['google_site_verification'])) {
1940 $new_input['searchengines']['google_site_verification'] = sanitize_text_field($input['searchengines']['google_site_verification']);
1941 }
1942 if (isset($input['searchengines']['pinterest_site_verification'])) {
1943 $new_input['searchengines']['pinterest_site_verification'] = sanitize_text_field($input['searchengines']['pinterest_site_verification']);
1944 }
1945 if (isset($input['searchengines']['norton_save_site_verification'])) {
1946 $new_input['searchengines']['norton_save_site_verification'] = sanitize_text_field($input['searchengines']['norton_save_site_verification']);
1947 }
1948
1949 // Local Business SEO Settings
1950 if (isset($input['localseo']['local_seo_person_organization'])) {
1951 $new_input['localseo']['local_seo_person_organization'] = sanitize_text_field($input['localseo']['local_seo_person_organization']);
1952 }
1953 if (isset($input['localseo']['local_seo_name'])) {
1954 $new_input['localseo']['local_seo_name'] = sanitize_text_field($input['localseo']['local_seo_name']);
1955 }
1956 if (isset($input['localseo']['local_seo_logo'])) {
1957 $new_input['localseo']['local_seo_logo'] = sanitize_url($input['localseo']['local_seo_logo']);
1958 }
1959 if (isset($input['localseo']['local_seo_url'])) {
1960 $new_input['localseo']['local_seo_url'] = sanitize_url($input['localseo']['local_seo_url']);
1961 }
1962 if (isset($input['localseo']['local_seo_email'])) {
1963 $new_input['localseo']['local_seo_email'] = sanitize_email($input['localseo']['local_seo_email']);
1964 }
1965 if (isset($input['localseo']['local_seo_phone'])) {
1966 $new_input['localseo']['local_seo_phone'] = sanitize_text_field($input['localseo']['local_seo_phone']);
1967 }
1968 if (isset($input['localseo']['address']['street'])) {
1969 $new_input['localseo']['address']['street'] = sanitize_text_field($input['localseo']['address']['street']);
1970 }
1971 if (isset($input['localseo']['address']['locality'])) {
1972 $new_input['localseo']['address']['locality'] = sanitize_text_field($input['localseo']['address']['locality']);
1973 }
1974 if (isset($input['localseo']['address']['region'])) {
1975 $new_input['localseo']['address']['region'] = sanitize_text_field($input['localseo']['address']['region']);
1976 }
1977 if (isset($input['localseo']['address']['postancode'])) {
1978 $new_input['localseo']['address']['postancode'] = sanitize_text_field($input['localseo']['address']['postancode']);
1979 }
1980 if (isset($input['localseo']['address']['country'])) {
1981 $new_input['localseo']['address']['country'] = sanitize_text_field($input['localseo']['address']['country']);
1982 }
1983 if (isset($input['localseo']['local_seo_business_type'])) {
1984 $new_input['localseo']['local_seo_business_type'] = sanitize_text_field($input['localseo']['local_seo_business_type']);
1985 }
1986 if (isset($input['localseo']['local_seo_hours_format'])) {
1987 $new_input['localseo']['local_seo_hours_format'] = sanitize_text_field($input['localseo']['local_seo_hours_format']);
1988 }
1989 if (isset($input['localseo']['days'])) {
1990 $new_input['localseo']['days'] = sanitize_text_field($input['localseo']['days']);
1991 }
1992 if (isset($input['localseo']['times'])) {
1993 $new_input['localseo']['times'] = sanitize_text_field($input['localseo']['times']);
1994 }
1995 if (isset($input['localseo']['phonetype'])) {
1996 $new_input['localseo']['phonetype'] = sanitize_text_field($input['localseo']['phonetype']);
1997 }
1998 if (isset($input['localseo']['phonenumber'])) {
1999 $new_input['localseo']['phonenumber'] = sanitize_text_field($input['localseo']['phonenumber']);
2000 }
2001 if (isset($input['localseo']['local_seo_price_range'])) {
2002 $new_input['localseo']['local_seo_price_range'] = sanitize_text_field($input['localseo']['local_seo_price_range']);
2003 }
2004 if (isset($input['localseo']['local_seo_about_page'])) {
2005 $new_input['localseo']['local_seo_about_page'] = sanitize_text_field($input['localseo']['local_seo_about_page']);
2006 }
2007 if (isset($input['localseo']['local_seo_contact_page'])) {
2008 $new_input['localseo']['local_seo_contact_page'] = sanitize_text_field($input['localseo']['local_seo_contact_page']);
2009 }
2010 if (isset($input['localseo']['local_seo_map_key'])) {
2011 $new_input['localseo']['local_seo_map_key'] = sanitize_text_field($input['localseo']['local_seo_map_key']);
2012 }
2013 if (isset($input['localseo']['local_seo_geo_coordinates'])) {
2014 $new_input['localseo']['local_seo_geo_coordinates'] = sanitize_text_field($input['localseo']['local_seo_geo_coordinates']);
2015 }
2016
2017 // Breadcrumbs Settings
2018 if (isset($input['breadcrumbs'])) {
2019 $new_input['breadcrumbs']['enabled'] = !empty($input['breadcrumbs']['enabled']);
2020 $new_input['breadcrumbs']['show_current_page'] = !empty($input['breadcrumbs']['show_current_page']);
2021 $new_input['breadcrumbs']['disable_schema'] = !empty($input['breadcrumbs']['disable_schema']);
2022 if (isset($input['breadcrumbs']['separator'])) {
2023 $new_input['breadcrumbs']['separator'] = sanitize_text_field($input['breadcrumbs']['separator']);
2024 }
2025 if (isset($input['breadcrumbs']['home_label'])) {
2026 $new_input['breadcrumbs']['home_label'] = sanitize_text_field($input['breadcrumbs']['home_label']);
2027 }
2028 if (isset($input['breadcrumbs']['home_url'])) {
2029 $new_input['breadcrumbs']['home_url'] = esc_url_raw($input['breadcrumbs']['home_url']);
2030 }
2031 if (isset($input['breadcrumbs']['prefix_text'])) {
2032 $new_input['breadcrumbs']['prefix_text'] = sanitize_text_field($input['breadcrumbs']['prefix_text']);
2033 }
2034 if (isset($input['breadcrumbs']['archive_label_format'])) {
2035 $new_input['breadcrumbs']['archive_label_format'] = sanitize_text_field($input['breadcrumbs']['archive_label_format']);
2036 }
2037 // Remove raw breadcrumbs so sanitized values (especially checkbox
2038 // booleans) survive the shallow array_merge($new_input, $input).
2039 unset($input['breadcrumbs']);
2040 }
2041
2042 // Code Snippets Settings
2043 if (isset($input['codesnippets']['header_snippet'])) {
2044 $new_input['codesnippets']['header_snippet'] = sanitize_text_field($input['codesnippets']['header_snippet']);
2045 }
2046 if (isset($input['codesnippets']['footer_snippet'])) {
2047 $new_input['codesnippets']['footer_snippet'] = sanitize_text_field($input['codesnippets']['footer_snippet']);
2048 }
2049
2050
2051 // Optimal Settings
2052 if (isset($input['optimal_settings']['no_index_posts'])) {
2053 $new_input['optimal_settings']['no_index_posts'] = boolval($input['optimal_settings']['no_index_posts']);
2054 }
2055 if (isset($input['optimal_settings']['no_follow_links'])) {
2056 $new_input['optimal_settings']['no_follow_links'] = boolval($input['optimal_settings']['no_follow_links']);
2057 }
2058 if (isset($input['optimal_settings']['open_external_links'])) {
2059 $new_input['optimal_settings']['open_external_links'] = boolval($input['optimal_settings']['open_external_links']);
2060 }
2061 if (isset($input['optimal_settings']['add_alt_image_tags'])) {
2062 $new_input['optimal_settings']['add_alt_image_tags'] = boolval($input['optimal_settings']['add_alt_image_tags']);
2063 }
2064 if (isset($input['optimal_settings']['add_title_image_tags'])) {
2065 $new_input['optimal_settings']['add_title_image_tags'] = boolval($input['optimal_settings']['add_title_image_tags']);
2066 }
2067
2068 // Site Information - Optimal Settings
2069 if (isset($input['optimal_settings']['site_info']['type'])) {
2070 $new_input['optimal_settings']['site_info']['type'] = sanitize_text_field($input['optimal_settings']['site_info']['type']);
2071 }
2072 if (isset($input['optimal_settings']['site_info']['business_type'])) {
2073 $new_input['optimal_settings']['site_info']['business_type'] = sanitize_text_field($input['optimal_settings']['site_info']['business_type']);
2074 }
2075 if (isset($input['optimal_settings']['site_info']['company_name'])) {
2076 $new_input['optimal_settings']['site_info']['company_name'] = sanitize_text_field($input['optimal_settings']['site_info']['company_name']);
2077 }
2078 if (isset($input['optimal_settings']['site_info']['google_logo'])) {
2079 $new_input['optimal_settings']['site_info']['google_logo'] = sanitize_url($input['optimal_settings']['site_info']['google_logo']);
2080 }
2081 if (isset($input['optimal_settings']['site_info']['social_share_image'])) {
2082 $new_input['optimal_settings']['site_info']['social_share_image'] = sanitize_url($input['optimal_settings']['site_info']['social_share_image']);
2083 }
2084
2085 // Common Setting - Global Settings
2086 $common_robots_field = isset($input['common_robots_meta']) ? 'common_robots_meta' : 'common_robots_mata';
2087
2088 if (isset($input[$common_robots_field]['index'])) {
2089 $new_input['common_robots_meta']['index'] = boolval($input[$common_robots_field]['index']);
2090 }
2091 if (isset($input[$common_robots_field]['noindex'])) {
2092 $new_input['common_robots_meta']['noindex'] = boolval($input[$common_robots_field]['noindex']);
2093 }
2094 if (isset($input[$common_robots_field]['nofollow'])) {
2095 $new_input['common_robots_meta']['nofollow'] = boolval($input[$common_robots_field]['nofollow']);
2096 }
2097 if (isset($input[$common_robots_field]['noarchive'])) {
2098 $new_input['common_robots_meta']['noarchive'] = boolval($input[$common_robots_field]['noarchive']);
2099 }
2100 if (isset($input[$common_robots_field]['noimageindex'])) {
2101 $new_input['common_robots_meta']['noimageindex'] = boolval($input[$common_robots_field]['noimageindex']);
2102 }
2103 if (isset($input[$common_robots_field]['nosnippet'])) {
2104 $new_input['common_robots_meta']['nosnippet'] = boolval($input[$common_robots_field]['nosnippet']);
2105 }
2106
2107 // Advance Setting - Global Settings
2108 $advance_robots_field = isset($input['advance_robots_meta']) ? 'advance_robots_meta' : 'advance_robots_mata';
2109
2110 if (isset($input[$advance_robots_field]['max-snippet']['enable'])) {
2111 $new_input['advance_robots_meta']['max-snippet']['enable'] = boolval($input[$advance_robots_field]['max-snippet']['enable']);
2112 }
2113 if (isset($input[$advance_robots_field]['max-snippet']['length'])) {
2114 $new_input['advance_robots_meta']['max-snippet']['length'] = sanitize_text_field($input[$advance_robots_field]['max-snippet']['length']);
2115 }
2116 if (isset($input[$advance_robots_field]['max-video-preview']['enable'])) {
2117 $new_input['advance_robots_meta']['max-video-preview']['enable'] = boolval($input[$advance_robots_field]['max-video-preview']['enable']);
2118 }
2119 if (isset($input[$advance_robots_field]['max-video-preview']['length'])) {
2120 $new_input['advance_robots_meta']['max-video-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-video-preview']['length']);
2121 }
2122 if (isset($input[$advance_robots_field]['max-image-preview']['enable'])) {
2123 $new_input['advance_robots_meta']['max-image-preview']['enable'] = boolval($input[$advance_robots_field]['max-image-preview']['enable']);
2124 }
2125 if (isset($input[$advance_robots_field]['max-image-preview']['length'])) {
2126 $new_input['advance_robots_meta']['max-image-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-image-preview']['length']);
2127 }
2128
2129 // Social meta settings
2130 if (isset($input['social_meta']['facebook_page_url'])) {
2131 $new_input['social_meta']['facebook_page_url'] = sanitize_text_field($input['social_meta']['facebook_page_url']);
2132 }
2133 if (isset($input['social_meta']['facebook_authorship'])) {
2134 $new_input['social_meta']['facebook_authorship'] = sanitize_text_field($input['social_meta']['facebook_authorship']);
2135 }
2136 if (isset($input['social_meta']['facebook_admin'])) {
2137 $new_input['social_meta']['facebook_admin'] = sanitize_text_field($input['social_meta']['facebook_admin']);
2138 }
2139 if (isset($input['social_meta']['facebook_app'])) {
2140 $new_input['social_meta']['facebook_app'] = sanitize_text_field($input['social_meta']['facebook_app']);
2141 }
2142 if (isset($input['social_meta']['facebook_secret'])) {
2143 $new_input['social_meta']['facebook_secret'] = sanitize_text_field($input['social_meta']['facebook_secret']);
2144 }
2145 if (isset($input['social_meta']['twitter_username'])) {
2146 $new_input['social_meta']['twitter_username'] = sanitize_text_field($input['social_meta']['twitter_username']);
2147 }
2148
2149 // Common Meta Settings — Open Graph / article toggle checkboxes.
2150 // Unchecked HTML checkboxes are absent from $input, so an explicit
2151 // 'true'/'false' round-trip is required to persist opt-outs.
2152 if (isset($input['common_meta_settings'])) {
2153 $og_toggle_fields = [
2154 'og_image_dimensions',
2155 'article_timestamps',
2156 'article_author',
2157 'article_section',
2158 'article_tags',
2159 'twitter_image_alt',
2160 ];
2161 foreach ($og_toggle_fields as $field) {
2162 $new_input['common_meta_settings'][$field] = (isset($input['common_meta_settings'][$field]) && $input['common_meta_settings'][$field] === 'true') ? 'true' : 'false';
2163 }
2164 // Remove raw common_meta_settings so sanitized toggle values
2165 // survive the shallow array_merge($new_input, $input).
2166 unset($input['common_meta_settings']);
2167 }
2168
2169 # Handle whitelabel URL fields with improved empty value handling
2170 if (isset($input['whitelabel'])) {
2171 $existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
2172
2173 $general_input = $input['general'] ?? [];
2174 $plugin_name = isset($general_input['white_label_plugin_name']) ? trim((string) $general_input['white_label_plugin_name']) : '';
2175 $logo_light = isset($input['whitelabel']['logo_light']) ? trim((string) $input['whitelabel']['logo_light']) : '';
2176 $logo_dark = isset($input['whitelabel']['logo_dark']) ? trim((string) $input['whitelabel']['logo_dark']) : '';
2177 $author = isset($general_input['white_label_plugin_author']) ? trim((string) $general_input['white_label_plugin_author']) : '';
2178 $author_uri = isset($general_input['white_label_plugin_author_uri']) ? trim((string) $general_input['white_label_plugin_author_uri']) : '';
2179 $plugin_uri = isset($general_input['white_label_plugin_uri']) ? trim((string) $general_input['white_label_plugin_uri']) : '';
2180 $domain = isset($input['whitelabel']['domain']) ? trim((string) $input['whitelabel']['domain']) : '';
2181 $description = isset($general_input['white_label_plugin_description']) ? trim((string) $general_input['white_label_plugin_description']) : '';
2182
2183 $has_core_field = (!empty($plugin_name) || (!empty($logo_light) && filter_var($logo_light, FILTER_VALIDATE_URL)) || (!empty($logo_dark) && filter_var($logo_dark, FILTER_VALIDATE_URL)) || !empty($author));
2184 $has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
2185
2186 if ($has_optional_only && !$has_core_field) {
2187 add_settings_error(
2188 'metasync_options',
2189 'whitelabel_partial',
2190 'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.',
2191 'error'
2192 );
2193 $new_input['whitelabel'] = $existing_whitelabel;
2194 $whitelabel_validation_failed = true;
2195 } else {
2196 $new_input['whitelabel'] = $existing_whitelabel;
2197
2198 if (isset($input['whitelabel']['logo'])) {
2199 $logo_value = trim($input['whitelabel']['logo']);
2200
2201 if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
2202 $new_input['whitelabel']['logo'] = esc_url_raw($logo_value);
2203
2204 } else {
2205 $new_input['whitelabel']['logo'] = '';
2206
2207 }
2208 }
2209
2210 if (isset($input['whitelabel']['logo_light'])) {
2211 $logo_light_value = trim($input['whitelabel']['logo_light']);
2212
2213 if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
2214 $new_input['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
2215 } else {
2216 $new_input['whitelabel']['logo_light'] = '';
2217 }
2218 }
2219
2220 if (isset($input['whitelabel']['logo_dark'])) {
2221 $logo_dark_value = trim($input['whitelabel']['logo_dark']);
2222
2223 if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
2224 $new_input['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
2225 } else {
2226 $new_input['whitelabel']['logo_dark'] = '';
2227 }
2228 }
2229
2230 if (isset($input['whitelabel']['domain'])) {
2231 $domain_value = trim($input['whitelabel']['domain']);
2232
2233 if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
2234 $new_input['whitelabel']['domain'] = esc_url_raw($domain_value);
2235
2236 } else {
2237 $old_domain = $existing_whitelabel['domain'] ?? '';
2238 $new_input['whitelabel']['domain'] = '';
2239
2240 if (!empty($old_domain)) {
2241 $new_input['_trigger_heartbeat_after_save'] = 'Domain cleared from: ' . $old_domain;
2242 }
2243 }
2244 }
2245
2246 if (isset($input['whitelabel']['settings_password'])) {
2247 $password_value = trim($input['whitelabel']['settings_password']);
2248 $new_input['whitelabel']['settings_password'] = sanitize_text_field($password_value);
2249 } else {
2250 if (isset($existing_whitelabel['settings_password'])) {
2251 $new_input['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2252 }
2253 }
2254
2255 if (isset($input['whitelabel']['recovery_email'])) {
2256 $recovery_email = trim($input['whitelabel']['recovery_email']);
2257 if (!empty($recovery_email)) {
2258 $sanitized_email = sanitize_email($recovery_email);
2259 if (is_email($sanitized_email)) {
2260 $new_input['whitelabel']['recovery_email'] = $sanitized_email;
2261 } else {
2262 add_settings_error(
2263 'metasync_messages',
2264 'metasync_message',
2265 __('Invalid recovery email address.', 'metasync'),
2266 'error'
2267 );
2268 }
2269 } else {
2270 $new_input['whitelabel']['recovery_email'] = '';
2271 }
2272 } else {
2273 if (isset($existing_whitelabel['recovery_email'])) {
2274 $new_input['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
2275 }
2276 }
2277
2278 $has_password = !empty($new_input['whitelabel']['settings_password']);
2279 $has_recovery_email = !empty($new_input['whitelabel']['recovery_email']);
2280 if ($has_password && !$has_recovery_email) {
2281 add_settings_error(
2282 'metasync_messages',
2283 'metasync_message',
2284 __('Password Recovery Email is required when a password is set.', 'metasync'),
2285 'error'
2286 );
2287 $new_input['whitelabel']['settings_password'] = '';
2288 }
2289
2290 if (isset($input['whitelabel']['access_control'])) {
2291 $new_input['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($input['whitelabel']['access_control']);
2292 }
2293
2294 // Sanitize quick_links (up to 5 entries)
2295 if (isset($input['whitelabel']['quick_links']) && is_array($input['whitelabel']['quick_links'])) {
2296 $sanitized_links = [];
2297 foreach (array_slice($input['whitelabel']['quick_links'], 0, 5) as $link) {
2298 $url = isset($link['url']) ? esc_url_raw(trim((string) $link['url'])) : '';
2299 if (empty($url)) {
2300 continue; // skip blank rows
2301 }
2302 $sanitized_links[] = [
2303 'label' => sanitize_text_field($link['label'] ?? ''),
2304 'url' => $url,
2305 'external' => !empty($link['external']),
2306 ];
2307 }
2308 $new_input['whitelabel']['quick_links'] = $sanitized_links;
2309 } else {
2310 // Preserve existing links when not submitted
2311 if (isset($existing_whitelabel['quick_links'])) {
2312 $new_input['whitelabel']['quick_links'] = $existing_whitelabel['quick_links'];
2313 }
2314 }
2315
2316 // Sanitize color palette
2317 if (isset($input['whitelabel']['color_palette']) && is_array($input['whitelabel']['color_palette'])) {
2318 $allowed_vars = array(
2319 'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
2320 'dashboard-text-primary', 'dashboard-text-secondary',
2321 'dashboard-accent', 'dashboard-accent-hover',
2322 'dashboard-success', 'dashboard-warning', 'dashboard-error',
2323 'dashboard-border',
2324 'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
2325 'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
2326 );
2327 $sanitized_palette = array();
2328 foreach (array('dark', 'light') as $theme_key) {
2329 if (isset($input['whitelabel']['color_palette'][$theme_key]) && is_array($input['whitelabel']['color_palette'][$theme_key])) {
2330 $sanitized_palette[$theme_key] = array();
2331 foreach ($input['whitelabel']['color_palette'][$theme_key] as $var_name => $color_value) {
2332 if (in_array($var_name, $allowed_vars, true)) {
2333 $color_value = trim(sanitize_text_field($color_value));
2334 if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
2335 $sanitized_palette[$theme_key][$var_name] = $color_value;
2336 }
2337 }
2338 }
2339 }
2340 }
2341 $new_input['whitelabel']['color_palette'] = $sanitized_palette;
2342 } else {
2343 if (isset($existing_whitelabel['color_palette'])) {
2344 $new_input['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
2345 }
2346 }
2347
2348 $new_input['whitelabel']['updated_at'] = time();
2349 }
2350 } else {
2351 $existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
2352 $has_existing_whitelabel = !empty($existing_whitelabel['domain']) || !empty($existing_whitelabel['logo']);
2353
2354 if ($has_existing_whitelabel) {
2355 $new_input['whitelabel'] = [
2356 'is_whitelabel' => false,
2357 'domain' => '',
2358 'logo' => '',
2359 'company_name' => '',
2360 'updated_at' => time()
2361 ];
2362
2363 if (!empty($existing_whitelabel['domain'])) {
2364 delete_transient('metasync_heartbeat_status_cache');
2365 do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel settings cleared - domain changed to default');
2366 }
2367 }
2368 }
2369
2370 // Indexation Control Settings
2371 if (isset($input['seo_controls']['index_date_archives'])) {
2372 $new_input['seo_controls']['index_date_archives'] = boolval($input['seo_controls']['index_date_archives']);
2373 }
2374 if (isset($input['seo_controls']['index_tag_archives'])) {
2375 $new_input['seo_controls']['index_tag_archives'] = boolval($input['seo_controls']['index_tag_archives']);
2376 }
2377 if (isset($input['seo_controls']['index_author_archives'])) {
2378 $new_input['seo_controls']['index_author_archives'] = boolval($input['seo_controls']['index_author_archives']);
2379 }
2380 if (isset($input['seo_controls']['index_format_archives'])) {
2381 $new_input['seo_controls']['index_format_archives'] = boolval($input['seo_controls']['index_format_archives']);
2382 }
2383
2384 // Handle post-save heartbeat trigger for domain changes
2385 if (isset($new_input['_trigger_heartbeat_after_save'])) {
2386 $context = $new_input['_trigger_heartbeat_after_save'];
2387 unset($new_input['_trigger_heartbeat_after_save']);
2388
2389 add_action('updated_option_metasync_options', function() use ($context) {
2390 delete_transient('metasync_heartbeat_status_cache');
2391 do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel domain change - ' . $context);
2392 });
2393 }
2394
2395 $result = array_merge($new_input, $input);
2396
2397 if ($whitelabel_validation_failed) {
2398 $result['whitelabel'] = Metasync::get_option()['whitelabel'] ?? [];
2399 }
2400
2401 delete_transient('metasync_otto_js_detected');
2402
2403 return $result;
2404 }
2405
2406 /**
2407 * AJAX handler for saving general/whitelabel/advanced settings.
2408 *
2409 * Moved from Metasync_Admin – the admin class now delegates here.
2410 */
2411 public function meta_sync_save_settings()
2412 {
2413 if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
2414 wp_send_json_error(array('message' => 'Invalid nonce'));
2415 return;
2416 }
2417
2418 if (!Metasync::current_user_has_plugin_access()) {
2419 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
2420 return;
2421 }
2422
2423 $text_fields = [
2424 'searchatlas_api_key', 'apikey',
2425 'white_label_plugin_name', 'white_label_plugin_description',
2426 'white_label_plugin_author', 'white_label_plugin_menu_slug',
2427 'white_label_plugin_menu_icon', 'enabled_plugin_css',
2428 'enabled_elementor_plugin_css_color','enabled_elementor_plugin_css',
2429 'otto_pixel_uuid','periodic_clear_otto_cache','periodic_clear_ottopage_cache',
2430 'periodic_clear_ottopost_cache', 'whitelabel_otto_name', 'otto_wp_rocket_compat'
2431 ];
2432
2433 $url_fields = [
2434 'white_label_plugin_author_uri', 'white_label_plugin_uri'
2435 ];
2436
2437 $bool_fields = ['otto_disable_on_loggedin', 'otto_disable_preview_button', 'otto_disable_for_bots' , 'hide_dashboard_framework', 'show_admin_bar_status', 'enable_auto_updates', 'disable_common_robots_metabox', 'disable_advance_robots_metabox', 'disable_redirection_metabox', 'disable_canonical_metabox', 'disable_social_opengraph_metabox', 'disable_schema_markup_metabox', 'open_external_links'];
2438
2439 $url_fields = ['white_label_plugin_author_uri', 'white_label_plugin_uri'];
2440
2441 $metasync_options = Metasync::get_option();
2442 if (!is_array($metasync_options)) {
2443 $metasync_options = array();
2444 }
2445 if (!isset($metasync_options['general']) || !is_array($metasync_options['general'])) {
2446 $metasync_options['general'] = array();
2447 }
2448
2449 $validation_errors = [];
2450
2451 $active_tab = isset($_POST['active_tab']) ? sanitize_text_field($_POST['active_tab']) : 'general';
2452 $general_tab_submitted = ($active_tab === 'general');
2453 $whitelabel_tab_submitted = ($active_tab === 'whitelabel');
2454
2455 foreach ($text_fields as $field) {
2456
2457 if (isset($_POST['metasync_options']['general'][$field])) {
2458
2459 $value = trim($_POST['metasync_options']['general'][$field]);
2460
2461 // searchatlas_api_key must be clearable to support key removal (WP-334)
2462 $clearable_fields = [
2463 'searchatlas_api_key',
2464 'white_label_plugin_name',
2465 'white_label_plugin_description',
2466 'white_label_plugin_author',
2467 'white_label_plugin_menu_slug',
2468 'white_label_plugin_menu_icon',
2469 'whitelabel_otto_name',
2470 'otto_pixel_uuid'
2471 ];
2472
2473 if ($value === '' && !in_array($field, $clearable_fields)) {
2474 continue;
2475 }
2476
2477 if ($field === 'white_label_plugin_name') {
2478 if (strlen($value) > 16) {
2479 $validation_errors[$field] = 'Plugin name must not exceed 16 characters';
2480 continue;
2481 }
2482 }
2483
2484 if ($field === 'white_label_plugin_menu_icon') {
2485
2486 if ($value === '') {
2487 $metasync_options['general'][$field] = '';
2488 } elseif (filter_var($value, FILTER_VALIDATE_URL)) {
2489
2490 $image_extensions = ['png', 'svg'];
2491
2492 $path = parse_url($value, PHP_URL_PATH);
2493 $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
2494
2495 if (in_array($extension, $image_extensions, true)) {
2496 $metasync_options['general'][$field] = esc_url_raw($value);
2497 } else {
2498 $validation_errors[] = 'Invalid Menu icon format. Only PNG and SVG are allowed.';
2499 }
2500 } else {
2501 $validation_errors[] = 'Invalid Menu icon URL format.';
2502 }
2503 } else {
2504 $metasync_options['general'][$field] = sanitize_text_field($_POST['metasync_options']['general'][$field]);
2505 }
2506 }
2507 }
2508
2509 $textarea_fields = ['otto_bot_whitelist', 'otto_bot_blacklist'];
2510 if ($general_tab_submitted) {
2511 foreach ($textarea_fields as $field) {
2512 if (isset($_POST['metasync_options']['general'][$field])) {
2513 $metasync_options['general'][$field] = sanitize_textarea_field($_POST['metasync_options']['general'][$field]);
2514 } else {
2515 $metasync_options['general'][$field] = '';
2516 }
2517 }
2518 }
2519
2520 if ($general_tab_submitted) {
2521 foreach ($bool_fields as $field) {
2522 if (isset($_POST['metasync_options']['general'][$field])) {
2523 $metasync_options['general'][$field] = filter_var($_POST['metasync_options']['general'][$field], FILTER_VALIDATE_BOOLEAN);
2524 }else {
2525 $metasync_options['general'][$field] = false;
2526 }
2527 }
2528 }
2529
2530 if ($general_tab_submitted || $whitelabel_tab_submitted) {
2531 foreach ($url_fields as $field) {
2532 if (isset($_POST['metasync_options']['general'][$field])) {
2533
2534 $value = trim($_POST['metasync_options']['general'][$field]);
2535
2536 $whitelabel_clearable_url_fields = [
2537 'white_label_plugin_author_uri',
2538 'white_label_plugin_uri'
2539 ];
2540
2541 if ($value === '' && !in_array($field, $whitelabel_clearable_url_fields)) {
2542 continue;
2543 }
2544
2545 if ($value === '' && in_array($field, $whitelabel_clearable_url_fields)) {
2546 $metasync_options['general'][$field] = '';
2547 continue;
2548 }
2549
2550 if (filter_var($value, FILTER_VALIDATE_URL)) {
2551
2552 $parsed_url = parse_url($value);
2553 $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
2554
2555 if (strpos($host, '.') !== false || filter_var($host, FILTER_VALIDATE_IP)) {
2556 $metasync_options['general'][$field] = esc_url_raw($value);
2557 } else {
2558 $field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
2559 $validation_errors[] = 'Invalid ' . $field_name . ' format. Please use a proper domain name (e.g., example.com).';
2560 }
2561 } else {
2562 $field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
2563 $validation_errors[] = 'Invalid ' . $field_name . ' format.';
2564 }
2565 }
2566 }
2567 }
2568
2569 if ($general_tab_submitted) {
2570 if (isset($_POST['metasync_options']['general']['content_genius_sync_roles']) && is_array($_POST['metasync_options']['general']['content_genius_sync_roles'])) {
2571 $metasync_options['general']['content_genius_sync_roles'] = array_map('sanitize_text_field', $_POST['metasync_options']['general']['content_genius_sync_roles']);
2572 } else {
2573 $metasync_options['general']['content_genius_sync_roles'] = array();
2574 }
2575 }
2576
2577 if ($general_tab_submitted) {
2578 $breadcrumbs_input = isset($_POST['metasync_options']['breadcrumbs']) && is_array($_POST['metasync_options']['breadcrumbs'])
2579 ? $_POST['metasync_options']['breadcrumbs']
2580 : array();
2581
2582 if (!isset($metasync_options['breadcrumbs']) || !is_array($metasync_options['breadcrumbs'])) {
2583 $metasync_options['breadcrumbs'] = array();
2584 }
2585
2586 $metasync_options['breadcrumbs']['enabled'] = !empty($breadcrumbs_input['enabled']);
2587 $metasync_options['breadcrumbs']['disable_schema'] = !empty($breadcrumbs_input['disable_schema']);
2588 $metasync_options['breadcrumbs']['show_current_page'] = !empty($breadcrumbs_input['show_current_page']);
2589 $metasync_options['breadcrumbs']['separator'] = isset($breadcrumbs_input['separator']) ? sanitize_text_field($breadcrumbs_input['separator']) : '»';
2590 $metasync_options['breadcrumbs']['home_label'] = isset($breadcrumbs_input['home_label']) ? sanitize_text_field($breadcrumbs_input['home_label']) : 'Home';
2591 $metasync_options['breadcrumbs']['home_url'] = isset($breadcrumbs_input['home_url']) ? esc_url_raw($breadcrumbs_input['home_url']) : '';
2592 $metasync_options['breadcrumbs']['prefix_text'] = isset($breadcrumbs_input['prefix_text']) ? sanitize_text_field($breadcrumbs_input['prefix_text']) : '';
2593 $metasync_options['breadcrumbs']['archive_label_format'] = isset($breadcrumbs_input['archive_label_format']) ? sanitize_text_field($breadcrumbs_input['archive_label_format']) : '{name}';
2594 }
2595
2596 // Open Graph / article toggle checkboxes live under common_meta_settings.
2597 // Unchecked HTML checkboxes are absent from $_POST, so we explicitly
2598 // write 'false' for any missing toggle to persist opt-outs.
2599 if ($general_tab_submitted) {
2600 $og_toggle_fields = [
2601 'open_graph_meta_tags',
2602 'facebook_meta_tags',
2603 'twitter_meta_tags',
2604 'og_image_dimensions',
2605 'article_timestamps',
2606 'article_author',
2607 'article_section',
2608 'article_tags',
2609 'twitter_image_alt',
2610 ];
2611 if (!isset($metasync_options['common_meta_settings']) || !is_array($metasync_options['common_meta_settings'])) {
2612 $metasync_options['common_meta_settings'] = [];
2613 }
2614 foreach ($og_toggle_fields as $field) {
2615 $posted = isset($_POST['metasync_options']['common_meta_settings'][$field])
2616 ? sanitize_text_field($_POST['metasync_options']['common_meta_settings'][$field])
2617 : '';
2618 $metasync_options['common_meta_settings'][$field] = ($posted === 'true') ? 'true' : 'false';
2619 }
2620 }
2621
2622 if ($general_tab_submitted && isset($_POST['metasync_options']['general']['default_page_builder'])) {
2623 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
2624 $available_builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
2625 $builder_value = sanitize_text_field($_POST['metasync_options']['general']['default_page_builder']);
2626 if (isset($available_builders[$builder_value]) && $available_builders[$builder_value]['detected']) {
2627 $metasync_options['general']['default_page_builder'] = $builder_value;
2628 } else {
2629 $metasync_options['general']['default_page_builder'] = 'gutenberg';
2630 if (isset($available_builders[$builder_value]) && !$available_builders[$builder_value]['detected']) {
2631 $validation_errors[] = sprintf(__('Builder "%s" is not detected on this site. Reverted to Gutenberg.', 'metasync'), $available_builders[$builder_value]['label']);
2632 }
2633 }
2634 }
2635
2636 if ($whitelabel_tab_submitted && isset($_POST['metasync_options']['whitelabel'])) {
2637 $gp = isset($_POST['metasync_options']['general']) && is_array($_POST['metasync_options']['general']) ? $_POST['metasync_options']['general'] : [];
2638 $wp = isset($_POST['metasync_options']['whitelabel']) && is_array($_POST['metasync_options']['whitelabel']) ? $_POST['metasync_options']['whitelabel'] : [];
2639 $plugin_name = isset($gp['white_label_plugin_name']) ? trim((string) $gp['white_label_plugin_name']) : '';
2640 $logo_light = isset($wp['logo_light']) ? trim((string) $wp['logo_light']) : '';
2641 $logo_dark = isset($wp['logo_dark']) ? trim((string) $wp['logo_dark']) : '';
2642 $author = isset($gp['white_label_plugin_author']) ? trim((string) $gp['white_label_plugin_author']) : '';
2643 $author_uri = isset($gp['white_label_plugin_author_uri']) ? trim((string) $gp['white_label_plugin_author_uri']) : '';
2644 $plugin_uri = isset($gp['white_label_plugin_uri']) ? trim((string) $gp['white_label_plugin_uri']) : '';
2645 $domain = isset($wp['domain']) ? trim((string) $wp['domain']) : '';
2646 $description = isset($gp['white_label_plugin_description']) ? trim((string) $gp['white_label_plugin_description']) : '';
2647
2648 $has_core_field = (!empty($plugin_name) || (!empty($logo_light) && filter_var($logo_light, FILTER_VALIDATE_URL)) || (!empty($logo_dark) && filter_var($logo_dark, FILTER_VALIDATE_URL)) || !empty($author));
2649 $has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
2650
2651 if ($has_optional_only && !$has_core_field) {
2652 $validation_errors[] = 'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.';
2653 }
2654 }
2655
2656 if (!empty($validation_errors)) {
2657 wp_send_json_error([
2658 'errors' => $validation_errors
2659 ]);
2660 return;
2661 }
2662
2663 $old_options = Metasync::get_option('general') ?? [];
2664 $old_api_key = $old_options['searchatlas_api_key'] ?? '';
2665
2666 $api_key_field_present = isset( $_POST['metasync_options']['general']['searchatlas_api_key'] );
2667 $api_key_validated = null; // null = not checked, true = valid, false = rejected
2668 $is_connected = null;
2669 $api_key_warning = null;
2670 $proposed_new_api_key = $metasync_options['general']['searchatlas_api_key'] ?? '';
2671
2672 // WP-308: When the API key changes, save it first, then POST heartbeat to register
2673 // the WPWebsiteHeartbeat row on the backend, then GET ping to verify.
2674 // The old flow (WP-239) did GET ping first, which failed because the row didn't exist yet.
2675 if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
2676 // Step 1: Save the new key so SyncCustomerParams can use it
2677 Metasync::set_option($metasync_options);
2678
2679 // Step 2: POST heartbeat to register the site (creates WPWebsiteHeartbeat row)
2680 $sync_request = new Metasync_Sync_Requests();
2681 $sync_response = $sync_request->SyncCustomerParams();
2682 $sync_status = Metasync_Sync_Requests::get_response_code($sync_response);
2683
2684 if ( $sync_status == 200 ) {
2685 // Step 3: GET ping to verify registration succeeded
2686 $ping = Metasync_Heartbeat_Manager::instance()->validate_api_key( $proposed_new_api_key );
2687
2688 if ( $ping['connected'] === true ) {
2689 $api_key_validated = true;
2690 $is_connected = true;
2691 if ( ! empty( $ping['otto_pixel_uuid'] ) ) {
2692 $existing_uuid = $metasync_options['general']['otto_pixel_uuid'] ?? '';
2693 if ( empty( $existing_uuid ) || $existing_uuid !== $ping['otto_pixel_uuid'] ) {
2694 $metasync_options['general']['otto_pixel_uuid'] = sanitize_text_field( $ping['otto_pixel_uuid'] );
2695 Metasync::set_option($metasync_options);
2696 }
2697 }
2698 } else {
2699 // POST succeeded but ping didn't confirm — keep the key, warn user
2700 $api_key_validated = true;
2701 $is_connected = true;
2702 $api_key_warning = 'API key saved and heartbeat sent. Connection verification will complete on the next heartbeat.';
2703 }
2704 } elseif ( is_wp_error($sync_response) ) {
2705 // Network error during POST — keep the key, warn user
2706 $api_key_warning = 'Could not register with the server (network error). The key was saved — it will be verified on the next heartbeat.';
2707 } else {
2708 // POST returned non-200 — the key is likely invalid, revert
2709 $metasync_options['general']['searchatlas_api_key'] = $old_api_key;
2710 Metasync::set_option($metasync_options);
2711 $api_key_validated = false;
2712 $is_connected = false;
2713 $api_key_warning = empty( $old_api_key )
2714 ? 'The API key could not be verified. Please check it and try again.'
2715 : 'The API key could not be verified. Your API key has been reverted to the previously saved key.';
2716 }
2717 } else {
2718 // Key unchanged — just save options
2719 Metasync::set_option($metasync_options);
2720 }
2721
2722 // Update connection state immediately when validated
2723 if ( $api_key_validated === true ) {
2724 update_option( 'metasync_last_known_connection_state', true );
2725 set_transient( 'metasync_heartbeat_status_cache', [
2726 'status' => true,
2727 'timestamp' => time(),
2728 'cached_until' => time() + 300,
2729 'updated_by' => 'manual_save_ping_connected',
2730 ], 300 );
2731 }
2732
2733 $data = Metasync::get_option('general');
2734 $new_api_key = $data['searchatlas_api_key'] ?? '';
2735
2736 $api_key_changed = $old_api_key !== $new_api_key;
2737 $api_key_added = empty($old_api_key) && !empty($new_api_key);
2738 $api_key_removed = !empty($old_api_key) && empty($new_api_key);
2739
2740 if ( $api_key_validated === true ) {
2741 # Use current_time('mysql') so the value is consistent with the
2742 # cron heartbeat writer and renders correctly via time_elapsed_string.
2743 $send_auth_token_timestamp = Metasync::get_option();
2744 $send_auth_token_timestamp['general']['send_auth_token_timestamp'] = current_time('mysql');
2745 Metasync::set_option($send_auth_token_timestamp);
2746
2747 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
2748 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - validated via ping');
2749 } else {
2750 // Key unchanged or removed — run sync as before
2751 $sync_request = new Metasync_Sync_Requests();
2752 $response = $sync_request->SyncCustomerParams();
2753 $responseCode = Metasync_Sync_Requests::get_response_code($response);
2754
2755 if ($responseCode == 200) {
2756 # Use current_time('mysql') for consistency with cron heartbeat.
2757 $send_auth_token_timestamp = Metasync::get_option();
2758 $send_auth_token_timestamp['general']['send_auth_token_timestamp'] = current_time('mysql');
2759 Metasync::set_option($send_auth_token_timestamp);
2760
2761 if ($api_key_added) {
2762 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
2763 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - new key added');
2764 } elseif ($api_key_removed) {
2765 Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
2766 delete_transient('metasync_heartbeat_status_cache');
2767 } elseif ($api_key_changed && !empty($new_api_key)) {
2768 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - key changed');
2769 }
2770 }
2771 }
2772
2773 if (isset($_POST['metasync_options']['whitelabel'])) {
2774
2775 $whitelabel_data = $_POST['metasync_options']['whitelabel'];
2776 $existing_whitelabel = $metasync_options['whitelabel'] ?? [];
2777
2778 if (isset($whitelabel_data['logo'])) {
2779 $logo_value = trim($whitelabel_data['logo']);
2780
2781 if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
2782 $metasync_options['whitelabel']['logo'] = esc_url_raw($logo_value);
2783 } else {
2784 $metasync_options['whitelabel']['logo'] = '';
2785 }
2786 }
2787
2788 if (isset($whitelabel_data['logo_light'])) {
2789 $logo_light_value = trim($whitelabel_data['logo_light']);
2790
2791 if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
2792 $metasync_options['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
2793 } else {
2794 $metasync_options['whitelabel']['logo_light'] = '';
2795 }
2796 }
2797
2798 if (isset($whitelabel_data['logo_dark'])) {
2799 $logo_dark_value = trim($whitelabel_data['logo_dark']);
2800
2801 if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
2802 $metasync_options['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
2803 } else {
2804 $metasync_options['whitelabel']['logo_dark'] = '';
2805 }
2806 }
2807
2808 if (isset($whitelabel_data['domain'])) {
2809 $domain_value = trim($whitelabel_data['domain']);
2810 if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
2811 $metasync_options['whitelabel']['domain'] = esc_url_raw($domain_value);
2812 } else {
2813 $metasync_options['whitelabel']['domain'] = '';
2814 }
2815 }
2816
2817 if (isset($whitelabel_data['settings_password'])) {
2818 $password_value = trim($whitelabel_data['settings_password']);
2819
2820 $hide_settings_enabled = isset($whitelabel_data['hide_settings']) && $whitelabel_data['hide_settings'] == '1';
2821
2822 if ($hide_settings_enabled && empty($password_value)) {
2823 if (isset($existing_whitelabel['settings_password'])) {
2824 $metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2825 }
2826 } else {
2827 $metasync_options['whitelabel']['settings_password'] = sanitize_text_field($password_value);
2828 }
2829 } else {
2830 if (isset($existing_whitelabel['settings_password'])) {
2831 $metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2832 }
2833 }
2834
2835 if (isset($whitelabel_data['hide_settings'])) {
2836 $metasync_options['whitelabel']['hide_settings'] = filter_var($whitelabel_data['hide_settings'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
2837 } else {
2838 $metasync_options['whitelabel']['hide_settings'] = 0;
2839 }
2840
2841 if (isset($whitelabel_data['recovery_email'])) {
2842 $recovery_email = trim($whitelabel_data['recovery_email']);
2843 if (!empty($recovery_email)) {
2844 $sanitized_email = sanitize_email($recovery_email);
2845 if (is_email($sanitized_email)) {
2846 $metasync_options['whitelabel']['recovery_email'] = $sanitized_email;
2847 }
2848 } else {
2849 $metasync_options['whitelabel']['recovery_email'] = '';
2850 }
2851 } else {
2852 if (isset($existing_whitelabel['recovery_email'])) {
2853 $metasync_options['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
2854 }
2855 }
2856
2857 $has_password = !empty($metasync_options['whitelabel']['settings_password']);
2858 $has_recovery_email = !empty($metasync_options['whitelabel']['recovery_email']);
2859 if ($has_password && !$has_recovery_email) {
2860 $metasync_options['whitelabel']['settings_password'] = '';
2861 }
2862
2863 if (isset($whitelabel_data['access_control'])) {
2864 $metasync_options['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($whitelabel_data['access_control']);
2865 }
2866
2867 if (!empty($metasync_options['whitelabel']['hide_settings']) && empty($metasync_options['whitelabel']['settings_password'])) {
2868 $metasync_options['whitelabel']['hide_settings'] = 0;
2869 }
2870
2871 // Sanitize color palette
2872 if (isset($whitelabel_data['color_palette']) && is_array($whitelabel_data['color_palette'])) {
2873 $allowed_vars = array(
2874 'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
2875 'dashboard-text-primary', 'dashboard-text-secondary',
2876 'dashboard-accent', 'dashboard-accent-hover',
2877 'dashboard-success', 'dashboard-warning', 'dashboard-error',
2878 'dashboard-border',
2879 'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
2880 'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
2881 );
2882 $sanitized_palette = array();
2883 foreach (array('dark', 'light') as $theme_key) {
2884 if (isset($whitelabel_data['color_palette'][$theme_key]) && is_array($whitelabel_data['color_palette'][$theme_key])) {
2885 $sanitized_palette[$theme_key] = array();
2886 foreach ($whitelabel_data['color_palette'][$theme_key] as $var_name => $color_value) {
2887 if (in_array($var_name, $allowed_vars, true)) {
2888 $color_value = trim(sanitize_text_field($color_value));
2889 if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
2890 $sanitized_palette[$theme_key][$var_name] = $color_value;
2891 }
2892 }
2893 }
2894 }
2895 }
2896 $metasync_options['whitelabel']['color_palette'] = $sanitized_palette;
2897 } else {
2898 if (isset($existing_whitelabel['color_palette'])) {
2899 $metasync_options['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
2900 }
2901 }
2902
2903 $metasync_options['whitelabel']['updated_at'] = time();
2904
2905 Metasync::set_option($metasync_options);
2906
2907 }
2908
2909 $redirect_url = isset($_GET['tab']) ?
2910 admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings&tab='.$_GET['tab']) :
2911 admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings');
2912
2913 // Also save edge cache settings if present in the payload (section renders inside this form)
2914 if (class_exists('Metasync_Edge_Cache_Settings')) {
2915 Metasync_Edge_Cache_Settings::save_from_post();
2916 }
2917
2918 if ($general_tab_submitted && isset($_POST['metasync_llms_txt_settings'])) {
2919 $llms_input = wp_unslash($_POST['metasync_llms_txt_settings']);
2920 if (is_array($llms_input)) {
2921 update_option('metasync_llms_txt_settings', $this->sanitize_llms_txt_settings($llms_input));
2922 }
2923 }
2924
2925 $response_payload = array(
2926 'message' => 'Settings saved successfully!',
2927 'redirect_url' => $redirect_url,
2928 );
2929
2930 if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
2931 if ( $api_key_validated !== null ) {
2932 $response_payload['api_key_validated'] = $api_key_validated;
2933 $response_payload['is_connected'] = $is_connected;
2934 }
2935 if ( $api_key_warning !== null ) {
2936 $response_payload['warning'] = $api_key_warning;
2937 }
2938 if ( $api_key_validated === false ) {
2939 $response_payload['previous_api_key'] = $old_api_key;
2940 }
2941 } elseif ( $api_key_field_present && $proposed_new_api_key === '' && $old_api_key !== '' ) {
2942 $response_payload['api_key_removed'] = true;
2943 $response_payload['warning'] = 'The API key has been removed. The plugin will no longer sync with the server.';
2944 }
2945
2946 delete_transient('metasync_otto_js_detected');
2947
2948 wp_send_json_success( $response_payload );
2949 }
2950
2951 /**
2952 * AJAX handler for saving execution settings.
2953 *
2954 * Moved from Metasync_Admin – the admin class now delegates here.
2955 */
2956 public function ajax_save_execution_settings()
2957 {
2958 if (!isset($_POST['execution_settings_nonce']) || !wp_verify_nonce($_POST['execution_settings_nonce'], 'metasync_execution_settings_nonce')) {
2959 wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
2960 return;
2961 }
2962
2963 if (!Metasync::current_user_has_plugin_access()) {
2964 wp_send_json_error(array('message' => 'Insufficient permissions to save settings.'));
2965 return;
2966 }
2967
2968 $existing_settings = get_option('metasync_execution_settings', array());
2969
2970 $settings = array(
2971 'max_execution_time' => isset($_POST['max_execution_time']) ? absint($_POST['max_execution_time']) : 30,
2972 'max_memory_limit' => isset($_POST['max_memory_limit']) ? absint($_POST['max_memory_limit']) : 256,
2973 'log_batch_size' => isset($_POST['log_batch_size']) ? absint($_POST['log_batch_size']) : 1000,
2974 'action_scheduler_batches' => isset($_POST['action_scheduler_batches']) ? absint($_POST['action_scheduler_batches']) : 1,
2975 'otto_rate_limit' => isset($_POST['otto_rate_limit']) ? absint($_POST['otto_rate_limit']) : 10,
2976 'otto_cache_ttl' => isset($existing_settings['otto_cache_ttl']) ? absint($existing_settings['otto_cache_ttl']) : 30,
2977 'queue_cleanup_days' => isset($_POST['queue_cleanup_days']) ? absint($_POST['queue_cleanup_days']) : 31
2978 );
2979
2980 $server_limits = Metasync_Settings_Fields::instance()->get_server_limits();
2981
2982 if ($settings['max_execution_time'] < 1 || $settings['max_execution_time'] > 300) {
2983 wp_send_json_error(array('message' => 'Max Execution Time must be between 1 and 300 seconds.'));
2984 return;
2985 }
2986
2987 if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) {
2988 wp_send_json_error(array('message' => sprintf('Max Execution Time exceeds server limit of %d seconds. Please reduce the value.', (int)$server_limits['max_execution_time_raw'])));
2989 return;
2990 }
2991
2992 $can_change_memory = Metasync_Settings_Fields::instance()->can_change_memory_limit();
2993 if ($can_change_memory && ($settings['max_memory_limit'] < 64 || $settings['max_memory_limit'] > 512)) {
2994 wp_send_json_error(array('message' => 'Max Memory Limit must be between 64 and 512 MB.'));
2995 return;
2996 }
2997
2998 if ($can_change_memory && $server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
2999 wp_send_json_error(array('message' => sprintf('Max Memory Limit exceeds server limit of %d MB. Please reduce the value.', $server_limits['memory_limit_raw'])));
3000 return;
3001 }
3002
3003 if (!$can_change_memory) {
3004 $settings['max_memory_limit'] = isset($existing_settings['max_memory_limit'])
3005 ? $existing_settings['max_memory_limit']
3006 : 256;
3007 }
3008
3009 if ($settings['log_batch_size'] < 100 || $settings['log_batch_size'] > 5000) {
3010 wp_send_json_error(array('message' => 'Log Batch Size must be between 100 and 5000 lines.'));
3011 return;
3012 }
3013
3014 if ($settings['action_scheduler_batches'] < 1 || $settings['action_scheduler_batches'] > 10) {
3015 wp_send_json_error(array('message' => 'Action Scheduler Concurrent Batches must be between 1 and 10.'));
3016 return;
3017 }
3018
3019 if ($settings['otto_rate_limit'] < 1 || $settings['otto_rate_limit'] > 60) {
3020 wp_send_json_error(array('message' => 'OTTO API Calls Per Minute must be between 1 and 60.'));
3021 return;
3022 }
3023
3024 if ($settings['queue_cleanup_days'] < 7 || $settings['queue_cleanup_days'] > 90) {
3025 wp_send_json_error(array('message' => 'Queue Auto-Cleanup Days must be between 7 and 90 days.'));
3026 return;
3027 }
3028
3029 $saved = update_option('metasync_execution_settings', $settings);
3030
3031 if ($saved !== false) {
3032 wp_send_json_success(array(
3033 'message' => 'Execution settings saved successfully!'
3034 ));
3035 } else {
3036 wp_send_json_error(array('message' => 'Failed to save settings. Please try again.'));
3037 }
3038 }
3039
3040 /**
3041 * AJAX handler for saving SEO / Indexation Control settings.
3042 *
3043 * Moved from Metasync_Admin – the admin class now delegates here.
3044 */
3045 public function meta_sync_save_seo_controls()
3046 {
3047 try {
3048 if (!isset($_POST['meta_sync_seo_controls_nonce']) || !wp_verify_nonce($_POST['meta_sync_seo_controls_nonce'], 'meta_sync_seo_controls_nonce')) {
3049 wp_send_json_error(array('message' => 'Invalid nonce'));
3050 return;
3051 }
3052
3053 $current_options = Metasync::get_option();
3054
3055 $original_options = json_decode(json_encode($current_options), true);
3056
3057 if (!isset($current_options['seo_controls'])) {
3058 $current_options['seo_controls'] = [];
3059 }
3060
3061 $seo_control_fields = [
3062 'index_date_archives',
3063 'index_tag_archives',
3064 'index_author_archives',
3065 'index_category_archives',
3066 'index_format_archives',
3067 'override_robots_tags',
3068 'add_nofollow_to_external_links',
3069 'enable_googleinstantindex',
3070 'enable_binginstantindex'
3071 ];
3072
3073 foreach ($seo_control_fields as $field) {
3074 if (isset($_POST['metasync_options']['seo_controls'][$field]) && $_POST['metasync_options']['seo_controls'][$field] === 'true') {
3075 $current_options['seo_controls'][$field] = 'true';
3076 } else {
3077 $current_options['seo_controls'][$field] = 'false';
3078 }
3079 }
3080
3081 $bing_save_result = true;
3082 if (isset($_POST['metasync_bing_api_key_inline'])) {
3083 $bing_save_result = $this->save_bing_inline_settings_ajax();
3084 }
3085
3086 // Save Google Instant Indexing post types
3087 if (isset($_POST['metasync_post_types']) && is_array($_POST['metasync_post_types'])) {
3088 $google_post_types = array_values(array_map('sanitize_title', wp_unslash($_POST['metasync_post_types'])));
3089 $google_settings = get_option('metasync_options_instant_indexing', ['post_types' => []]);
3090 $google_settings['post_types'] = $google_post_types;
3091 update_option('metasync_options_instant_indexing', $google_settings);
3092 }
3093
3094 $options_changed = (json_encode($original_options) !== json_encode($current_options));
3095
3096 $result = Metasync::set_option($current_options);
3097
3098 if (!$result && !$options_changed) {
3099 $result = true;
3100 }
3101
3102 if ($result && $bing_save_result) {
3103 $success_message = 'Indexation Control settings saved successfully!';
3104 if (isset($_POST['metasync_bing_api_key_inline'])) {
3105 $success_message = 'Indexation Control and Bing Instant Indexing settings saved successfully!';
3106 }
3107 wp_send_json_success(array(
3108 'message' => $success_message,
3109 'saved_data' => $current_options['seo_controls']
3110 ));
3111 } else {
3112 $error_message = 'Failed to save Indexation Control settings';
3113 if (!$bing_save_result) {
3114 $error_message = 'Failed to save Bing Instant Indexing settings';
3115 }
3116 wp_send_json_error(array('message' => $error_message));
3117 }
3118 } catch (Exception $e) {
3119 error_log('Indexation Control AJAX Exception: ' . $e->getMessage());
3120 error_log('Indexation Control AJAX Stack trace: ' . $e->getTraceAsString());
3121 wp_send_json_error(array('message' => 'Error saving settings: ' . $e->getMessage()));
3122 }
3123 }
3124
3125 /**
3126 * Save Bing IndexNow inline settings from SEO Controls page.
3127 *
3128 * Moved from Metasync_Admin – the admin class now delegates here.
3129 *
3130 * @return bool True on success, false on failure
3131 */
3132 public function save_bing_inline_settings_ajax()
3133 {
3134 $api_key = isset($_POST['metasync_bing_api_key_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_api_key_inline'])) : '';
3135 $endpoint = isset($_POST['metasync_bing_endpoint_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_endpoint_inline'])) : 'indexnow';
3136 $post_types = isset($_POST['metasync_bing_post_types_inline']) && is_array($_POST['metasync_bing_post_types_inline']) ? array_map('sanitize_title', wp_unslash($_POST['metasync_bing_post_types_inline'])) : [];
3137 $disable_other_plugins = isset($_POST['metasync_bing_disable_other_plugins_inline']) ? true : false;
3138
3139 $existing_settings = get_option('metasync_options_bing_instant_indexing', []);
3140
3141 $new_settings = [
3142 'api_key' => $api_key,
3143 'endpoint' => $endpoint,
3144 'post_types' => array_values($post_types),
3145 'disable_other_plugins' => $disable_other_plugins,
3146 ];
3147
3148 $settings = array_merge($existing_settings, $new_settings);
3149
3150 $settings_changed = (json_encode($existing_settings) !== json_encode($settings));
3151
3152 $result = update_option('metasync_options_bing_instant_indexing', $settings);
3153
3154 if (!$result && !$settings_changed) {
3155 $result = true;
3156 }
3157
3158 if (!empty($api_key)) {
3159 $safe_key = sanitize_file_name( $api_key );
3160 $file_path = ABSPATH . $safe_key . '.txt';
3161 $real_path = realpath( ABSPATH );
3162 if ( false === $real_path || 0 !== strpos( realpath( dirname( $file_path ) ), $real_path ) ) {
3163 error_log( 'Bing IndexNow: Refusing to write outside ABSPATH' );
3164 return false;
3165 }
3166 $file_result = file_put_contents( $file_path, $safe_key );
3167
3168 if ($file_result === false) {
3169 error_log('Bing IndexNow: Failed to create API key verification file at ' . $file_path);
3170 return false;
3171 }
3172
3173 $current_options = Metasync::get_option();
3174 if (!isset($current_options['seo_controls']['enable_binginstantindex']) || $current_options['seo_controls']['enable_binginstantindex'] !== 'true') {
3175 $current_options['seo_controls']['enable_binginstantindex'] = 'true';
3176 Metasync::set_option($current_options);
3177 }
3178 }
3179
3180 return $result;
3181 }
3182
3183 /**
3184 * Sanitize the metasync_llms_txt_settings option.
3185 *
3186 * The Settings API delivers raw form data; we coerce it into a predictable
3187 * shape so the generator and MCP tools can rely on the structure.
3188 *
3189 * @param mixed $input Raw form data.
3190 * @return array
3191 */
3192 public function sanitize_llms_txt_settings($input)
3193 {
3194 if (!is_array($input)) {
3195 $input = array();
3196 }
3197
3198 $existing = get_option('metasync_llms_txt_settings', array());
3199 if (!is_array($existing)) {
3200 $existing = array();
3201 }
3202
3203 $output = $existing;
3204
3205 $output['enabled'] = !empty($input['enabled'])
3206 ? (bool) filter_var($input['enabled'], FILTER_VALIDATE_BOOLEAN)
3207 : false;
3208
3209 if (isset($input['post_types']) && is_array($input['post_types'])) {
3210 $output['post_types'] = array_values(array_filter(array_map('sanitize_text_field', $input['post_types'])));
3211 } else {
3212 $output['post_types'] = array('page', 'post');
3213 }
3214
3215 if (isset($input['max_posts'])) {
3216 $max = absint($input['max_posts']);
3217 if ($max < 1) {
3218 $max = 1;
3219 }
3220 if ($max > 500) {
3221 $max = 500;
3222 }
3223 $output['max_posts'] = $max;
3224 }
3225
3226 if (isset($input['excluded_ids'])) {
3227 if (is_array($input['excluded_ids'])) {
3228 $ids = $input['excluded_ids'];
3229 } else {
3230 $ids = preg_split('/[\s,]+/', (string) $input['excluded_ids']);
3231 }
3232 $output['excluded_ids'] = array_values(array_filter(array_map('absint', (array) $ids)));
3233 }
3234
3235 if (isset($input['custom_description'])) {
3236 $output['custom_description'] = sanitize_text_field((string) $input['custom_description']);
3237 }
3238
3239 $output['llms_full_enabled'] = !empty($input['llms_full_enabled'])
3240 ? (bool) filter_var($input['llms_full_enabled'], FILTER_VALIDATE_BOOLEAN)
3241 : false;
3242
3243 if (isset($input['max_posts_full'])) {
3244 $max_full = absint($input['max_posts_full']);
3245 if ($max_full < 1) {
3246 $max_full = 1;
3247 }
3248 if ($max_full > 500) {
3249 $max_full = 500;
3250 }
3251 $output['max_posts_full'] = $max_full;
3252 }
3253
3254 return $output;
3255 }
3256 }
3257