PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.10
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.10
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 2.6.4 2.6.3 2.5.23 All 138 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.10, at includes/class-metasync-settings-registration.php

3,239 lines 161.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 'open_external_links',
885 'Open External Links in New Tab/Window',
886 function() use ($option_key) {
887 $enabled = Metasync::get_option('general')['open_external_links'] ?? false;
888 printf(
889 '<input type="checkbox" id="open_external_links" name="' . $option_key . '[general][open_external_links]" value="1" %s />',
890 $enabled ? 'checked' : ''
891 );
892 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>');
893 },
894 $page_slug . '_general',
895 $SECTION_METASYNC
896 );
897
898 add_settings_field(
899 'permalink_structure',
900 'The Permalink setting of website',
901 function() {
902
903 $current_permalink_structure = get_option('permalink_structure');
904 $current_rewrite_rules = get_option('rewrite_rules');
905 // Check if the current permalink structure is set to "Plain"
906 if (($current_permalink_structure == '/%post_id%/' || $current_permalink_structure == '') && $current_rewrite_rules == '') {
907 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>');
908 } else {
909 printf('<span class="description" style="color:#008000;opacity:1;">Permalink is Okay </span>');
910 }
911 },
912 $page_slug . '_general',
913 $SECTION_METASYNC
914 );
915
916 # Add Hide Dashboard Framework setting
917 add_settings_field(
918 'hide_dashboard_framework',
919 'Hide Dashboard',
920 function() use ($option_key) {
921 $hide_dashboard = Metasync::get_option('general')['hide_dashboard_framework'] ?? '';
922 printf(
923 '<input type="checkbox" id="hide_dashboard_framework" name="' . $option_key . '[general][hide_dashboard_framework]" value="true" %s />',
924 isset($hide_dashboard) && $hide_dashboard == 'true' ? 'checked' : ''
925 );
926 printf('<span class="description"> Hide the %s dashboard</span>', esc_html(Metasync_Settings_Registration::instance()->get_effective_menu_title()));
927 },
928 $page_slug . '_general',
929 $SECTION_METASYNC
930 );
931
932 // REMOVED (CVE-2025-14386): disable_single_signup_login setting was removed
933
934 # Adding the "Show Admin Bar Status" setting
935 add_settings_field(
936 'show_admin_bar_status',
937 'Show ' . $this->get_effective_menu_title() . ' Status in Admin Bar',
938 function() use ($option_key) {
939 $show_admin_bar = Metasync::get_option('general')['show_admin_bar_status'] ?? true;
940 printf(
941 '<input type="checkbox" id="show_admin_bar_status" name="' . $option_key . '[general][show_admin_bar_status]" value="true" %s />',
942 $show_admin_bar ? 'checked' : ''
943 );
944 printf('<span class="description">Show the %s status indicator in the WordPress admin bar.</span>', esc_html(Metasync::get_effective_plugin_name()));
945 },
946 $page_slug . '_general',
947 $SECTION_METASYNC
948 );
949
950 # Adding the "Enable Auto-updates" setting
951 add_settings_field(
952 'enable_auto_updates',
953 'Enable Automatic Updates',
954 function() use ($option_key) {
955 $enable_auto_updates = Metasync::get_option('general')['enable_auto_updates'] ?? false;
956 printf(
957 '<input type="checkbox" id="enable_auto_updates" name="' . $option_key . '[general][enable_auto_updates]" value="true" %s />',
958 $enable_auto_updates ? 'checked' : ''
959 );
960 printf('<span class="description">Allow WordPress to automatically update this plugin when new versions are available.</span>');
961 },
962 $page_slug . '_general',
963 $SECTION_METASYNC
964 );
965
966 # Add User Role Sync Setting
967 add_settings_field(
968 'content_genius_sync_roles',
969 'Content Genius User Roles to Sync',
970 function() use ($option_key) {
971 $selected_roles = Metasync::get_option('general')['content_genius_sync_roles'] ?? array();
972
973 # If it's a string (single role from old version), convert to array
974 if (!is_array($selected_roles)) {
975 $selected_roles = array($selected_roles);
976 }
977
978 # Get all WordPress user roles safely
979 if (!function_exists('wp_roles')) {
980 require_once(ABSPATH . 'wp-admin/includes/user.php');
981 }
982 global $wp_roles;
983
984 # Safety check for $wp_roles
985 if (!isset($wp_roles) || !is_object($wp_roles)) {
986 $wp_roles = new WP_Roles();
987 }
988
989 $all_roles = $wp_roles->roles;
990
991 # Start container
992 ?>
993 <div class="metasync-role-selector-container">
994
995 <!-- All Roles Option -->
996 <label class="metasync-role-option-all">
997 <input type="checkbox"
998 name="<?php echo esc_attr($option_key); ?>[general][content_genius_sync_roles][]"
999 value="all"
1000 <?php checked(empty($selected_roles) || in_array('all', $selected_roles), true); ?> />
1001 <strong>All Roles</strong>
1002 </label>
1003
1004 <!-- Divider -->
1005 <hr class="metasync-role-divider">
1006
1007 <?php
1008 # Display each role as a checkbox
1009 if (!empty($all_roles) && is_array($all_roles)) {
1010 foreach ($all_roles as $role_key => $role_details) {
1011 # Safety check for role data
1012 if (!isset($role_details['name'])) {
1013 continue;
1014 }
1015
1016 $is_checked = in_array($role_key, $selected_roles);
1017 $role_name = translate_user_role($role_details['name']);
1018 ?>
1019 <label class="metasync-role-option">
1020 <input type="checkbox"
1021 name="<?php echo esc_attr($option_key); ?>[general][content_genius_sync_roles][]"
1022 value="<?php echo esc_attr($role_key); ?>"
1023 <?php checked($is_checked, true); ?> />
1024 <span class="metasync-role-label"><?php echo esc_html($role_name); ?></span>
1025 </label>
1026 <?php
1027 }
1028 }
1029 ?>
1030
1031 </div>
1032
1033 <!-- Description -->
1034 <p class="description" style="margin-top: 10px;">
1035 <strong>How it works:</strong>
1036 Select which user roles should be synced with Content Genius.
1037 If <strong>"All Roles"</strong> is selected or none are selected, all users will be synced.
1038 Otherwise, only users with the selected roles will be included in the sync.
1039 </p>
1040 <?php
1041 },
1042 $page_slug . '_general',
1043 $SECTION_METASYNC
1044 );
1045
1046 # Default Page Builder setting
1047 add_settings_field(
1048 'default_page_builder',
1049 'Default Page Builder',
1050 function() use ($option_key) {
1051 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
1052 $current = Metasync::get_option('general')['default_page_builder'] ?? '';
1053 $builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
1054 $auto_detected = Metasync_HTML_To_Builder_Converter::auto_detect_builder();
1055
1056 echo '<div class="metasync-page-builder-setting">';
1057 echo '<style>#default_page_builder:focus { color: #fff; }</style>';
1058 echo '<select id="default_page_builder" name="' . esc_attr($option_key) . '[general][default_page_builder]" style="min-width: 300px;">';
1059
1060 foreach ($builders as $key => $builder) {
1061 $is_detected = $builder['detected'];
1062 $label = esc_html($builder['label']);
1063 if ($is_detected && $key !== 'gutenberg') {
1064 $label .= ' — Detected';
1065 } elseif (!$is_detected) {
1066 $label .= ' — Not Detected';
1067 }
1068 if ($key === 'gutenberg' && empty($current)) {
1069 $label .= ' — Default';
1070 }
1071 printf(
1072 '<option value="%s" %s%s>%s</option>',
1073 esc_attr($key),
1074 selected(empty($current) ? 'gutenberg' : $current, $key, false),
1075 $is_detected ? '' : ' disabled',
1076 $label
1077 );
1078 }
1079
1080 echo '</select>';
1081 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>';
1082
1083 # Show current auto-detection result
1084 if (!empty($builders[$auto_detected]) && $auto_detected !== 'gutenberg') {
1085 printf(
1086 '<p class="description" style="margin-top: 8px; color: #0073aa;">🔍 Auto-detected: <strong>%s</strong> is active on this site.</p>',
1087 esc_html($builders[$auto_detected]['label'])
1088 );
1089 }
1090
1091 # Show description for the selected builder
1092 echo '<div id="metasync-builder-descriptions" style="margin-top: 10px;">';
1093 foreach ($builders as $key => $builder) {
1094 $is_visible = (empty($current) ? 'gutenberg' : $current) === $key;
1095 printf(
1096 '<p class="description metasync-builder-desc" data-builder="%s" style="%s">%s</p>',
1097 esc_attr($key),
1098 $is_visible ? '' : 'display:none;',
1099 esc_html($builder['description'])
1100 );
1101 }
1102 echo '</div>';
1103
1104 # Inline JS to toggle description on select change
1105 ?>
1106 <script>
1107 document.getElementById('default_page_builder')?.addEventListener('change', function() {
1108 document.querySelectorAll('.metasync-builder-desc').forEach(function(el) {
1109 el.style.display = el.dataset.builder === this.value ? '' : 'none';
1110 }.bind(this));
1111 });
1112 </script>
1113 <?php
1114 },
1115 $page_slug . '_general',
1116 $SECTION_METASYNC
1117 );
1118
1119 add_settings_field(
1120 'import_external_data',
1121 'Import settings and data from SEO Plugins',
1122 function() {
1123 printf(
1124 '<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>',
1125 esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-import-external'))
1126 );
1127 printf('<p class="description">Import settings and data from other SEO plugins (Yoast, Rank Math, AIOSEO, etc).</p>');
1128 },
1129 $page_slug . '_general',
1130 $SECTION_METASYNC
1131 );
1132
1133
1134
1135 /*
1136 add field to save color for elementor and assign custom color to the heading
1137 */
1138 if(is_admin()){
1139 add_settings_field(
1140 'white_label_plugin_name',
1141 'Plugin Name',
1142 function() use ($option_key) {
1143 $value = Metasync::get_option('general')['white_label_plugin_name'] ?? '';
1144 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_name]" value="' . esc_attr($value) . '" maxlength="16" />');
1145 printf('<p class="description">This name will be used for general plugin branding (WordPress menus, page titles, and system messages). Maximum 16 characters.</p>');
1146 },
1147 $page_slug . '_branding',
1148 $SECTION_METASYNC
1149 );
1150
1151 add_settings_field(
1152 'whitelabel_otto_name',
1153 'OTTO Name',
1154 function() use ($option_key) {
1155 $value = Metasync::get_option('general')['whitelabel_otto_name'] ?? '';
1156 printf('<input type="text" name="' . $option_key . '[general][whitelabel_otto_name]" value="' . esc_attr($value) . '" />');
1157 $example_name = !empty($value) ? $value : 'OTTO';
1158 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));
1159 },
1160 $page_slug . '_branding',
1161 $SECTION_METASYNC
1162 );
1163
1164 add_settings_field(
1165 'whitelabel_logo_light_url',
1166 'Logo (Light Theme)',
1167 function() use ($option_key) {
1168 $whitelabel_settings = Metasync::get_whitelabel_settings();
1169 $value = $whitelabel_settings['logo_light'] ?? '';
1170 printf('<input type="url" name="' . $option_key . '[whitelabel][logo_light]" value="' . esc_attr($value) . '" size="60" />');
1171 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()));
1172 },
1173 $page_slug . '_branding',
1174 $SECTION_METASYNC
1175 );
1176
1177 add_settings_field(
1178 'whitelabel_logo_dark_url',
1179 'Logo (Dark Theme)',
1180 function() use ($option_key) {
1181 $whitelabel_settings = Metasync::get_whitelabel_settings();
1182 $value = $whitelabel_settings['logo_dark'] ?? '';
1183 printf('<input type="url" name="' . $option_key . '[whitelabel][logo_dark]" value="' . esc_attr($value) . '" size="60" />');
1184 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()));
1185 },
1186 $page_slug . '_branding',
1187 $SECTION_METASYNC
1188 );
1189
1190 add_settings_field(
1191 'whitelabel_domain_url',
1192 'Dashboard URL',
1193 function() use ($option_key) {
1194 $whitelabel_settings = Metasync::get_whitelabel_settings();
1195 $value = $whitelabel_settings['domain'] ?? '';
1196 printf('<input type="url" name="' . $option_key . '[whitelabel][domain]" value="' . esc_attr($value) . '" size="60" />');
1197 printf('<p class="description">Enter your whitelabel dashboard URL (e.g., https://yourdashboard.com). Used for branding purposes.</p>');
1198 },
1199 $page_slug . '_branding',
1200 $SECTION_METASYNC
1201 );
1202 add_settings_field(
1203 'white_label_plugin_description',
1204 'Plugin Description',
1205 function() use ($option_key) {
1206 $value = Metasync::get_option('general')['white_label_plugin_description'] ?? '';
1207 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_description]" value="' . esc_attr($value) . '" />');
1208 },
1209 $page_slug . '_branding',
1210 $SECTION_METASYNC
1211 );
1212
1213 add_settings_field(
1214 'white_label_plugin_author',
1215 'Author',
1216 function() use ($option_key) {
1217 $value = Metasync::get_option('general')['white_label_plugin_author'] ?? '';
1218 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_author]" value="' . esc_attr($value) . '" />');
1219 },
1220 $page_slug . '_branding',
1221 $SECTION_METASYNC
1222 );
1223
1224 add_settings_field(
1225 'white_label_plugin_author_uri',
1226 'Author URI',
1227 function() use ($option_key) {
1228 $value = Metasync::get_option('general')['white_label_plugin_author_uri'] ?? '';
1229 # printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_author_uri]" value="' . esc_attr($value) . '" />');
1230 # Fixed printf usage
1231 printf('<input type="text" name="%s" value="%s" />', esc_attr($option_key . '[general][white_label_plugin_author_uri]'), esc_attr($value) );
1232
1233 },
1234 $page_slug . '_branding',
1235 $SECTION_METASYNC
1236 );
1237 add_settings_field(
1238 'white_label_plugin_uri',
1239 'Plugin URI',
1240 function() use ($option_key) {
1241 $value = Metasync::get_option('general')['white_label_plugin_uri'] ?? ''; // New option for Plugin URI
1242 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_uri]" value="' . esc_attr($value) . '" />');
1243 },
1244 $page_slug . '_branding',
1245 $SECTION_METASYNC
1246 );
1247 register_setting(Metasync_Admin::option_group,
1248 Metasync_Admin::option_key,
1249 array(self::instance(), 'sanitize')
1250 );
1251
1252 // DEPRECATED: Menu Name and Menu Title fields removed
1253 add_settings_field(
1254 'white_label_plugin_menu_slug',
1255 'Menu Slug',
1256 function() use ($option_key) {
1257 $value = Metasync::get_option('general')['white_label_plugin_menu_slug'] ?? '';
1258 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_menu_slug]" value="' . esc_attr($value) . '" />');
1259 },
1260 $page_slug . '_branding',
1261 $SECTION_METASYNC
1262 );
1263 add_settings_field(
1264 'white_label_plugin_menu_icon',
1265 'Menu Icon',
1266 function() use ($option_key) {
1267 $value = Metasync::get_option('general')['white_label_plugin_menu_icon'] ?? '';
1268 printf('<input type="text" name="' . $option_key . '[general][white_label_plugin_menu_icon]" value="' . esc_attr($value) . '" />');
1269 },
1270 $page_slug . '_branding',
1271 $SECTION_METASYNC
1272 );
1273 }
1274 // HIDDEN: Choose Style Option setting
1275 /*
1276 add_settings_field(
1277 'enabled_plugin_css',
1278 'Choose Style Option',
1279 function() use ($option_key) {
1280 $enabled_plugin_css = Metasync::get_option('general')['enabled_plugin_css'] ?? '';
1281
1282 // Output radio button for Default Style.css active
1283
1284 printf(
1285 '<input type="radio" id="enable_default" name="' . $option_key . '[general][enabled_plugin_css]" value="default" %s />',
1286 ($enabled_plugin_css == 'default'||$enabled_plugin_css =='') ? 'checked' : ''
1287 );
1288 printf('<label for="enable_default">Default</label><br>');
1289
1290
1291 // Output radio button for Metasync Style
1292 printf(
1293 '<input type="radio" id="enable_metasync" name="' . $option_key . '[general][enabled_plugin_css]" value="metasync" %s />',
1294 ($enabled_plugin_css == 'metasync') ? 'checked' : ''
1295 );
1296 printf('<label for="enable_metasync">Metasync Style</label>');
1297
1298 printf('<p class="description"> Choose the default page Style Sheet: Default or MetaSync.</p>');
1299 },
1300 $page_slug . '_branding',
1301 $SECTION_METASYNC
1302 );
1303 */
1304
1305 add_settings_field(
1306 'enabled_elementor_plugin_css',
1307 'Choose Elementor Font Style',
1308 function() use ($option_key) {
1309 $enabled_elementor_plugin_css = Metasync::get_option('general')['enabled_elementor_plugin_css'] ?? 'default';
1310 printf('<select name="' . $option_key . '[general][enabled_elementor_plugin_css]" id="heading_style">');
1311 printf('<option value="default"'.selected($enabled_elementor_plugin_css, 'default', false).'>Default</option>');
1312 printf('<option value="custom" '. selected($enabled_elementor_plugin_css, 'custom', false) . '>Custom</option>');
1313 printf('</select>');
1314 },
1315 $page_slug . '_branding',
1316 $SECTION_METASYNC
1317 );
1318 add_settings_field(
1319 'enabled_elementor_plugin_css_color',
1320 'Choose Elementor Font Color',
1321 function() use ($option_key) {
1322 $enabled_elementor_plugin_css_color = Metasync::get_option('general')['enabled_elementor_plugin_css_color'] ?? '#000000';
1323 printf('<input type="color" id="elementor_default_color_metasync" name="' . $option_key . '[general][enabled_elementor_plugin_css_color]" value="'.$enabled_elementor_plugin_css_color.'">');
1324 },
1325 $page_slug . '_branding',
1326 $SECTION_METASYNC
1327 );
1328
1329 add_settings_field(
1330 'whitelabel_settings_password',
1331 'Settings Password',
1332 function() use ($option_key) {
1333 $whitelabel_settings = Metasync::get_whitelabel_settings();
1334 $value = $whitelabel_settings['settings_password'] ?? '';
1335 printf('<input type="password" name="' . $option_key . '[whitelabel][settings_password]" value="' . esc_attr($value) . '" size="30" autocomplete="new-password" />');
1336 printf('<p class="description">Set a custom password to protect the branding settings section.</p>');
1337 },
1338 $page_slug . '_branding',
1339 $SECTION_METASYNC
1340 );
1341
1342 add_settings_field(
1343 'whitelabel_recovery_email',
1344 'Recovery Email',
1345 function() use ($option_key) {
1346 $whitelabel_settings = Metasync::get_whitelabel_settings();
1347 $value = $whitelabel_settings['recovery_email'] ?? '';
1348 $has_password = !empty($whitelabel_settings['settings_password']);
1349 printf('<input type="email" name="' . $option_key . '[whitelabel][recovery_email]" value="' . esc_attr($value) . '" size="30" autocomplete="email" %s />', $has_password ? 'required' : '');
1350 printf('<p class="description">Email address to receive password recovery. <strong>Required when password is set.</strong></p>');
1351 },
1352 $page_slug . '_branding',
1353 $SECTION_METASYNC
1354 );
1355
1356 add_settings_field(
1357 'whitelabel_quick_links',
1358 'Quick Links',
1359 function() use ($option_key) {
1360 $whitelabel_settings = Metasync::get_whitelabel_settings();
1361 $links = isset($whitelabel_settings['quick_links']) && is_array($whitelabel_settings['quick_links'])
1362 ? $whitelabel_settings['quick_links']
1363 : [];
1364 // Ensure at least 5 rows
1365 while (count($links) < 5) {
1366 $links[] = ['label' => '', 'url' => '', 'external' => false];
1367 }
1368 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>';
1369 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>';
1370 echo '<table class="widefat metasync-quick-links-table" style="max-width:560px;">';
1371 echo '<thead><tr><th>Label</th><th>URL</th><th>Open in new tab</th></tr></thead><tbody>';
1372 foreach (array_slice($links, 0, 5) as $i => $link) {
1373 $label = esc_attr($link['label'] ?? '');
1374 $url = esc_attr($link['url'] ?? '');
1375 $external = !empty($link['external']);
1376 printf(
1377 '<tr>
1378 <td><input type="text" name="%s[whitelabel][quick_links][%d][label]" value="%s" placeholder="Link label" style="width:100%%" /></td>
1379 <td><input type="url" name="%s[whitelabel][quick_links][%d][url]" value="%s" placeholder="https://…" style="width:100%%" /></td>
1380 <td style="text-align:center"><input type="checkbox" name="%s[whitelabel][quick_links][%d][external]" value="1" %s /></td>
1381 </tr>',
1382 esc_attr($option_key), $i, $label,
1383 esc_attr($option_key), $i, $url,
1384 esc_attr($option_key), $i, checked($external, true, false)
1385 );
1386 }
1387 echo '</tbody></table>';
1388 },
1389 $page_slug . '_branding',
1390 $SECTION_METASYNC
1391 );
1392
1393 // ======================================================================
1394 // Schema Markup Settings — dedicated page (SEO → Schema Markup)
1395 // ======================================================================
1396 $schema_page = $page_slug . '_schema-markup';
1397
1398 add_settings_section(
1399 'metasync_schema_org',
1400 'Organization Schema (Site-Wide)',
1401 function() {
1402 echo '<p>This Organization schema is injected on every page of your site.</p>';
1403 },
1404 $schema_page
1405 );
1406
1407 add_settings_field(
1408 'schema_org_name',
1409 'Organization Name',
1410 function() use ($option_key) {
1411 $schema = Metasync::get_option('schema') ?? [];
1412 $value = $schema['org_name'] ?? '';
1413 printf('<input type="text" name="' . $option_key . '[schema][org_name]" value="%s" size="40" placeholder="Your organization name" />', esc_attr($value));
1414 },
1415 $schema_page,
1416 'metasync_schema_org'
1417 );
1418
1419 add_settings_field(
1420 'schema_org_url',
1421 'Organization URL',
1422 function() use ($option_key) {
1423 $schema = Metasync::get_option('schema') ?? [];
1424 $value = $schema['org_url'] ?? '';
1425 printf('<input type="url" name="' . $option_key . '[schema][org_url]" value="%s" size="40" placeholder="https://example.com" />', esc_attr($value));
1426 },
1427 $schema_page,
1428 'metasync_schema_org'
1429 );
1430
1431 add_settings_field(
1432 'schema_org_logo',
1433 'Organization Logo URL',
1434 function() use ($option_key) {
1435 $schema = Metasync::get_option('schema') ?? [];
1436 $value = $schema['org_logo'] ?? '';
1437 printf('<input type="url" name="' . $option_key . '[schema][org_logo]" value="%s" size="40" placeholder="https://example.com/logo.png" />', esc_attr($value));
1438 },
1439 $schema_page,
1440 'metasync_schema_org'
1441 );
1442
1443 add_settings_field(
1444 'schema_org_contact_telephone',
1445 'Contact Telephone',
1446 function() use ($option_key) {
1447 $schema = Metasync::get_option('schema') ?? [];
1448 $value = $schema['org_contact_telephone'] ?? '';
1449 printf('<input type="text" name="' . $option_key . '[schema][org_contact_telephone]" value="%s" size="30" placeholder="+1-555-1234" />', esc_attr($value));
1450 },
1451 $schema_page,
1452 'metasync_schema_org'
1453 );
1454
1455 add_settings_field(
1456 'schema_org_contact_type',
1457 'Contact Type',
1458 function() use ($option_key) {
1459 $schema = Metasync::get_option('schema') ?? [];
1460 $value = $schema['org_contact_type'] ?? '';
1461 printf('<input type="text" name="' . $option_key . '[schema][org_contact_type]" value="%s" size="30" placeholder="customer support" />', esc_attr($value));
1462 },
1463 $schema_page,
1464 'metasync_schema_org'
1465 );
1466
1467 add_settings_field(
1468 'schema_org_same_as',
1469 'Social Profile URLs',
1470 function() use ($option_key) {
1471 $schema = Metasync::get_option('schema') ?? [];
1472 $value = $schema['org_same_as'] ?? '';
1473 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));
1474 echo '<p class="description">Enter one social profile URL per line.</p>';
1475 },
1476 $schema_page,
1477 'metasync_schema_org'
1478 );
1479
1480 add_settings_section(
1481 'metasync_schema_website',
1482 'WebSite Schema (Homepage Only)',
1483 function() {
1484 echo '<p>This WebSite schema is injected on the homepage only. Enables the Google Sitelinks Searchbox.</p>';
1485 },
1486 $schema_page
1487 );
1488
1489 add_settings_field(
1490 'schema_website_name',
1491 'WebSite Name',
1492 function() use ($option_key) {
1493 $schema = Metasync::get_option('schema') ?? [];
1494 $value = $schema['website_name'] ?? '';
1495 printf('<input type="text" name="' . $option_key . '[schema][website_name]" value="%s" size="40" placeholder="Your website name" />', esc_attr($value));
1496 },
1497 $schema_page,
1498 'metasync_schema_website'
1499 );
1500
1501 add_settings_field(
1502 'schema_website_url',
1503 'WebSite URL',
1504 function() use ($option_key) {
1505 $schema = Metasync::get_option('schema') ?? [];
1506 $value = $schema['website_url'] ?? '';
1507 printf('<input type="url" name="' . $option_key . '[schema][website_url]" value="%s" size="40" placeholder="https://example.com" />', esc_attr($value));
1508 },
1509 $schema_page,
1510 'metasync_schema_website'
1511 );
1512
1513 add_settings_field(
1514 'schema_website_searchbox',
1515 'Enable Sitelinks Searchbox',
1516 function() use ($option_key) {
1517 $schema = Metasync::get_option('schema') ?? [];
1518 $value = $schema['website_searchbox'] ?? false;
1519 printf(
1520 '<input type="checkbox" name="' . $option_key . '[schema][website_searchbox]" value="1" %s />',
1521 checked($value, true, false)
1522 );
1523 echo '<span class="description">Adds SearchAction to WebSite schema for Google Sitelinks Searchbox.</span>';
1524 },
1525 $schema_page,
1526 'metasync_schema_website'
1527 );
1528
1529 add_settings_section(
1530 'metasync_schema_compat',
1531 'Compatibility',
1532 function() {
1533 echo '<p>Control how MetaSync interacts with other plugins that output structured data.</p>';
1534 },
1535 $schema_page
1536 );
1537
1538 add_settings_field(
1539 'schema_override_woocommerce',
1540 'Override WooCommerce Product Schema',
1541 function() use ($option_key) {
1542 $schema = Metasync::get_option('schema') ?? [];
1543 $value = $schema['override_woocommerce_schema'] ?? false;
1544 printf(
1545 '<input type="checkbox" name="' . $option_key . '[schema][override_woocommerce_schema]" value="1" %s />',
1546 checked($value, true, false)
1547 );
1548 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>';
1549 },
1550 $schema_page,
1551 'metasync_schema_compat'
1552 );
1553
1554 // ----------------------------------------------------------------
1555 // Site Verification page
1556 // ----------------------------------------------------------------
1557 add_settings_section(
1558 $SECTION_SEARCHENGINE,
1559 '',
1560 function() {},
1561 $page_slug . '_searchengines-verification'
1562 );
1563
1564 add_settings_field(
1565 'google_site_verification',
1566 'Google Search Console',
1567 array( Metasync_Settings_Fields::instance(), 'google_site_verification_callback' ),
1568 $page_slug . '_searchengines-verification',
1569 $SECTION_SEARCHENGINE
1570 );
1571
1572 add_settings_field(
1573 'bing_site_verification',
1574 'Bing Webmaster Tools',
1575 array( Metasync_Settings_Fields::instance(), 'bing_site_verification_callback' ),
1576 $page_slug . '_searchengines-verification',
1577 $SECTION_SEARCHENGINE
1578 );
1579
1580 add_settings_field(
1581 'yandex_site_verification',
1582 'Yandex Webmaster',
1583 array( Metasync_Settings_Fields::instance(), 'yandex_site_verification_callback' ),
1584 $page_slug . '_searchengines-verification',
1585 $SECTION_SEARCHENGINE
1586 );
1587
1588 add_settings_field(
1589 'pinterest_site_verification',
1590 'Pinterest',
1591 array( Metasync_Settings_Fields::instance(), 'pinterest_site_verification_callback' ),
1592 $page_slug . '_searchengines-verification',
1593 $SECTION_SEARCHENGINE
1594 );
1595
1596 add_settings_field(
1597 'baidu_site_verification',
1598 'Baidu',
1599 function() use ($option_key) {
1600 $value = Metasync::get_option('searchengines')['baidu_site_verification'] ?? '';
1601 printf( '<input type="text" id="baidu_site_verification" name="' . $option_key . '[searchengines][baidu_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1602 },
1603 $page_slug . '_searchengines-verification',
1604 $SECTION_SEARCHENGINE
1605 );
1606
1607 add_settings_field(
1608 'alexa_site_verification',
1609 'Alexa',
1610 function() use ($option_key) {
1611 $value = Metasync::get_option('searchengines')['alexa_site_verification'] ?? '';
1612 printf( '<input type="text" id="alexa_site_verification" name="' . $option_key . '[searchengines][alexa_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1613 },
1614 $page_slug . '_searchengines-verification',
1615 $SECTION_SEARCHENGINE
1616 );
1617
1618 add_settings_field(
1619 'norton_save_site_verification',
1620 'Norton Safe Web',
1621 function() use ($option_key) {
1622 $value = Metasync::get_option('searchengines')['norton_save_site_verification'] ?? '';
1623 printf( '<input type="text" id="norton_save_site_verification" name="' . $option_key . '[searchengines][norton_save_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1624 },
1625 $page_slug . '_searchengines-verification',
1626 $SECTION_SEARCHENGINE
1627 );
1628
1629 // ----------------------------------------------------------------
1630 // Code Snippets page
1631 // ----------------------------------------------------------------
1632 add_settings_section(
1633 $SECTION_CODESNIPPETS,
1634 '',
1635 function() {},
1636 $page_slug . '_code-snippets'
1637 );
1638
1639 add_settings_field(
1640 'header_snippet',
1641 'Header Code',
1642 array( Metasync_Settings_Fields::instance(), 'header_snippets_callback' ),
1643 $page_slug . '_code-snippets',
1644 $SECTION_CODESNIPPETS
1645 );
1646
1647 add_settings_field(
1648 'footer_snippet',
1649 'Footer Code',
1650 array( Metasync_Settings_Fields::instance(), 'footer_snippets_callback' ),
1651 $page_slug . '_code-snippets',
1652 $SECTION_CODESNIPPETS
1653 );
1654
1655 // ----------------------------------------------------------------
1656 // Local Business page
1657 // ----------------------------------------------------------------
1658 add_settings_section(
1659 $SECTION_LOCALSEO,
1660 '',
1661 function() {},
1662 $page_slug . '_local-seo'
1663 );
1664
1665 $fields_instance = Metasync_Settings_Fields::instance();
1666
1667 add_settings_field( 'local_seo_person_organization', 'Type', array( $fields_instance, 'local_seo_person_organization_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1668 add_settings_field( 'local_seo_name', 'Name', array( $fields_instance, 'local_seo_name_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1669 add_settings_field( 'local_seo_logo', 'Logo', array( $fields_instance, 'local_seo_logo_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1670 add_settings_field( 'local_seo_url', 'Website URL', array( $fields_instance, 'local_seo_url_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1671 add_settings_field( 'local_seo_email', 'Email', array( $fields_instance, 'local_seo_email_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1672 add_settings_field( 'local_seo_phone', 'Phone', array( $fields_instance, 'local_seo_phone_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1673 add_settings_field( 'local_seo_address', 'Address', array( $fields_instance, 'local_seo_address_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1674 add_settings_field( 'local_seo_business_type', 'Business Type', array( $fields_instance, 'local_seo_business_type_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1675 add_settings_field( 'local_seo_opening_hours', 'Opening Hours', array( $fields_instance, 'local_seo_opening_hours_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1676 add_settings_field( 'local_seo_phone_numbers', 'Phone Numbers', array( $fields_instance, 'local_seo_phone_numbers_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1677 add_settings_field( 'local_seo_price_range', 'Price Range', array( $fields_instance, 'local_seo_price_range_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1678 add_settings_field( 'local_seo_about_page', 'About Page', array( $fields_instance, 'local_seo_about_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1679 add_settings_field( 'local_seo_contact_page', 'Contact Page', array( $fields_instance, 'local_seo_contact_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1680 add_settings_field( 'local_seo_map_key', 'Google Maps Key', array( $fields_instance, 'local_seo_map_key_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1681 add_settings_field( 'local_seo_geo_coordinates', 'Geo Coordinates', array( $fields_instance, 'local_seo_geo_coordinates_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1682
1683 // ----------------------------------------------------------------
1684 // Breadcrumbs page
1685 // ----------------------------------------------------------------
1686 add_settings_section(
1687 $SECTION_BREADCRUMBS,
1688 '',
1689 function() {},
1690 $page_slug . '_breadcrumbs'
1691 );
1692
1693 add_settings_field(
1694 'breadcrumbs_enabled',
1695 'Enable Breadcrumbs',
1696 function() use ($option_key) {
1697 $value = Metasync::get_option('breadcrumbs')['enabled'] ?? true;
1698 printf(
1699 '<input type="checkbox" id="breadcrumbs_enabled" name="' . $option_key . '[breadcrumbs][enabled]" value="1" %s />',
1700 checked(1, $value, false)
1701 );
1702 echo '<p class="description">Output breadcrumb trail HTML on your site.</p>';
1703 },
1704 $page_slug . '_breadcrumbs',
1705 $SECTION_BREADCRUMBS
1706 );
1707
1708 add_settings_field(
1709 'breadcrumbs_separator',
1710 'Separator',
1711 function() use ($option_key) {
1712 $value = Metasync::get_option('breadcrumbs')['separator'] ?? '&raquo;';
1713 printf(
1714 '<input type="text" id="breadcrumbs_separator" name="' . $option_key . '[breadcrumbs][separator]" value="%s" size="10" />',
1715 esc_attr($value)
1716 );
1717 echo '<p class="description">Character or HTML entity shown between crumbs (e.g. &raquo; or /).</p>';
1718 },
1719 $page_slug . '_breadcrumbs',
1720 $SECTION_BREADCRUMBS
1721 );
1722
1723 add_settings_field(
1724 'breadcrumbs_home_label',
1725 'Home Label',
1726 function() use ($option_key) {
1727 $value = Metasync::get_option('breadcrumbs')['home_label'] ?? 'Home';
1728 printf(
1729 '<input type="text" id="breadcrumbs_home_label" name="' . $option_key . '[breadcrumbs][home_label]" value="%s" size="30" />',
1730 esc_attr($value)
1731 );
1732 },
1733 $page_slug . '_breadcrumbs',
1734 $SECTION_BREADCRUMBS
1735 );
1736
1737 add_settings_field(
1738 'breadcrumbs_home_url',
1739 'Home URL',
1740 function() use ($option_key) {
1741 $value = Metasync::get_option('breadcrumbs')['home_url'] ?? '';
1742 printf(
1743 '<input type="url" id="breadcrumbs_home_url" name="' . $option_key . '[breadcrumbs][home_url]" value="%s" size="50" />',
1744 esc_attr($value)
1745 );
1746 echo '<p class="description">Leave blank to use the site home URL.</p>';
1747 },
1748 $page_slug . '_breadcrumbs',
1749 $SECTION_BREADCRUMBS
1750 );
1751
1752 add_settings_field(
1753 'breadcrumbs_show_current_page',
1754 'Show Current Page',
1755 function() use ($option_key) {
1756 $value = Metasync::get_option('breadcrumbs')['show_current_page'] ?? true;
1757 printf(
1758 '<input type="checkbox" id="breadcrumbs_show_current_page" name="' . $option_key . '[breadcrumbs][show_current_page]" value="1" %s />',
1759 checked(1, $value, false)
1760 );
1761 echo '<p class="description">Include the current page as the last (non-linked) crumb.</p>';
1762 },
1763 $page_slug . '_breadcrumbs',
1764 $SECTION_BREADCRUMBS
1765 );
1766
1767 add_settings_field(
1768 'breadcrumbs_prefix_text',
1769 'Prefix Text',
1770 function() use ($option_key) {
1771 $value = Metasync::get_option('breadcrumbs')['prefix_text'] ?? '';
1772 printf(
1773 '<input type="text" id="breadcrumbs_prefix_text" name="' . $option_key . '[breadcrumbs][prefix_text]" value="%s" size="30" />',
1774 esc_attr($value)
1775 );
1776 echo '<p class="description">Optional text before the breadcrumb trail (e.g. "You are here:").</p>';
1777 },
1778 $page_slug . '_breadcrumbs',
1779 $SECTION_BREADCRUMBS
1780 );
1781
1782 add_settings_field(
1783 'breadcrumbs_archive_label_format',
1784 'Archive Label Format',
1785 function() use ($option_key) {
1786 $value = Metasync::get_option('breadcrumbs')['archive_label_format'] ?? '{name}';
1787 printf(
1788 '<input type="text" id="breadcrumbs_archive_label_format" name="' . $option_key . '[breadcrumbs][archive_label_format]" value="%s" size="30" />',
1789 esc_attr($value)
1790 );
1791 echo '<p class="description">Format for archive crumb labels. Use <code>{name}</code> as placeholder for the archive name.</p>';
1792 },
1793 $page_slug . '_breadcrumbs',
1794 $SECTION_BREADCRUMBS
1795 );
1796
1797 # Open Graph / Article meta toggles — surface each new tag family as its
1798 # own opt-out checkbox on the main Settings page (Open Graph accordion).
1799 add_settings_field(
1800 'og_image_dimensions',
1801 'OG Image Dimensions',
1802 array(Metasync_Settings_Fields::instance(), 'og_image_dimensions_callback'),
1803 $page_slug . '_general',
1804 $SECTION_METASYNC
1805 );
1806
1807 add_settings_field(
1808 'article_timestamps',
1809 'Article Timestamps',
1810 array(Metasync_Settings_Fields::instance(), 'article_timestamps_callback'),
1811 $page_slug . '_general',
1812 $SECTION_METASYNC
1813 );
1814
1815 add_settings_field(
1816 'article_author',
1817 'Article Author',
1818 array(Metasync_Settings_Fields::instance(), 'article_author_callback'),
1819 $page_slug . '_general',
1820 $SECTION_METASYNC
1821 );
1822
1823 add_settings_field(
1824 'article_section',
1825 'Article Section',
1826 array(Metasync_Settings_Fields::instance(), 'article_section_callback'),
1827 $page_slug . '_general',
1828 $SECTION_METASYNC
1829 );
1830
1831 add_settings_field(
1832 'article_tags',
1833 'Article Tags',
1834 array(Metasync_Settings_Fields::instance(), 'article_tags_callback'),
1835 $page_slug . '_general',
1836 $SECTION_METASYNC
1837 );
1838
1839 add_settings_field(
1840 'twitter_image_alt',
1841 'Twitter Image Alt',
1842 array(Metasync_Settings_Fields::instance(), 'twitter_image_alt_callback'),
1843 $page_slug . '_general',
1844 $SECTION_METASYNC
1845 );
1846
1847 }
1848
1849 /**
1850 * Sanitize each setting field as needed.
1851 *
1852 * Formerly Metasync_Admin::sanitize().
1853 *
1854 * @param array $input Contains all settings fields as array keys
1855 * @return array Sanitized settings
1856 */
1857 public function sanitize($input)
1858 {
1859 $new_input = Metasync::get_option();
1860 $whitelabel_validation_failed = false;
1861
1862 # Determine which tab is being submitted (same logic as AJAX handler)
1863 # Check POST first (from AJAX), then GET (from form action URL)
1864 $active_tab = 'general';
1865 if (isset($_POST['active_tab'])) {
1866 $active_tab = sanitize_text_field($_POST['active_tab']);
1867 } elseif (isset($_GET['tab'])) {
1868 $active_tab = sanitize_text_field($_GET['tab']);
1869 } elseif (isset($_POST['_wp_http_referer'])) {
1870 # Parse referer URL to get tab parameter
1871 $referer = wp_parse_url($_POST['_wp_http_referer']);
1872 if (isset($referer['query'])) {
1873 parse_str($referer['query'], $referer_params);
1874 if (isset($referer_params['tab'])) {
1875 $active_tab = sanitize_text_field($referer_params['tab']);
1876 }
1877 }
1878 }
1879 $general_tab_submitted = ($active_tab === 'general');
1880
1881 // General Settings
1882 if (isset($input['general']['apikey'])) {
1883 $new_input['general']['apikey'] = sanitize_text_field($input['general']['apikey']);
1884 }
1885
1886 // Meta Box Visibility Settings
1887 if (isset($input['general']) && $general_tab_submitted) {
1888 $checkbox_fields = [
1889 'disable_common_robots_metabox',
1890 'disable_advance_robots_metabox',
1891 'disable_redirection_metabox',
1892 'disable_canonical_metabox',
1893 'disable_social_opengraph_metabox',
1894 'disable_schema_markup_metabox',
1895 'open_external_links'
1896 ];
1897
1898 foreach ($checkbox_fields as $field) {
1899 if (isset($input['general'][$field])) {
1900 $new_input['general'][$field] = filter_var($input['general'][$field], FILTER_VALIDATE_BOOLEAN);
1901 } else {
1902 $new_input['general'][$field] = false;
1903 }
1904 // Remove the raw value so the sanitized boolean in $new_input
1905 // survives the shallow array_merge($new_input, $input) below.
1906 unset($input['general'][$field]);
1907 }
1908 }
1909
1910 // Site Verification Settings
1911 if (isset($input['searchengines']['bing_site_verification'])) {
1912 $new_input['searchengines']['bing_site_verification'] = sanitize_text_field($input['searchengines']['bing_site_verification']);
1913 }
1914 if (isset($input['searchengines']['baidu_site_verification'])) {
1915 $new_input['searchengines']['baidu_site_verification'] = sanitize_text_field($input['searchengines']['baidu_site_verification']);
1916 }
1917 if (isset($input['searchengines']['alexa_site_verification'])) {
1918 $new_input['searchengines']['alexa_site_verification'] = sanitize_text_field($input['searchengines']['alexa_site_verification']);
1919 }
1920 if (isset($input['searchengines']['yandex_site_verification'])) {
1921 $new_input['searchengines']['yandex_site_verification'] = sanitize_text_field($input['searchengines']['yandex_site_verification']);
1922 }
1923 if (isset($input['searchengines']['google_site_verification'])) {
1924 $new_input['searchengines']['google_site_verification'] = sanitize_text_field($input['searchengines']['google_site_verification']);
1925 }
1926 if (isset($input['searchengines']['pinterest_site_verification'])) {
1927 $new_input['searchengines']['pinterest_site_verification'] = sanitize_text_field($input['searchengines']['pinterest_site_verification']);
1928 }
1929 if (isset($input['searchengines']['norton_save_site_verification'])) {
1930 $new_input['searchengines']['norton_save_site_verification'] = sanitize_text_field($input['searchengines']['norton_save_site_verification']);
1931 }
1932
1933 // Local Business SEO Settings
1934 if (isset($input['localseo']['local_seo_person_organization'])) {
1935 $new_input['localseo']['local_seo_person_organization'] = sanitize_text_field($input['localseo']['local_seo_person_organization']);
1936 }
1937 if (isset($input['localseo']['local_seo_name'])) {
1938 $new_input['localseo']['local_seo_name'] = sanitize_text_field($input['localseo']['local_seo_name']);
1939 }
1940 if (isset($input['localseo']['local_seo_logo'])) {
1941 $new_input['localseo']['local_seo_logo'] = sanitize_url($input['localseo']['local_seo_logo']);
1942 }
1943 if (isset($input['localseo']['local_seo_url'])) {
1944 $new_input['localseo']['local_seo_url'] = sanitize_url($input['localseo']['local_seo_url']);
1945 }
1946 if (isset($input['localseo']['local_seo_email'])) {
1947 $new_input['localseo']['local_seo_email'] = sanitize_email($input['localseo']['local_seo_email']);
1948 }
1949 if (isset($input['localseo']['local_seo_phone'])) {
1950 $new_input['localseo']['local_seo_phone'] = sanitize_text_field($input['localseo']['local_seo_phone']);
1951 }
1952 if (isset($input['localseo']['address']['street'])) {
1953 $new_input['localseo']['address']['street'] = sanitize_text_field($input['localseo']['address']['street']);
1954 }
1955 if (isset($input['localseo']['address']['locality'])) {
1956 $new_input['localseo']['address']['locality'] = sanitize_text_field($input['localseo']['address']['locality']);
1957 }
1958 if (isset($input['localseo']['address']['region'])) {
1959 $new_input['localseo']['address']['region'] = sanitize_text_field($input['localseo']['address']['region']);
1960 }
1961 if (isset($input['localseo']['address']['postancode'])) {
1962 $new_input['localseo']['address']['postancode'] = sanitize_text_field($input['localseo']['address']['postancode']);
1963 }
1964 if (isset($input['localseo']['address']['country'])) {
1965 $new_input['localseo']['address']['country'] = sanitize_text_field($input['localseo']['address']['country']);
1966 }
1967 if (isset($input['localseo']['local_seo_business_type'])) {
1968 $new_input['localseo']['local_seo_business_type'] = sanitize_text_field($input['localseo']['local_seo_business_type']);
1969 }
1970 if (isset($input['localseo']['local_seo_hours_format'])) {
1971 $new_input['localseo']['local_seo_hours_format'] = sanitize_text_field($input['localseo']['local_seo_hours_format']);
1972 }
1973 if (isset($input['localseo']['days'])) {
1974 $new_input['localseo']['days'] = sanitize_text_field($input['localseo']['days']);
1975 }
1976 if (isset($input['localseo']['times'])) {
1977 $new_input['localseo']['times'] = sanitize_text_field($input['localseo']['times']);
1978 }
1979 if (isset($input['localseo']['phonetype'])) {
1980 $new_input['localseo']['phonetype'] = sanitize_text_field($input['localseo']['phonetype']);
1981 }
1982 if (isset($input['localseo']['phonenumber'])) {
1983 $new_input['localseo']['phonenumber'] = sanitize_text_field($input['localseo']['phonenumber']);
1984 }
1985 if (isset($input['localseo']['local_seo_price_range'])) {
1986 $new_input['localseo']['local_seo_price_range'] = sanitize_text_field($input['localseo']['local_seo_price_range']);
1987 }
1988 if (isset($input['localseo']['local_seo_about_page'])) {
1989 $new_input['localseo']['local_seo_about_page'] = sanitize_text_field($input['localseo']['local_seo_about_page']);
1990 }
1991 if (isset($input['localseo']['local_seo_contact_page'])) {
1992 $new_input['localseo']['local_seo_contact_page'] = sanitize_text_field($input['localseo']['local_seo_contact_page']);
1993 }
1994 if (isset($input['localseo']['local_seo_map_key'])) {
1995 $new_input['localseo']['local_seo_map_key'] = sanitize_text_field($input['localseo']['local_seo_map_key']);
1996 }
1997 if (isset($input['localseo']['local_seo_geo_coordinates'])) {
1998 $new_input['localseo']['local_seo_geo_coordinates'] = sanitize_text_field($input['localseo']['local_seo_geo_coordinates']);
1999 }
2000
2001 // Breadcrumbs Settings
2002 if (isset($input['breadcrumbs'])) {
2003 $new_input['breadcrumbs']['enabled'] = !empty($input['breadcrumbs']['enabled']);
2004 $new_input['breadcrumbs']['show_current_page'] = !empty($input['breadcrumbs']['show_current_page']);
2005 $new_input['breadcrumbs']['disable_schema'] = !empty($input['breadcrumbs']['disable_schema']);
2006 if (isset($input['breadcrumbs']['separator'])) {
2007 $new_input['breadcrumbs']['separator'] = sanitize_text_field($input['breadcrumbs']['separator']);
2008 }
2009 if (isset($input['breadcrumbs']['home_label'])) {
2010 $new_input['breadcrumbs']['home_label'] = sanitize_text_field($input['breadcrumbs']['home_label']);
2011 }
2012 if (isset($input['breadcrumbs']['home_url'])) {
2013 $new_input['breadcrumbs']['home_url'] = esc_url_raw($input['breadcrumbs']['home_url']);
2014 }
2015 if (isset($input['breadcrumbs']['prefix_text'])) {
2016 $new_input['breadcrumbs']['prefix_text'] = sanitize_text_field($input['breadcrumbs']['prefix_text']);
2017 }
2018 if (isset($input['breadcrumbs']['archive_label_format'])) {
2019 $new_input['breadcrumbs']['archive_label_format'] = sanitize_text_field($input['breadcrumbs']['archive_label_format']);
2020 }
2021 // Remove raw breadcrumbs so sanitized values (especially checkbox
2022 // booleans) survive the shallow array_merge($new_input, $input).
2023 unset($input['breadcrumbs']);
2024 }
2025
2026 // Code Snippets Settings
2027 if (isset($input['codesnippets']['header_snippet'])) {
2028 $new_input['codesnippets']['header_snippet'] = sanitize_text_field($input['codesnippets']['header_snippet']);
2029 }
2030 if (isset($input['codesnippets']['footer_snippet'])) {
2031 $new_input['codesnippets']['footer_snippet'] = sanitize_text_field($input['codesnippets']['footer_snippet']);
2032 }
2033
2034
2035 // Optimal Settings
2036 if (isset($input['optimal_settings']['no_index_posts'])) {
2037 $new_input['optimal_settings']['no_index_posts'] = boolval($input['optimal_settings']['no_index_posts']);
2038 }
2039 if (isset($input['optimal_settings']['no_follow_links'])) {
2040 $new_input['optimal_settings']['no_follow_links'] = boolval($input['optimal_settings']['no_follow_links']);
2041 }
2042 if (isset($input['optimal_settings']['open_external_links'])) {
2043 $new_input['optimal_settings']['open_external_links'] = boolval($input['optimal_settings']['open_external_links']);
2044 }
2045 if (isset($input['optimal_settings']['add_alt_image_tags'])) {
2046 $new_input['optimal_settings']['add_alt_image_tags'] = boolval($input['optimal_settings']['add_alt_image_tags']);
2047 }
2048 if (isset($input['optimal_settings']['add_title_image_tags'])) {
2049 $new_input['optimal_settings']['add_title_image_tags'] = boolval($input['optimal_settings']['add_title_image_tags']);
2050 }
2051
2052 // Site Information - Optimal Settings
2053 if (isset($input['optimal_settings']['site_info']['type'])) {
2054 $new_input['optimal_settings']['site_info']['type'] = sanitize_text_field($input['optimal_settings']['site_info']['type']);
2055 }
2056 if (isset($input['optimal_settings']['site_info']['business_type'])) {
2057 $new_input['optimal_settings']['site_info']['business_type'] = sanitize_text_field($input['optimal_settings']['site_info']['business_type']);
2058 }
2059 if (isset($input['optimal_settings']['site_info']['company_name'])) {
2060 $new_input['optimal_settings']['site_info']['company_name'] = sanitize_text_field($input['optimal_settings']['site_info']['company_name']);
2061 }
2062 if (isset($input['optimal_settings']['site_info']['google_logo'])) {
2063 $new_input['optimal_settings']['site_info']['google_logo'] = sanitize_url($input['optimal_settings']['site_info']['google_logo']);
2064 }
2065 if (isset($input['optimal_settings']['site_info']['social_share_image'])) {
2066 $new_input['optimal_settings']['site_info']['social_share_image'] = sanitize_url($input['optimal_settings']['site_info']['social_share_image']);
2067 }
2068
2069 // Common Setting - Global Settings
2070 $common_robots_field = isset($input['common_robots_meta']) ? 'common_robots_meta' : 'common_robots_mata';
2071
2072 if (isset($input[$common_robots_field]['index'])) {
2073 $new_input['common_robots_meta']['index'] = boolval($input[$common_robots_field]['index']);
2074 }
2075 if (isset($input[$common_robots_field]['noindex'])) {
2076 $new_input['common_robots_meta']['noindex'] = boolval($input[$common_robots_field]['noindex']);
2077 }
2078 if (isset($input[$common_robots_field]['nofollow'])) {
2079 $new_input['common_robots_meta']['nofollow'] = boolval($input[$common_robots_field]['nofollow']);
2080 }
2081 if (isset($input[$common_robots_field]['noarchive'])) {
2082 $new_input['common_robots_meta']['noarchive'] = boolval($input[$common_robots_field]['noarchive']);
2083 }
2084 if (isset($input[$common_robots_field]['noimageindex'])) {
2085 $new_input['common_robots_meta']['noimageindex'] = boolval($input[$common_robots_field]['noimageindex']);
2086 }
2087 if (isset($input[$common_robots_field]['nosnippet'])) {
2088 $new_input['common_robots_meta']['nosnippet'] = boolval($input[$common_robots_field]['nosnippet']);
2089 }
2090
2091 // Advance Setting - Global Settings
2092 $advance_robots_field = isset($input['advance_robots_meta']) ? 'advance_robots_meta' : 'advance_robots_mata';
2093
2094 if (isset($input[$advance_robots_field]['max-snippet']['enable'])) {
2095 $new_input['advance_robots_meta']['max-snippet']['enable'] = boolval($input[$advance_robots_field]['max-snippet']['enable']);
2096 }
2097 if (isset($input[$advance_robots_field]['max-snippet']['length'])) {
2098 $new_input['advance_robots_meta']['max-snippet']['length'] = sanitize_text_field($input[$advance_robots_field]['max-snippet']['length']);
2099 }
2100 if (isset($input[$advance_robots_field]['max-video-preview']['enable'])) {
2101 $new_input['advance_robots_meta']['max-video-preview']['enable'] = boolval($input[$advance_robots_field]['max-video-preview']['enable']);
2102 }
2103 if (isset($input[$advance_robots_field]['max-video-preview']['length'])) {
2104 $new_input['advance_robots_meta']['max-video-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-video-preview']['length']);
2105 }
2106 if (isset($input[$advance_robots_field]['max-image-preview']['enable'])) {
2107 $new_input['advance_robots_meta']['max-image-preview']['enable'] = boolval($input[$advance_robots_field]['max-image-preview']['enable']);
2108 }
2109 if (isset($input[$advance_robots_field]['max-image-preview']['length'])) {
2110 $new_input['advance_robots_meta']['max-image-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-image-preview']['length']);
2111 }
2112
2113 // Social meta settings
2114 if (isset($input['social_meta']['facebook_page_url'])) {
2115 $new_input['social_meta']['facebook_page_url'] = sanitize_text_field($input['social_meta']['facebook_page_url']);
2116 }
2117 if (isset($input['social_meta']['facebook_authorship'])) {
2118 $new_input['social_meta']['facebook_authorship'] = sanitize_text_field($input['social_meta']['facebook_authorship']);
2119 }
2120 if (isset($input['social_meta']['facebook_admin'])) {
2121 $new_input['social_meta']['facebook_admin'] = sanitize_text_field($input['social_meta']['facebook_admin']);
2122 }
2123 if (isset($input['social_meta']['facebook_app'])) {
2124 $new_input['social_meta']['facebook_app'] = sanitize_text_field($input['social_meta']['facebook_app']);
2125 }
2126 if (isset($input['social_meta']['facebook_secret'])) {
2127 $new_input['social_meta']['facebook_secret'] = sanitize_text_field($input['social_meta']['facebook_secret']);
2128 }
2129 if (isset($input['social_meta']['twitter_username'])) {
2130 $new_input['social_meta']['twitter_username'] = sanitize_text_field($input['social_meta']['twitter_username']);
2131 }
2132
2133 // Common Meta Settings — Open Graph / article toggle checkboxes.
2134 // Unchecked HTML checkboxes are absent from $input, so an explicit
2135 // 'true'/'false' round-trip is required to persist opt-outs.
2136 if (isset($input['common_meta_settings'])) {
2137 $og_toggle_fields = [
2138 'og_image_dimensions',
2139 'article_timestamps',
2140 'article_author',
2141 'article_section',
2142 'article_tags',
2143 'twitter_image_alt',
2144 ];
2145 foreach ($og_toggle_fields as $field) {
2146 $new_input['common_meta_settings'][$field] = (isset($input['common_meta_settings'][$field]) && $input['common_meta_settings'][$field] === 'true') ? 'true' : 'false';
2147 }
2148 // Remove raw common_meta_settings so sanitized toggle values
2149 // survive the shallow array_merge($new_input, $input).
2150 unset($input['common_meta_settings']);
2151 }
2152
2153 # Handle whitelabel URL fields with improved empty value handling
2154 if (isset($input['whitelabel'])) {
2155 $existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
2156
2157 $general_input = $input['general'] ?? [];
2158 $plugin_name = isset($general_input['white_label_plugin_name']) ? trim((string) $general_input['white_label_plugin_name']) : '';
2159 $logo_light = isset($input['whitelabel']['logo_light']) ? trim((string) $input['whitelabel']['logo_light']) : '';
2160 $logo_dark = isset($input['whitelabel']['logo_dark']) ? trim((string) $input['whitelabel']['logo_dark']) : '';
2161 $author = isset($general_input['white_label_plugin_author']) ? trim((string) $general_input['white_label_plugin_author']) : '';
2162 $author_uri = isset($general_input['white_label_plugin_author_uri']) ? trim((string) $general_input['white_label_plugin_author_uri']) : '';
2163 $plugin_uri = isset($general_input['white_label_plugin_uri']) ? trim((string) $general_input['white_label_plugin_uri']) : '';
2164 $domain = isset($input['whitelabel']['domain']) ? trim((string) $input['whitelabel']['domain']) : '';
2165 $description = isset($general_input['white_label_plugin_description']) ? trim((string) $general_input['white_label_plugin_description']) : '';
2166
2167 $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));
2168 $has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
2169
2170 if ($has_optional_only && !$has_core_field) {
2171 add_settings_error(
2172 'metasync_options',
2173 'whitelabel_partial',
2174 'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.',
2175 'error'
2176 );
2177 $new_input['whitelabel'] = $existing_whitelabel;
2178 $whitelabel_validation_failed = true;
2179 } else {
2180 $new_input['whitelabel'] = $existing_whitelabel;
2181
2182 if (isset($input['whitelabel']['logo'])) {
2183 $logo_value = trim($input['whitelabel']['logo']);
2184
2185 if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
2186 $new_input['whitelabel']['logo'] = esc_url_raw($logo_value);
2187
2188 } else {
2189 $new_input['whitelabel']['logo'] = '';
2190
2191 }
2192 }
2193
2194 if (isset($input['whitelabel']['logo_light'])) {
2195 $logo_light_value = trim($input['whitelabel']['logo_light']);
2196
2197 if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
2198 $new_input['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
2199 } else {
2200 $new_input['whitelabel']['logo_light'] = '';
2201 }
2202 }
2203
2204 if (isset($input['whitelabel']['logo_dark'])) {
2205 $logo_dark_value = trim($input['whitelabel']['logo_dark']);
2206
2207 if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
2208 $new_input['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
2209 } else {
2210 $new_input['whitelabel']['logo_dark'] = '';
2211 }
2212 }
2213
2214 if (isset($input['whitelabel']['domain'])) {
2215 $domain_value = trim($input['whitelabel']['domain']);
2216
2217 if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
2218 $new_input['whitelabel']['domain'] = esc_url_raw($domain_value);
2219
2220 } else {
2221 $old_domain = $existing_whitelabel['domain'] ?? '';
2222 $new_input['whitelabel']['domain'] = '';
2223
2224 if (!empty($old_domain)) {
2225 $new_input['_trigger_heartbeat_after_save'] = 'Domain cleared from: ' . $old_domain;
2226 }
2227 }
2228 }
2229
2230 if (isset($input['whitelabel']['settings_password'])) {
2231 $password_value = trim($input['whitelabel']['settings_password']);
2232 $new_input['whitelabel']['settings_password'] = sanitize_text_field($password_value);
2233 } else {
2234 if (isset($existing_whitelabel['settings_password'])) {
2235 $new_input['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2236 }
2237 }
2238
2239 if (isset($input['whitelabel']['recovery_email'])) {
2240 $recovery_email = trim($input['whitelabel']['recovery_email']);
2241 if (!empty($recovery_email)) {
2242 $sanitized_email = sanitize_email($recovery_email);
2243 if (is_email($sanitized_email)) {
2244 $new_input['whitelabel']['recovery_email'] = $sanitized_email;
2245 } else {
2246 add_settings_error(
2247 'metasync_messages',
2248 'metasync_message',
2249 __('Invalid recovery email address.', 'metasync'),
2250 'error'
2251 );
2252 }
2253 } else {
2254 $new_input['whitelabel']['recovery_email'] = '';
2255 }
2256 } else {
2257 if (isset($existing_whitelabel['recovery_email'])) {
2258 $new_input['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
2259 }
2260 }
2261
2262 $has_password = !empty($new_input['whitelabel']['settings_password']);
2263 $has_recovery_email = !empty($new_input['whitelabel']['recovery_email']);
2264 if ($has_password && !$has_recovery_email) {
2265 add_settings_error(
2266 'metasync_messages',
2267 'metasync_message',
2268 __('Password Recovery Email is required when a password is set.', 'metasync'),
2269 'error'
2270 );
2271 $new_input['whitelabel']['settings_password'] = '';
2272 }
2273
2274 if (isset($input['whitelabel']['access_control'])) {
2275 $new_input['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($input['whitelabel']['access_control']);
2276 }
2277
2278 // Sanitize quick_links (up to 5 entries)
2279 if (isset($input['whitelabel']['quick_links']) && is_array($input['whitelabel']['quick_links'])) {
2280 $sanitized_links = [];
2281 foreach (array_slice($input['whitelabel']['quick_links'], 0, 5) as $link) {
2282 $url = isset($link['url']) ? esc_url_raw(trim((string) $link['url'])) : '';
2283 if (empty($url)) {
2284 continue; // skip blank rows
2285 }
2286 $sanitized_links[] = [
2287 'label' => sanitize_text_field($link['label'] ?? ''),
2288 'url' => $url,
2289 'external' => !empty($link['external']),
2290 ];
2291 }
2292 $new_input['whitelabel']['quick_links'] = $sanitized_links;
2293 } else {
2294 // Preserve existing links when not submitted
2295 if (isset($existing_whitelabel['quick_links'])) {
2296 $new_input['whitelabel']['quick_links'] = $existing_whitelabel['quick_links'];
2297 }
2298 }
2299
2300 // Sanitize color palette
2301 if (isset($input['whitelabel']['color_palette']) && is_array($input['whitelabel']['color_palette'])) {
2302 $allowed_vars = array(
2303 'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
2304 'dashboard-text-primary', 'dashboard-text-secondary',
2305 'dashboard-accent', 'dashboard-accent-hover',
2306 'dashboard-success', 'dashboard-warning', 'dashboard-error',
2307 'dashboard-border',
2308 'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
2309 'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
2310 );
2311 $sanitized_palette = array();
2312 foreach (array('dark', 'light') as $theme_key) {
2313 if (isset($input['whitelabel']['color_palette'][$theme_key]) && is_array($input['whitelabel']['color_palette'][$theme_key])) {
2314 $sanitized_palette[$theme_key] = array();
2315 foreach ($input['whitelabel']['color_palette'][$theme_key] as $var_name => $color_value) {
2316 if (in_array($var_name, $allowed_vars, true)) {
2317 $color_value = trim(sanitize_text_field($color_value));
2318 if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
2319 $sanitized_palette[$theme_key][$var_name] = $color_value;
2320 }
2321 }
2322 }
2323 }
2324 }
2325 $new_input['whitelabel']['color_palette'] = $sanitized_palette;
2326 } else {
2327 if (isset($existing_whitelabel['color_palette'])) {
2328 $new_input['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
2329 }
2330 }
2331
2332 $new_input['whitelabel']['updated_at'] = time();
2333 }
2334 } else {
2335 $existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
2336 $has_existing_whitelabel = !empty($existing_whitelabel['domain']) || !empty($existing_whitelabel['logo']);
2337
2338 if ($has_existing_whitelabel) {
2339 $new_input['whitelabel'] = [
2340 'is_whitelabel' => false,
2341 'domain' => '',
2342 'logo' => '',
2343 'company_name' => '',
2344 'updated_at' => time()
2345 ];
2346
2347 if (!empty($existing_whitelabel['domain'])) {
2348 delete_transient('metasync_heartbeat_status_cache');
2349 do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel settings cleared - domain changed to default');
2350 }
2351 }
2352 }
2353
2354 // Indexation Control Settings
2355 if (isset($input['seo_controls']['index_date_archives'])) {
2356 $new_input['seo_controls']['index_date_archives'] = boolval($input['seo_controls']['index_date_archives']);
2357 }
2358 if (isset($input['seo_controls']['index_tag_archives'])) {
2359 $new_input['seo_controls']['index_tag_archives'] = boolval($input['seo_controls']['index_tag_archives']);
2360 }
2361 if (isset($input['seo_controls']['index_author_archives'])) {
2362 $new_input['seo_controls']['index_author_archives'] = boolval($input['seo_controls']['index_author_archives']);
2363 }
2364 if (isset($input['seo_controls']['index_format_archives'])) {
2365 $new_input['seo_controls']['index_format_archives'] = boolval($input['seo_controls']['index_format_archives']);
2366 }
2367
2368 // Handle post-save heartbeat trigger for domain changes
2369 if (isset($new_input['_trigger_heartbeat_after_save'])) {
2370 $context = $new_input['_trigger_heartbeat_after_save'];
2371 unset($new_input['_trigger_heartbeat_after_save']);
2372
2373 add_action('updated_option_metasync_options', function() use ($context) {
2374 delete_transient('metasync_heartbeat_status_cache');
2375 do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel domain change - ' . $context);
2376 });
2377 }
2378
2379 $result = array_merge($new_input, $input);
2380
2381 if ($whitelabel_validation_failed) {
2382 $result['whitelabel'] = Metasync::get_option()['whitelabel'] ?? [];
2383 }
2384
2385 delete_transient('metasync_otto_js_detected');
2386
2387 return $result;
2388 }
2389
2390 /**
2391 * AJAX handler for saving general/whitelabel/advanced settings.
2392 *
2393 * Moved from Metasync_Admin – the admin class now delegates here.
2394 */
2395 public function meta_sync_save_settings()
2396 {
2397 if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
2398 wp_send_json_error(array('message' => 'Invalid nonce'));
2399 return;
2400 }
2401
2402 if (!Metasync::current_user_has_plugin_access()) {
2403 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
2404 return;
2405 }
2406
2407 $text_fields = [
2408 'searchatlas_api_key', 'apikey',
2409 'white_label_plugin_name', 'white_label_plugin_description',
2410 'white_label_plugin_author', 'white_label_plugin_menu_slug',
2411 'white_label_plugin_menu_icon', 'enabled_plugin_css',
2412 'enabled_elementor_plugin_css_color','enabled_elementor_plugin_css',
2413 'otto_pixel_uuid','periodic_clear_otto_cache','periodic_clear_ottopage_cache',
2414 'periodic_clear_ottopost_cache', 'whitelabel_otto_name', 'otto_wp_rocket_compat'
2415 ];
2416
2417 $url_fields = [
2418 'white_label_plugin_author_uri', 'white_label_plugin_uri'
2419 ];
2420
2421 $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'];
2422
2423 $url_fields = ['white_label_plugin_author_uri', 'white_label_plugin_uri'];
2424
2425 $metasync_options = Metasync::get_option();
2426 if (!is_array($metasync_options)) {
2427 $metasync_options = array();
2428 }
2429 if (!isset($metasync_options['general']) || !is_array($metasync_options['general'])) {
2430 $metasync_options['general'] = array();
2431 }
2432
2433 $validation_errors = [];
2434
2435 $active_tab = isset($_POST['active_tab']) ? sanitize_text_field($_POST['active_tab']) : 'general';
2436 $general_tab_submitted = ($active_tab === 'general');
2437 $whitelabel_tab_submitted = ($active_tab === 'whitelabel');
2438
2439 foreach ($text_fields as $field) {
2440
2441 if (isset($_POST['metasync_options']['general'][$field])) {
2442
2443 $value = trim($_POST['metasync_options']['general'][$field]);
2444
2445 // searchatlas_api_key must be clearable to support key removal (WP-334)
2446 $clearable_fields = [
2447 'searchatlas_api_key',
2448 'white_label_plugin_name',
2449 'white_label_plugin_description',
2450 'white_label_plugin_author',
2451 'white_label_plugin_menu_slug',
2452 'white_label_plugin_menu_icon',
2453 'whitelabel_otto_name',
2454 'otto_pixel_uuid'
2455 ];
2456
2457 if ($value === '' && !in_array($field, $clearable_fields)) {
2458 continue;
2459 }
2460
2461 if ($field === 'white_label_plugin_name') {
2462 if (strlen($value) > 16) {
2463 $validation_errors[$field] = 'Plugin name must not exceed 16 characters';
2464 continue;
2465 }
2466 }
2467
2468 if ($field === 'white_label_plugin_menu_icon') {
2469
2470 if ($value === '') {
2471 $metasync_options['general'][$field] = '';
2472 } elseif (filter_var($value, FILTER_VALIDATE_URL)) {
2473
2474 $image_extensions = ['png', 'svg'];
2475
2476 $path = parse_url($value, PHP_URL_PATH);
2477 $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
2478
2479 if (in_array($extension, $image_extensions, true)) {
2480 $metasync_options['general'][$field] = esc_url_raw($value);
2481 } else {
2482 $validation_errors[] = 'Invalid Menu icon format. Only PNG and SVG are allowed.';
2483 }
2484 } else {
2485 $validation_errors[] = 'Invalid Menu icon URL format.';
2486 }
2487 } else {
2488 $metasync_options['general'][$field] = sanitize_text_field($_POST['metasync_options']['general'][$field]);
2489 }
2490 }
2491 }
2492
2493 $textarea_fields = ['otto_bot_whitelist', 'otto_bot_blacklist'];
2494 if ($general_tab_submitted) {
2495 foreach ($textarea_fields as $field) {
2496 if (isset($_POST['metasync_options']['general'][$field])) {
2497 $metasync_options['general'][$field] = sanitize_textarea_field($_POST['metasync_options']['general'][$field]);
2498 } else {
2499 $metasync_options['general'][$field] = '';
2500 }
2501 }
2502 }
2503
2504 if ($general_tab_submitted) {
2505 foreach ($bool_fields as $field) {
2506 if (isset($_POST['metasync_options']['general'][$field])) {
2507 $metasync_options['general'][$field] = filter_var($_POST['metasync_options']['general'][$field], FILTER_VALIDATE_BOOLEAN);
2508 }else {
2509 $metasync_options['general'][$field] = false;
2510 }
2511 }
2512 }
2513
2514 if ($general_tab_submitted || $whitelabel_tab_submitted) {
2515 foreach ($url_fields as $field) {
2516 if (isset($_POST['metasync_options']['general'][$field])) {
2517
2518 $value = trim($_POST['metasync_options']['general'][$field]);
2519
2520 $whitelabel_clearable_url_fields = [
2521 'white_label_plugin_author_uri',
2522 'white_label_plugin_uri'
2523 ];
2524
2525 if ($value === '' && !in_array($field, $whitelabel_clearable_url_fields)) {
2526 continue;
2527 }
2528
2529 if ($value === '' && in_array($field, $whitelabel_clearable_url_fields)) {
2530 $metasync_options['general'][$field] = '';
2531 continue;
2532 }
2533
2534 if (filter_var($value, FILTER_VALIDATE_URL)) {
2535
2536 $parsed_url = parse_url($value);
2537 $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
2538
2539 if (strpos($host, '.') !== false || filter_var($host, FILTER_VALIDATE_IP)) {
2540 $metasync_options['general'][$field] = esc_url_raw($value);
2541 } else {
2542 $field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
2543 $validation_errors[] = 'Invalid ' . $field_name . ' format. Please use a proper domain name (e.g., example.com).';
2544 }
2545 } else {
2546 $field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
2547 $validation_errors[] = 'Invalid ' . $field_name . ' format.';
2548 }
2549 }
2550 }
2551 }
2552
2553 if ($general_tab_submitted) {
2554 if (isset($_POST['metasync_options']['general']['content_genius_sync_roles']) && is_array($_POST['metasync_options']['general']['content_genius_sync_roles'])) {
2555 $metasync_options['general']['content_genius_sync_roles'] = array_map('sanitize_text_field', $_POST['metasync_options']['general']['content_genius_sync_roles']);
2556 } else {
2557 $metasync_options['general']['content_genius_sync_roles'] = array();
2558 }
2559 }
2560
2561 if ($general_tab_submitted) {
2562 $breadcrumbs_input = isset($_POST['metasync_options']['breadcrumbs']) && is_array($_POST['metasync_options']['breadcrumbs'])
2563 ? $_POST['metasync_options']['breadcrumbs']
2564 : array();
2565
2566 if (!isset($metasync_options['breadcrumbs']) || !is_array($metasync_options['breadcrumbs'])) {
2567 $metasync_options['breadcrumbs'] = array();
2568 }
2569
2570 $metasync_options['breadcrumbs']['enabled'] = !empty($breadcrumbs_input['enabled']);
2571 $metasync_options['breadcrumbs']['disable_schema'] = !empty($breadcrumbs_input['disable_schema']);
2572 $metasync_options['breadcrumbs']['show_current_page'] = !empty($breadcrumbs_input['show_current_page']);
2573 $metasync_options['breadcrumbs']['separator'] = isset($breadcrumbs_input['separator']) ? sanitize_text_field($breadcrumbs_input['separator']) : '»';
2574 $metasync_options['breadcrumbs']['home_label'] = isset($breadcrumbs_input['home_label']) ? sanitize_text_field($breadcrumbs_input['home_label']) : 'Home';
2575 $metasync_options['breadcrumbs']['home_url'] = isset($breadcrumbs_input['home_url']) ? esc_url_raw($breadcrumbs_input['home_url']) : '';
2576 $metasync_options['breadcrumbs']['prefix_text'] = isset($breadcrumbs_input['prefix_text']) ? sanitize_text_field($breadcrumbs_input['prefix_text']) : '';
2577 $metasync_options['breadcrumbs']['archive_label_format'] = isset($breadcrumbs_input['archive_label_format']) ? sanitize_text_field($breadcrumbs_input['archive_label_format']) : '{name}';
2578 }
2579
2580 // Open Graph / article toggle checkboxes live under common_meta_settings.
2581 // Unchecked HTML checkboxes are absent from $_POST, so we explicitly
2582 // write 'false' for any missing toggle to persist opt-outs.
2583 if ($general_tab_submitted) {
2584 $og_toggle_fields = [
2585 'open_graph_meta_tags',
2586 'facebook_meta_tags',
2587 'twitter_meta_tags',
2588 'og_image_dimensions',
2589 'article_timestamps',
2590 'article_author',
2591 'article_section',
2592 'article_tags',
2593 'twitter_image_alt',
2594 ];
2595 if (!isset($metasync_options['common_meta_settings']) || !is_array($metasync_options['common_meta_settings'])) {
2596 $metasync_options['common_meta_settings'] = [];
2597 }
2598 foreach ($og_toggle_fields as $field) {
2599 $posted = isset($_POST['metasync_options']['common_meta_settings'][$field])
2600 ? sanitize_text_field($_POST['metasync_options']['common_meta_settings'][$field])
2601 : '';
2602 $metasync_options['common_meta_settings'][$field] = ($posted === 'true') ? 'true' : 'false';
2603 }
2604 }
2605
2606 if ($general_tab_submitted && isset($_POST['metasync_options']['general']['default_page_builder'])) {
2607 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
2608 $available_builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
2609 $builder_value = sanitize_text_field($_POST['metasync_options']['general']['default_page_builder']);
2610 if (isset($available_builders[$builder_value]) && $available_builders[$builder_value]['detected']) {
2611 $metasync_options['general']['default_page_builder'] = $builder_value;
2612 } else {
2613 $metasync_options['general']['default_page_builder'] = 'gutenberg';
2614 if (isset($available_builders[$builder_value]) && !$available_builders[$builder_value]['detected']) {
2615 $validation_errors[] = sprintf(__('Builder "%s" is not detected on this site. Reverted to Gutenberg.', 'metasync'), $available_builders[$builder_value]['label']);
2616 }
2617 }
2618 }
2619
2620 if ($whitelabel_tab_submitted && isset($_POST['metasync_options']['whitelabel'])) {
2621 $gp = isset($_POST['metasync_options']['general']) && is_array($_POST['metasync_options']['general']) ? $_POST['metasync_options']['general'] : [];
2622 $wp = isset($_POST['metasync_options']['whitelabel']) && is_array($_POST['metasync_options']['whitelabel']) ? $_POST['metasync_options']['whitelabel'] : [];
2623 $plugin_name = isset($gp['white_label_plugin_name']) ? trim((string) $gp['white_label_plugin_name']) : '';
2624 $logo_light = isset($wp['logo_light']) ? trim((string) $wp['logo_light']) : '';
2625 $logo_dark = isset($wp['logo_dark']) ? trim((string) $wp['logo_dark']) : '';
2626 $author = isset($gp['white_label_plugin_author']) ? trim((string) $gp['white_label_plugin_author']) : '';
2627 $author_uri = isset($gp['white_label_plugin_author_uri']) ? trim((string) $gp['white_label_plugin_author_uri']) : '';
2628 $plugin_uri = isset($gp['white_label_plugin_uri']) ? trim((string) $gp['white_label_plugin_uri']) : '';
2629 $domain = isset($wp['domain']) ? trim((string) $wp['domain']) : '';
2630 $description = isset($gp['white_label_plugin_description']) ? trim((string) $gp['white_label_plugin_description']) : '';
2631
2632 $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));
2633 $has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
2634
2635 if ($has_optional_only && !$has_core_field) {
2636 $validation_errors[] = 'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.';
2637 }
2638 }
2639
2640 if (!empty($validation_errors)) {
2641 wp_send_json_error([
2642 'errors' => $validation_errors
2643 ]);
2644 return;
2645 }
2646
2647 $old_options = Metasync::get_option('general') ?? [];
2648 $old_api_key = $old_options['searchatlas_api_key'] ?? '';
2649
2650 $api_key_field_present = isset( $_POST['metasync_options']['general']['searchatlas_api_key'] );
2651 $api_key_validated = null; // null = not checked, true = valid, false = rejected
2652 $is_connected = null;
2653 $api_key_warning = null;
2654 $proposed_new_api_key = $metasync_options['general']['searchatlas_api_key'] ?? '';
2655
2656 // WP-308: When the API key changes, save it first, then POST heartbeat to register
2657 // the WPWebsiteHeartbeat row on the backend, then GET ping to verify.
2658 // The old flow (WP-239) did GET ping first, which failed because the row didn't exist yet.
2659 if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
2660 // Step 1: Save the new key so SyncCustomerParams can use it
2661 Metasync::set_option($metasync_options);
2662
2663 // Step 2: POST heartbeat to register the site (creates WPWebsiteHeartbeat row)
2664 $sync_request = new Metasync_Sync_Requests();
2665 $sync_response = $sync_request->SyncCustomerParams();
2666 $sync_status = wp_remote_retrieve_response_code($sync_response);
2667
2668 if ( $sync_status == 200 ) {
2669 // Step 3: GET ping to verify registration succeeded
2670 $ping = Metasync_Heartbeat_Manager::instance()->validate_api_key( $proposed_new_api_key );
2671
2672 if ( $ping['connected'] === true ) {
2673 $api_key_validated = true;
2674 $is_connected = true;
2675 if ( ! empty( $ping['otto_pixel_uuid'] ) ) {
2676 $existing_uuid = $metasync_options['general']['otto_pixel_uuid'] ?? '';
2677 if ( empty( $existing_uuid ) || $existing_uuid !== $ping['otto_pixel_uuid'] ) {
2678 $metasync_options['general']['otto_pixel_uuid'] = sanitize_text_field( $ping['otto_pixel_uuid'] );
2679 Metasync::set_option($metasync_options);
2680 }
2681 }
2682 } else {
2683 // POST succeeded but ping didn't confirm — keep the key, warn user
2684 $api_key_validated = true;
2685 $is_connected = true;
2686 $api_key_warning = 'API key saved and heartbeat sent. Connection verification will complete on the next heartbeat.';
2687 }
2688 } elseif ( is_wp_error($sync_response) ) {
2689 // Network error during POST — keep the key, warn user
2690 $api_key_warning = 'Could not register with the server (network error). The key was saved — it will be verified on the next heartbeat.';
2691 } else {
2692 // POST returned non-200 — the key is likely invalid, revert
2693 $metasync_options['general']['searchatlas_api_key'] = $old_api_key;
2694 Metasync::set_option($metasync_options);
2695 $api_key_validated = false;
2696 $is_connected = false;
2697 $api_key_warning = empty( $old_api_key )
2698 ? 'The API key could not be verified. Please check it and try again.'
2699 : 'The API key could not be verified. Your API key has been reverted to the previously saved key.';
2700 }
2701 } else {
2702 // Key unchanged — just save options
2703 Metasync::set_option($metasync_options);
2704 }
2705
2706 // Update connection state immediately when validated
2707 if ( $api_key_validated === true ) {
2708 update_option( 'metasync_last_known_connection_state', true );
2709 set_transient( 'metasync_heartbeat_status_cache', [
2710 'status' => true,
2711 'timestamp' => time(),
2712 'cached_until' => time() + 300,
2713 'updated_by' => 'manual_save_ping_connected',
2714 ], 300 );
2715 }
2716
2717 $data = Metasync::get_option('general');
2718 $new_api_key = $data['searchatlas_api_key'] ?? '';
2719
2720 $api_key_changed = $old_api_key !== $new_api_key;
2721 $api_key_added = empty($old_api_key) && !empty($new_api_key);
2722 $api_key_removed = !empty($old_api_key) && empty($new_api_key);
2723
2724 if ( $api_key_validated === true ) {
2725 # Use current_time('mysql') so the value is consistent with the
2726 # cron heartbeat writer and renders correctly via time_elapsed_string.
2727 $send_auth_token_timestamp = Metasync::get_option();
2728 $send_auth_token_timestamp['general']['send_auth_token_timestamp'] = current_time('mysql');
2729 Metasync::set_option($send_auth_token_timestamp);
2730
2731 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
2732 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - validated via ping');
2733 } else {
2734 // Key unchanged or removed — run sync as before
2735 $sync_request = new Metasync_Sync_Requests();
2736 $response = $sync_request->SyncCustomerParams();
2737 $responseCode = wp_remote_retrieve_response_code($response);
2738
2739 if ($responseCode == 200) {
2740 # Use current_time('mysql') for consistency with cron heartbeat.
2741 $send_auth_token_timestamp = Metasync::get_option();
2742 $send_auth_token_timestamp['general']['send_auth_token_timestamp'] = current_time('mysql');
2743 Metasync::set_option($send_auth_token_timestamp);
2744
2745 if ($api_key_added) {
2746 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
2747 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - new key added');
2748 } elseif ($api_key_removed) {
2749 Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
2750 delete_transient('metasync_heartbeat_status_cache');
2751 } elseif ($api_key_changed && !empty($new_api_key)) {
2752 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - key changed');
2753 }
2754 }
2755 }
2756
2757 if (isset($_POST['metasync_options']['whitelabel'])) {
2758
2759 $whitelabel_data = $_POST['metasync_options']['whitelabel'];
2760 $existing_whitelabel = $metasync_options['whitelabel'] ?? [];
2761
2762 if (isset($whitelabel_data['logo'])) {
2763 $logo_value = trim($whitelabel_data['logo']);
2764
2765 if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
2766 $metasync_options['whitelabel']['logo'] = esc_url_raw($logo_value);
2767 } else {
2768 $metasync_options['whitelabel']['logo'] = '';
2769 }
2770 }
2771
2772 if (isset($whitelabel_data['logo_light'])) {
2773 $logo_light_value = trim($whitelabel_data['logo_light']);
2774
2775 if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
2776 $metasync_options['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
2777 } else {
2778 $metasync_options['whitelabel']['logo_light'] = '';
2779 }
2780 }
2781
2782 if (isset($whitelabel_data['logo_dark'])) {
2783 $logo_dark_value = trim($whitelabel_data['logo_dark']);
2784
2785 if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
2786 $metasync_options['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
2787 } else {
2788 $metasync_options['whitelabel']['logo_dark'] = '';
2789 }
2790 }
2791
2792 if (isset($whitelabel_data['domain'])) {
2793 $domain_value = trim($whitelabel_data['domain']);
2794 if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
2795 $metasync_options['whitelabel']['domain'] = esc_url_raw($domain_value);
2796 } else {
2797 $metasync_options['whitelabel']['domain'] = '';
2798 }
2799 }
2800
2801 if (isset($whitelabel_data['settings_password'])) {
2802 $password_value = trim($whitelabel_data['settings_password']);
2803
2804 $hide_settings_enabled = isset($whitelabel_data['hide_settings']) && $whitelabel_data['hide_settings'] == '1';
2805
2806 if ($hide_settings_enabled && empty($password_value)) {
2807 if (isset($existing_whitelabel['settings_password'])) {
2808 $metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2809 }
2810 } else {
2811 $metasync_options['whitelabel']['settings_password'] = sanitize_text_field($password_value);
2812 }
2813 } else {
2814 if (isset($existing_whitelabel['settings_password'])) {
2815 $metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2816 }
2817 }
2818
2819 if (isset($whitelabel_data['hide_settings'])) {
2820 $metasync_options['whitelabel']['hide_settings'] = filter_var($whitelabel_data['hide_settings'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
2821 } else {
2822 $metasync_options['whitelabel']['hide_settings'] = 0;
2823 }
2824
2825 if (isset($whitelabel_data['recovery_email'])) {
2826 $recovery_email = trim($whitelabel_data['recovery_email']);
2827 if (!empty($recovery_email)) {
2828 $sanitized_email = sanitize_email($recovery_email);
2829 if (is_email($sanitized_email)) {
2830 $metasync_options['whitelabel']['recovery_email'] = $sanitized_email;
2831 }
2832 } else {
2833 $metasync_options['whitelabel']['recovery_email'] = '';
2834 }
2835 } else {
2836 if (isset($existing_whitelabel['recovery_email'])) {
2837 $metasync_options['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
2838 }
2839 }
2840
2841 $has_password = !empty($metasync_options['whitelabel']['settings_password']);
2842 $has_recovery_email = !empty($metasync_options['whitelabel']['recovery_email']);
2843 if ($has_password && !$has_recovery_email) {
2844 $metasync_options['whitelabel']['settings_password'] = '';
2845 }
2846
2847 if (isset($whitelabel_data['access_control'])) {
2848 $metasync_options['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($whitelabel_data['access_control']);
2849 }
2850
2851 if (!empty($metasync_options['whitelabel']['hide_settings']) && empty($metasync_options['whitelabel']['settings_password'])) {
2852 $metasync_options['whitelabel']['hide_settings'] = 0;
2853 }
2854
2855 // Sanitize color palette
2856 if (isset($whitelabel_data['color_palette']) && is_array($whitelabel_data['color_palette'])) {
2857 $allowed_vars = array(
2858 'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
2859 'dashboard-text-primary', 'dashboard-text-secondary',
2860 'dashboard-accent', 'dashboard-accent-hover',
2861 'dashboard-success', 'dashboard-warning', 'dashboard-error',
2862 'dashboard-border',
2863 'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
2864 'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
2865 );
2866 $sanitized_palette = array();
2867 foreach (array('dark', 'light') as $theme_key) {
2868 if (isset($whitelabel_data['color_palette'][$theme_key]) && is_array($whitelabel_data['color_palette'][$theme_key])) {
2869 $sanitized_palette[$theme_key] = array();
2870 foreach ($whitelabel_data['color_palette'][$theme_key] as $var_name => $color_value) {
2871 if (in_array($var_name, $allowed_vars, true)) {
2872 $color_value = trim(sanitize_text_field($color_value));
2873 if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
2874 $sanitized_palette[$theme_key][$var_name] = $color_value;
2875 }
2876 }
2877 }
2878 }
2879 }
2880 $metasync_options['whitelabel']['color_palette'] = $sanitized_palette;
2881 } else {
2882 if (isset($existing_whitelabel['color_palette'])) {
2883 $metasync_options['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
2884 }
2885 }
2886
2887 $metasync_options['whitelabel']['updated_at'] = time();
2888
2889 Metasync::set_option($metasync_options);
2890
2891 }
2892
2893 $redirect_url = isset($_GET['tab']) ?
2894 admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings&tab='.$_GET['tab']) :
2895 admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings');
2896
2897 // Also save edge cache settings if present in the payload (section renders inside this form)
2898 if (class_exists('Metasync_Edge_Cache_Settings')) {
2899 Metasync_Edge_Cache_Settings::save_from_post();
2900 }
2901
2902 if ($general_tab_submitted && isset($_POST['metasync_llms_txt_settings'])) {
2903 $llms_input = wp_unslash($_POST['metasync_llms_txt_settings']);
2904 if (is_array($llms_input)) {
2905 update_option('metasync_llms_txt_settings', $this->sanitize_llms_txt_settings($llms_input));
2906 }
2907 }
2908
2909 $response_payload = array(
2910 'message' => 'Settings saved successfully!',
2911 'redirect_url' => $redirect_url,
2912 );
2913
2914 if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
2915 if ( $api_key_validated !== null ) {
2916 $response_payload['api_key_validated'] = $api_key_validated;
2917 $response_payload['is_connected'] = $is_connected;
2918 }
2919 if ( $api_key_warning !== null ) {
2920 $response_payload['warning'] = $api_key_warning;
2921 }
2922 if ( $api_key_validated === false ) {
2923 $response_payload['previous_api_key'] = $old_api_key;
2924 }
2925 } elseif ( $api_key_field_present && $proposed_new_api_key === '' && $old_api_key !== '' ) {
2926 $response_payload['api_key_removed'] = true;
2927 $response_payload['warning'] = 'The API key has been removed. The plugin will no longer sync with the server.';
2928 }
2929
2930 delete_transient('metasync_otto_js_detected');
2931
2932 wp_send_json_success( $response_payload );
2933 }
2934
2935 /**
2936 * AJAX handler for saving execution settings.
2937 *
2938 * Moved from Metasync_Admin – the admin class now delegates here.
2939 */
2940 public function ajax_save_execution_settings()
2941 {
2942 if (!isset($_POST['execution_settings_nonce']) || !wp_verify_nonce($_POST['execution_settings_nonce'], 'metasync_execution_settings_nonce')) {
2943 wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
2944 return;
2945 }
2946
2947 if (!Metasync::current_user_has_plugin_access()) {
2948 wp_send_json_error(array('message' => 'Insufficient permissions to save settings.'));
2949 return;
2950 }
2951
2952 $settings = array(
2953 'max_execution_time' => isset($_POST['max_execution_time']) ? absint($_POST['max_execution_time']) : 30,
2954 'max_memory_limit' => isset($_POST['max_memory_limit']) ? absint($_POST['max_memory_limit']) : 256,
2955 'log_batch_size' => isset($_POST['log_batch_size']) ? absint($_POST['log_batch_size']) : 1000,
2956 'action_scheduler_batches' => isset($_POST['action_scheduler_batches']) ? absint($_POST['action_scheduler_batches']) : 1,
2957 'otto_rate_limit' => isset($_POST['otto_rate_limit']) ? absint($_POST['otto_rate_limit']) : 10,
2958 'queue_cleanup_days' => isset($_POST['queue_cleanup_days']) ? absint($_POST['queue_cleanup_days']) : 31
2959 );
2960
2961 $server_limits = Metasync_Settings_Fields::instance()->get_server_limits();
2962
2963 if ($settings['max_execution_time'] < 1 || $settings['max_execution_time'] > 300) {
2964 wp_send_json_error(array('message' => 'Max Execution Time must be between 1 and 300 seconds.'));
2965 return;
2966 }
2967
2968 if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) {
2969 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'])));
2970 return;
2971 }
2972
2973 $can_change_memory = Metasync_Settings_Fields::instance()->can_change_memory_limit();
2974 if ($can_change_memory && ($settings['max_memory_limit'] < 64 || $settings['max_memory_limit'] > 512)) {
2975 wp_send_json_error(array('message' => 'Max Memory Limit must be between 64 and 512 MB.'));
2976 return;
2977 }
2978
2979 if ($can_change_memory && $server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
2980 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'])));
2981 return;
2982 }
2983
2984 if (!$can_change_memory) {
2985 $existing_settings = get_option('metasync_execution_settings', array());
2986 $settings['max_memory_limit'] = isset($existing_settings['max_memory_limit'])
2987 ? $existing_settings['max_memory_limit']
2988 : 256;
2989 }
2990
2991 if ($settings['log_batch_size'] < 100 || $settings['log_batch_size'] > 5000) {
2992 wp_send_json_error(array('message' => 'Log Batch Size must be between 100 and 5000 lines.'));
2993 return;
2994 }
2995
2996 if ($settings['action_scheduler_batches'] < 1 || $settings['action_scheduler_batches'] > 10) {
2997 wp_send_json_error(array('message' => 'Action Scheduler Concurrent Batches must be between 1 and 10.'));
2998 return;
2999 }
3000
3001 if ($settings['otto_rate_limit'] < 1 || $settings['otto_rate_limit'] > 60) {
3002 wp_send_json_error(array('message' => 'OTTO API Calls Per Minute must be between 1 and 60.'));
3003 return;
3004 }
3005
3006 if ($settings['queue_cleanup_days'] < 7 || $settings['queue_cleanup_days'] > 90) {
3007 wp_send_json_error(array('message' => 'Queue Auto-Cleanup Days must be between 7 and 90 days.'));
3008 return;
3009 }
3010
3011 $saved = update_option('metasync_execution_settings', $settings);
3012
3013 if ($saved !== false) {
3014 wp_send_json_success(array(
3015 'message' => 'Execution settings saved successfully!'
3016 ));
3017 } else {
3018 wp_send_json_error(array('message' => 'Failed to save settings. Please try again.'));
3019 }
3020 }
3021
3022 /**
3023 * AJAX handler for saving SEO / Indexation Control settings.
3024 *
3025 * Moved from Metasync_Admin – the admin class now delegates here.
3026 */
3027 public function meta_sync_save_seo_controls()
3028 {
3029 try {
3030 if (!isset($_POST['meta_sync_seo_controls_nonce']) || !wp_verify_nonce($_POST['meta_sync_seo_controls_nonce'], 'meta_sync_seo_controls_nonce')) {
3031 wp_send_json_error(array('message' => 'Invalid nonce'));
3032 return;
3033 }
3034
3035 $current_options = Metasync::get_option();
3036
3037 $original_options = json_decode(json_encode($current_options), true);
3038
3039 if (!isset($current_options['seo_controls'])) {
3040 $current_options['seo_controls'] = [];
3041 }
3042
3043 $seo_control_fields = [
3044 'index_date_archives',
3045 'index_tag_archives',
3046 'index_author_archives',
3047 'index_category_archives',
3048 'index_format_archives',
3049 'override_robots_tags',
3050 'add_nofollow_to_external_links',
3051 'enable_googleinstantindex',
3052 'enable_binginstantindex'
3053 ];
3054
3055 foreach ($seo_control_fields as $field) {
3056 if (isset($_POST['metasync_options']['seo_controls'][$field]) && $_POST['metasync_options']['seo_controls'][$field] === 'true') {
3057 $current_options['seo_controls'][$field] = 'true';
3058 } else {
3059 $current_options['seo_controls'][$field] = 'false';
3060 }
3061 }
3062
3063 $bing_save_result = true;
3064 if (isset($_POST['metasync_bing_api_key_inline'])) {
3065 $bing_save_result = $this->save_bing_inline_settings_ajax();
3066 }
3067
3068 // Save Google Instant Indexing post types
3069 if (isset($_POST['metasync_post_types']) && is_array($_POST['metasync_post_types'])) {
3070 $google_post_types = array_values(array_map('sanitize_title', wp_unslash($_POST['metasync_post_types'])));
3071 $google_settings = get_option('metasync_options_instant_indexing', ['post_types' => []]);
3072 $google_settings['post_types'] = $google_post_types;
3073 update_option('metasync_options_instant_indexing', $google_settings);
3074 }
3075
3076 $options_changed = (json_encode($original_options) !== json_encode($current_options));
3077
3078 $result = Metasync::set_option($current_options);
3079
3080 if (!$result && !$options_changed) {
3081 $result = true;
3082 }
3083
3084 if ($result && $bing_save_result) {
3085 $success_message = 'Indexation Control settings saved successfully!';
3086 if (isset($_POST['metasync_bing_api_key_inline'])) {
3087 $success_message = 'Indexation Control and Bing Instant Indexing settings saved successfully!';
3088 }
3089 wp_send_json_success(array(
3090 'message' => $success_message,
3091 'saved_data' => $current_options['seo_controls']
3092 ));
3093 } else {
3094 $error_message = 'Failed to save Indexation Control settings';
3095 if (!$bing_save_result) {
3096 $error_message = 'Failed to save Bing Instant Indexing settings';
3097 }
3098 wp_send_json_error(array('message' => $error_message));
3099 }
3100 } catch (Exception $e) {
3101 error_log('Indexation Control AJAX Exception: ' . $e->getMessage());
3102 error_log('Indexation Control AJAX Stack trace: ' . $e->getTraceAsString());
3103 wp_send_json_error(array('message' => 'Error saving settings: ' . $e->getMessage()));
3104 }
3105 }
3106
3107 /**
3108 * Save Bing IndexNow inline settings from SEO Controls page.
3109 *
3110 * Moved from Metasync_Admin – the admin class now delegates here.
3111 *
3112 * @return bool True on success, false on failure
3113 */
3114 public function save_bing_inline_settings_ajax()
3115 {
3116 $api_key = isset($_POST['metasync_bing_api_key_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_api_key_inline'])) : '';
3117 $endpoint = isset($_POST['metasync_bing_endpoint_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_endpoint_inline'])) : 'indexnow';
3118 $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'])) : [];
3119 $disable_other_plugins = isset($_POST['metasync_bing_disable_other_plugins_inline']) ? true : false;
3120
3121 $existing_settings = get_option('metasync_options_bing_instant_indexing', []);
3122
3123 $new_settings = [
3124 'api_key' => $api_key,
3125 'endpoint' => $endpoint,
3126 'post_types' => array_values($post_types),
3127 'disable_other_plugins' => $disable_other_plugins,
3128 ];
3129
3130 $settings = array_merge($existing_settings, $new_settings);
3131
3132 $settings_changed = (json_encode($existing_settings) !== json_encode($settings));
3133
3134 $result = update_option('metasync_options_bing_instant_indexing', $settings);
3135
3136 if (!$result && !$settings_changed) {
3137 $result = true;
3138 }
3139
3140 if (!empty($api_key)) {
3141 $safe_key = sanitize_file_name( $api_key );
3142 $file_path = ABSPATH . $safe_key . '.txt';
3143 $real_path = realpath( ABSPATH );
3144 if ( false === $real_path || 0 !== strpos( realpath( dirname( $file_path ) ), $real_path ) ) {
3145 error_log( 'Bing IndexNow: Refusing to write outside ABSPATH' );
3146 return false;
3147 }
3148 $file_result = file_put_contents( $file_path, $safe_key );
3149
3150 if ($file_result === false) {
3151 error_log('Bing IndexNow: Failed to create API key verification file at ' . $file_path);
3152 return false;
3153 }
3154
3155 $current_options = Metasync::get_option();
3156 if (!isset($current_options['seo_controls']['enable_binginstantindex']) || $current_options['seo_controls']['enable_binginstantindex'] !== 'true') {
3157 $current_options['seo_controls']['enable_binginstantindex'] = 'true';
3158 Metasync::set_option($current_options);
3159 }
3160 }
3161
3162 return $result;
3163 }
3164
3165 /**
3166 * Sanitize the metasync_llms_txt_settings option.
3167 *
3168 * The Settings API delivers raw form data; we coerce it into a predictable
3169 * shape so the generator and MCP tools can rely on the structure.
3170 *
3171 * @param mixed $input Raw form data.
3172 * @return array
3173 */
3174 public function sanitize_llms_txt_settings($input)
3175 {
3176 if (!is_array($input)) {
3177 $input = array();
3178 }
3179
3180 $existing = get_option('metasync_llms_txt_settings', array());
3181 if (!is_array($existing)) {
3182 $existing = array();
3183 }
3184
3185 $output = $existing;
3186
3187 $output['enabled'] = !empty($input['enabled'])
3188 ? (bool) filter_var($input['enabled'], FILTER_VALIDATE_BOOLEAN)
3189 : false;
3190
3191 if (isset($input['post_types']) && is_array($input['post_types'])) {
3192 $output['post_types'] = array_values(array_filter(array_map('sanitize_text_field', $input['post_types'])));
3193 } else {
3194 $output['post_types'] = array('page', 'post');
3195 }
3196
3197 if (isset($input['max_posts'])) {
3198 $max = absint($input['max_posts']);
3199 if ($max < 1) {
3200 $max = 1;
3201 }
3202 if ($max > 500) {
3203 $max = 500;
3204 }
3205 $output['max_posts'] = $max;
3206 }
3207
3208 if (isset($input['excluded_ids'])) {
3209 if (is_array($input['excluded_ids'])) {
3210 $ids = $input['excluded_ids'];
3211 } else {
3212 $ids = preg_split('/[\s,]+/', (string) $input['excluded_ids']);
3213 }
3214 $output['excluded_ids'] = array_values(array_filter(array_map('absint', (array) $ids)));
3215 }
3216
3217 if (isset($input['custom_description'])) {
3218 $output['custom_description'] = sanitize_text_field((string) $input['custom_description']);
3219 }
3220
3221 $output['llms_full_enabled'] = !empty($input['llms_full_enabled'])
3222 ? (bool) filter_var($input['llms_full_enabled'], FILTER_VALIDATE_BOOLEAN)
3223 : false;
3224
3225 if (isset($input['max_posts_full'])) {
3226 $max_full = absint($input['max_posts_full']);
3227 if ($max_full < 1) {
3228 $max_full = 1;
3229 }
3230 if ($max_full > 500) {
3231 $max_full = 500;
3232 }
3233 $output['max_posts_full'] = $max_full;
3234 }
3235
3236 return $output;
3237 }
3238 }
3239