PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.6
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.6
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-settings-registration.php

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

3,168 lines 157.7 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 '<table class="widefat" style="max-width:560px;">';
1370 echo '<thead><tr><th>Label</th><th>URL</th><th>Open in new tab</th></tr></thead><tbody>';
1371 foreach (array_slice($links, 0, 5) as $i => $link) {
1372 $label = esc_attr($link['label'] ?? '');
1373 $url = esc_attr($link['url'] ?? '');
1374 $external = !empty($link['external']);
1375 printf(
1376 '<tr>
1377 <td><input type="text" name="%s[whitelabel][quick_links][%d][label]" value="%s" placeholder="Link label" style="width:100%%" /></td>
1378 <td><input type="url" name="%s[whitelabel][quick_links][%d][url]" value="%s" placeholder="https://…" style="width:100%%" /></td>
1379 <td style="text-align:center"><input type="checkbox" name="%s[whitelabel][quick_links][%d][external]" value="1" %s /></td>
1380 </tr>',
1381 esc_attr($option_key), $i, $label,
1382 esc_attr($option_key), $i, $url,
1383 esc_attr($option_key), $i, checked($external, true, false)
1384 );
1385 }
1386 echo '</tbody></table>';
1387 },
1388 $page_slug . '_branding',
1389 $SECTION_METASYNC
1390 );
1391
1392 // ======================================================================
1393 // Schema Markup Settings — dedicated page (SEO → Schema Markup)
1394 // ======================================================================
1395 $schema_page = $page_slug . '_schema-markup';
1396
1397 add_settings_section(
1398 'metasync_schema_org',
1399 'Organization Schema (Site-Wide)',
1400 function() {
1401 echo '<p>This Organization schema is injected on every page of your site.</p>';
1402 },
1403 $schema_page
1404 );
1405
1406 add_settings_field(
1407 'schema_org_name',
1408 'Organization Name',
1409 function() use ($option_key) {
1410 $schema = Metasync::get_option('schema') ?? [];
1411 $value = $schema['org_name'] ?? '';
1412 printf('<input type="text" name="' . $option_key . '[schema][org_name]" value="%s" size="40" placeholder="Your organization name" />', esc_attr($value));
1413 },
1414 $schema_page,
1415 'metasync_schema_org'
1416 );
1417
1418 add_settings_field(
1419 'schema_org_url',
1420 'Organization URL',
1421 function() use ($option_key) {
1422 $schema = Metasync::get_option('schema') ?? [];
1423 $value = $schema['org_url'] ?? '';
1424 printf('<input type="url" name="' . $option_key . '[schema][org_url]" value="%s" size="40" placeholder="https://example.com" />', esc_attr($value));
1425 },
1426 $schema_page,
1427 'metasync_schema_org'
1428 );
1429
1430 add_settings_field(
1431 'schema_org_logo',
1432 'Organization Logo URL',
1433 function() use ($option_key) {
1434 $schema = Metasync::get_option('schema') ?? [];
1435 $value = $schema['org_logo'] ?? '';
1436 printf('<input type="url" name="' . $option_key . '[schema][org_logo]" value="%s" size="40" placeholder="https://example.com/logo.png" />', esc_attr($value));
1437 },
1438 $schema_page,
1439 'metasync_schema_org'
1440 );
1441
1442 add_settings_field(
1443 'schema_org_contact_telephone',
1444 'Contact Telephone',
1445 function() use ($option_key) {
1446 $schema = Metasync::get_option('schema') ?? [];
1447 $value = $schema['org_contact_telephone'] ?? '';
1448 printf('<input type="text" name="' . $option_key . '[schema][org_contact_telephone]" value="%s" size="30" placeholder="+1-555-1234" />', esc_attr($value));
1449 },
1450 $schema_page,
1451 'metasync_schema_org'
1452 );
1453
1454 add_settings_field(
1455 'schema_org_contact_type',
1456 'Contact Type',
1457 function() use ($option_key) {
1458 $schema = Metasync::get_option('schema') ?? [];
1459 $value = $schema['org_contact_type'] ?? '';
1460 printf('<input type="text" name="' . $option_key . '[schema][org_contact_type]" value="%s" size="30" placeholder="customer support" />', esc_attr($value));
1461 },
1462 $schema_page,
1463 'metasync_schema_org'
1464 );
1465
1466 add_settings_field(
1467 'schema_org_same_as',
1468 'Social Profile URLs',
1469 function() use ($option_key) {
1470 $schema = Metasync::get_option('schema') ?? [];
1471 $value = $schema['org_same_as'] ?? '';
1472 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));
1473 echo '<p class="description">Enter one social profile URL per line.</p>';
1474 },
1475 $schema_page,
1476 'metasync_schema_org'
1477 );
1478
1479 add_settings_section(
1480 'metasync_schema_website',
1481 'WebSite Schema (Homepage Only)',
1482 function() {
1483 echo '<p>This WebSite schema is injected on the homepage only. Enables the Google Sitelinks Searchbox.</p>';
1484 },
1485 $schema_page
1486 );
1487
1488 add_settings_field(
1489 'schema_website_name',
1490 'WebSite Name',
1491 function() use ($option_key) {
1492 $schema = Metasync::get_option('schema') ?? [];
1493 $value = $schema['website_name'] ?? '';
1494 printf('<input type="text" name="' . $option_key . '[schema][website_name]" value="%s" size="40" placeholder="Your website name" />', esc_attr($value));
1495 },
1496 $schema_page,
1497 'metasync_schema_website'
1498 );
1499
1500 add_settings_field(
1501 'schema_website_url',
1502 'WebSite URL',
1503 function() use ($option_key) {
1504 $schema = Metasync::get_option('schema') ?? [];
1505 $value = $schema['website_url'] ?? '';
1506 printf('<input type="url" name="' . $option_key . '[schema][website_url]" value="%s" size="40" placeholder="https://example.com" />', esc_attr($value));
1507 },
1508 $schema_page,
1509 'metasync_schema_website'
1510 );
1511
1512 add_settings_field(
1513 'schema_website_searchbox',
1514 'Enable Sitelinks Searchbox',
1515 function() use ($option_key) {
1516 $schema = Metasync::get_option('schema') ?? [];
1517 $value = $schema['website_searchbox'] ?? false;
1518 printf(
1519 '<input type="checkbox" name="' . $option_key . '[schema][website_searchbox]" value="1" %s />',
1520 checked($value, true, false)
1521 );
1522 echo '<span class="description">Adds SearchAction to WebSite schema for Google Sitelinks Searchbox.</span>';
1523 },
1524 $schema_page,
1525 'metasync_schema_website'
1526 );
1527
1528 add_settings_section(
1529 'metasync_schema_compat',
1530 'Compatibility',
1531 function() {
1532 echo '<p>Control how MetaSync interacts with other plugins that output structured data.</p>';
1533 },
1534 $schema_page
1535 );
1536
1537 add_settings_field(
1538 'schema_override_woocommerce',
1539 'Override WooCommerce Product Schema',
1540 function() use ($option_key) {
1541 $schema = Metasync::get_option('schema') ?? [];
1542 $value = $schema['override_woocommerce_schema'] ?? false;
1543 printf(
1544 '<input type="checkbox" name="' . $option_key . '[schema][override_woocommerce_schema]" value="1" %s />',
1545 checked($value, true, false)
1546 );
1547 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>';
1548 },
1549 $schema_page,
1550 'metasync_schema_compat'
1551 );
1552
1553 // ----------------------------------------------------------------
1554 // Site Verification page
1555 // ----------------------------------------------------------------
1556 add_settings_section(
1557 $SECTION_SEARCHENGINE,
1558 '',
1559 function() {},
1560 $page_slug . '_searchengines-verification'
1561 );
1562
1563 add_settings_field(
1564 'google_site_verification',
1565 'Google Search Console',
1566 array( Metasync_Settings_Fields::instance(), 'google_site_verification_callback' ),
1567 $page_slug . '_searchengines-verification',
1568 $SECTION_SEARCHENGINE
1569 );
1570
1571 add_settings_field(
1572 'bing_site_verification',
1573 'Bing Webmaster Tools',
1574 array( Metasync_Settings_Fields::instance(), 'bing_site_verification_callback' ),
1575 $page_slug . '_searchengines-verification',
1576 $SECTION_SEARCHENGINE
1577 );
1578
1579 add_settings_field(
1580 'yandex_site_verification',
1581 'Yandex Webmaster',
1582 array( Metasync_Settings_Fields::instance(), 'yandex_site_verification_callback' ),
1583 $page_slug . '_searchengines-verification',
1584 $SECTION_SEARCHENGINE
1585 );
1586
1587 add_settings_field(
1588 'pinterest_site_verification',
1589 'Pinterest',
1590 array( Metasync_Settings_Fields::instance(), 'pinterest_site_verification_callback' ),
1591 $page_slug . '_searchengines-verification',
1592 $SECTION_SEARCHENGINE
1593 );
1594
1595 add_settings_field(
1596 'baidu_site_verification',
1597 'Baidu',
1598 function() use ($option_key) {
1599 $value = Metasync::get_option('searchengines')['baidu_site_verification'] ?? '';
1600 printf( '<input type="text" id="baidu_site_verification" name="' . $option_key . '[searchengines][baidu_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1601 },
1602 $page_slug . '_searchengines-verification',
1603 $SECTION_SEARCHENGINE
1604 );
1605
1606 add_settings_field(
1607 'alexa_site_verification',
1608 'Alexa',
1609 function() use ($option_key) {
1610 $value = Metasync::get_option('searchengines')['alexa_site_verification'] ?? '';
1611 printf( '<input type="text" id="alexa_site_verification" name="' . $option_key . '[searchengines][alexa_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1612 },
1613 $page_slug . '_searchengines-verification',
1614 $SECTION_SEARCHENGINE
1615 );
1616
1617 add_settings_field(
1618 'norton_save_site_verification',
1619 'Norton Safe Web',
1620 function() use ($option_key) {
1621 $value = Metasync::get_option('searchengines')['norton_save_site_verification'] ?? '';
1622 printf( '<input type="text" id="norton_save_site_verification" name="' . $option_key . '[searchengines][norton_save_site_verification]" value="%s" size="50" />', esc_attr( $value ) );
1623 },
1624 $page_slug . '_searchengines-verification',
1625 $SECTION_SEARCHENGINE
1626 );
1627
1628 // ----------------------------------------------------------------
1629 // Code Snippets page
1630 // ----------------------------------------------------------------
1631 add_settings_section(
1632 $SECTION_CODESNIPPETS,
1633 '',
1634 function() {},
1635 $page_slug . '_code-snippets'
1636 );
1637
1638 add_settings_field(
1639 'header_snippet',
1640 'Header Code',
1641 array( Metasync_Settings_Fields::instance(), 'header_snippets_callback' ),
1642 $page_slug . '_code-snippets',
1643 $SECTION_CODESNIPPETS
1644 );
1645
1646 add_settings_field(
1647 'footer_snippet',
1648 'Footer Code',
1649 array( Metasync_Settings_Fields::instance(), 'footer_snippets_callback' ),
1650 $page_slug . '_code-snippets',
1651 $SECTION_CODESNIPPETS
1652 );
1653
1654 // ----------------------------------------------------------------
1655 // Local Business page
1656 // ----------------------------------------------------------------
1657 add_settings_section(
1658 $SECTION_LOCALSEO,
1659 '',
1660 function() {},
1661 $page_slug . '_local-seo'
1662 );
1663
1664 $fields_instance = Metasync_Settings_Fields::instance();
1665
1666 add_settings_field( 'local_seo_person_organization', 'Type', array( $fields_instance, 'local_seo_person_organization_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1667 add_settings_field( 'local_seo_name', 'Name', array( $fields_instance, 'local_seo_name_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1668 add_settings_field( 'local_seo_logo', 'Logo', array( $fields_instance, 'local_seo_logo_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1669 add_settings_field( 'local_seo_url', 'Website URL', array( $fields_instance, 'local_seo_url_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1670 add_settings_field( 'local_seo_email', 'Email', array( $fields_instance, 'local_seo_email_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1671 add_settings_field( 'local_seo_phone', 'Phone', array( $fields_instance, 'local_seo_phone_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1672 add_settings_field( 'local_seo_address', 'Address', array( $fields_instance, 'local_seo_address_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1673 add_settings_field( 'local_seo_business_type', 'Business Type', array( $fields_instance, 'local_seo_business_type_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1674 add_settings_field( 'local_seo_opening_hours', 'Opening Hours', array( $fields_instance, 'local_seo_opening_hours_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1675 add_settings_field( 'local_seo_phone_numbers', 'Phone Numbers', array( $fields_instance, 'local_seo_phone_numbers_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1676 add_settings_field( 'local_seo_price_range', 'Price Range', array( $fields_instance, 'local_seo_price_range_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1677 add_settings_field( 'local_seo_about_page', 'About Page', array( $fields_instance, 'local_seo_about_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1678 add_settings_field( 'local_seo_contact_page', 'Contact Page', array( $fields_instance, 'local_seo_contact_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1679 add_settings_field( 'local_seo_map_key', 'Google Maps Key', array( $fields_instance, 'local_seo_map_key_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1680 add_settings_field( 'local_seo_geo_coordinates', 'Geo Coordinates', array( $fields_instance, 'local_seo_geo_coordinates_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
1681
1682 // ----------------------------------------------------------------
1683 // Breadcrumbs page
1684 // ----------------------------------------------------------------
1685 add_settings_section(
1686 $SECTION_BREADCRUMBS,
1687 '',
1688 function() {},
1689 $page_slug . '_breadcrumbs'
1690 );
1691
1692 add_settings_field(
1693 'breadcrumbs_enabled',
1694 'Enable Breadcrumbs',
1695 function() use ($option_key) {
1696 $value = Metasync::get_option('breadcrumbs')['enabled'] ?? true;
1697 printf(
1698 '<input type="checkbox" id="breadcrumbs_enabled" name="' . $option_key . '[breadcrumbs][enabled]" value="1" %s />',
1699 checked(1, $value, false)
1700 );
1701 echo '<p class="description">Output breadcrumb trail HTML on your site.</p>';
1702 },
1703 $page_slug . '_breadcrumbs',
1704 $SECTION_BREADCRUMBS
1705 );
1706
1707 add_settings_field(
1708 'breadcrumbs_separator',
1709 'Separator',
1710 function() use ($option_key) {
1711 $value = Metasync::get_option('breadcrumbs')['separator'] ?? '&raquo;';
1712 printf(
1713 '<input type="text" id="breadcrumbs_separator" name="' . $option_key . '[breadcrumbs][separator]" value="%s" size="10" />',
1714 esc_attr($value)
1715 );
1716 echo '<p class="description">Character or HTML entity shown between crumbs (e.g. &raquo; or /).</p>';
1717 },
1718 $page_slug . '_breadcrumbs',
1719 $SECTION_BREADCRUMBS
1720 );
1721
1722 add_settings_field(
1723 'breadcrumbs_home_label',
1724 'Home Label',
1725 function() use ($option_key) {
1726 $value = Metasync::get_option('breadcrumbs')['home_label'] ?? 'Home';
1727 printf(
1728 '<input type="text" id="breadcrumbs_home_label" name="' . $option_key . '[breadcrumbs][home_label]" value="%s" size="30" />',
1729 esc_attr($value)
1730 );
1731 },
1732 $page_slug . '_breadcrumbs',
1733 $SECTION_BREADCRUMBS
1734 );
1735
1736 add_settings_field(
1737 'breadcrumbs_home_url',
1738 'Home URL',
1739 function() use ($option_key) {
1740 $value = Metasync::get_option('breadcrumbs')['home_url'] ?? '';
1741 printf(
1742 '<input type="url" id="breadcrumbs_home_url" name="' . $option_key . '[breadcrumbs][home_url]" value="%s" size="50" />',
1743 esc_attr($value)
1744 );
1745 echo '<p class="description">Leave blank to use the site home URL.</p>';
1746 },
1747 $page_slug . '_breadcrumbs',
1748 $SECTION_BREADCRUMBS
1749 );
1750
1751 add_settings_field(
1752 'breadcrumbs_show_current_page',
1753 'Show Current Page',
1754 function() use ($option_key) {
1755 $value = Metasync::get_option('breadcrumbs')['show_current_page'] ?? true;
1756 printf(
1757 '<input type="checkbox" id="breadcrumbs_show_current_page" name="' . $option_key . '[breadcrumbs][show_current_page]" value="1" %s />',
1758 checked(1, $value, false)
1759 );
1760 echo '<p class="description">Include the current page as the last (non-linked) crumb.</p>';
1761 },
1762 $page_slug . '_breadcrumbs',
1763 $SECTION_BREADCRUMBS
1764 );
1765
1766 add_settings_field(
1767 'breadcrumbs_prefix_text',
1768 'Prefix Text',
1769 function() use ($option_key) {
1770 $value = Metasync::get_option('breadcrumbs')['prefix_text'] ?? '';
1771 printf(
1772 '<input type="text" id="breadcrumbs_prefix_text" name="' . $option_key . '[breadcrumbs][prefix_text]" value="%s" size="30" />',
1773 esc_attr($value)
1774 );
1775 echo '<p class="description">Optional text before the breadcrumb trail (e.g. "You are here:").</p>';
1776 },
1777 $page_slug . '_breadcrumbs',
1778 $SECTION_BREADCRUMBS
1779 );
1780
1781 add_settings_field(
1782 'breadcrumbs_archive_label_format',
1783 'Archive Label Format',
1784 function() use ($option_key) {
1785 $value = Metasync::get_option('breadcrumbs')['archive_label_format'] ?? '{name}';
1786 printf(
1787 '<input type="text" id="breadcrumbs_archive_label_format" name="' . $option_key . '[breadcrumbs][archive_label_format]" value="%s" size="30" />',
1788 esc_attr($value)
1789 );
1790 echo '<p class="description">Format for archive crumb labels. Use <code>{name}</code> as placeholder for the archive name.</p>';
1791 },
1792 $page_slug . '_breadcrumbs',
1793 $SECTION_BREADCRUMBS
1794 );
1795
1796 # Open Graph / Article meta toggles — surface each new tag family as its
1797 # own opt-out checkbox on the main Settings page (Open Graph accordion).
1798 add_settings_field(
1799 'og_image_dimensions',
1800 'OG Image Dimensions',
1801 array(Metasync_Settings_Fields::instance(), 'og_image_dimensions_callback'),
1802 $page_slug . '_general',
1803 $SECTION_METASYNC
1804 );
1805
1806 add_settings_field(
1807 'article_timestamps',
1808 'Article Timestamps',
1809 array(Metasync_Settings_Fields::instance(), 'article_timestamps_callback'),
1810 $page_slug . '_general',
1811 $SECTION_METASYNC
1812 );
1813
1814 add_settings_field(
1815 'article_author',
1816 'Article Author',
1817 array(Metasync_Settings_Fields::instance(), 'article_author_callback'),
1818 $page_slug . '_general',
1819 $SECTION_METASYNC
1820 );
1821
1822 add_settings_field(
1823 'article_section',
1824 'Article Section',
1825 array(Metasync_Settings_Fields::instance(), 'article_section_callback'),
1826 $page_slug . '_general',
1827 $SECTION_METASYNC
1828 );
1829
1830 add_settings_field(
1831 'article_tags',
1832 'Article Tags',
1833 array(Metasync_Settings_Fields::instance(), 'article_tags_callback'),
1834 $page_slug . '_general',
1835 $SECTION_METASYNC
1836 );
1837
1838 add_settings_field(
1839 'twitter_image_alt',
1840 'Twitter Image Alt',
1841 array(Metasync_Settings_Fields::instance(), 'twitter_image_alt_callback'),
1842 $page_slug . '_general',
1843 $SECTION_METASYNC
1844 );
1845
1846 }
1847
1848 /**
1849 * Sanitize each setting field as needed.
1850 *
1851 * Formerly Metasync_Admin::sanitize().
1852 *
1853 * @param array $input Contains all settings fields as array keys
1854 * @return array Sanitized settings
1855 */
1856 public function sanitize($input)
1857 {
1858 $new_input = Metasync::get_option();
1859 $whitelabel_validation_failed = false;
1860
1861 # Determine which tab is being submitted (same logic as AJAX handler)
1862 # Check POST first (from AJAX), then GET (from form action URL)
1863 $active_tab = 'general';
1864 if (isset($_POST['active_tab'])) {
1865 $active_tab = sanitize_text_field($_POST['active_tab']);
1866 } elseif (isset($_GET['tab'])) {
1867 $active_tab = sanitize_text_field($_GET['tab']);
1868 } elseif (isset($_POST['_wp_http_referer'])) {
1869 # Parse referer URL to get tab parameter
1870 $referer = wp_parse_url($_POST['_wp_http_referer']);
1871 if (isset($referer['query'])) {
1872 parse_str($referer['query'], $referer_params);
1873 if (isset($referer_params['tab'])) {
1874 $active_tab = sanitize_text_field($referer_params['tab']);
1875 }
1876 }
1877 }
1878 $general_tab_submitted = ($active_tab === 'general');
1879
1880 // General Settings
1881 if (isset($input['general']['apikey'])) {
1882 $new_input['general']['apikey'] = sanitize_text_field($input['general']['apikey']);
1883 }
1884
1885 // Meta Box Visibility Settings
1886 if (isset($input['general']) && $general_tab_submitted) {
1887 $checkbox_fields = [
1888 'disable_common_robots_metabox',
1889 'disable_advance_robots_metabox',
1890 'disable_redirection_metabox',
1891 'disable_canonical_metabox',
1892 'disable_social_opengraph_metabox',
1893 'disable_schema_markup_metabox',
1894 'open_external_links'
1895 ];
1896
1897 foreach ($checkbox_fields as $field) {
1898 if (isset($input['general'][$field])) {
1899 $new_input['general'][$field] = filter_var($input['general'][$field], FILTER_VALIDATE_BOOLEAN);
1900 } else {
1901 $new_input['general'][$field] = false;
1902 }
1903 }
1904 }
1905
1906 // Site Verification Settings
1907 if (isset($input['searchengines']['bing_site_verification'])) {
1908 $new_input['searchengines']['bing_site_verification'] = sanitize_text_field($input['searchengines']['bing_site_verification']);
1909 }
1910 if (isset($input['searchengines']['baidu_site_verification'])) {
1911 $new_input['searchengines']['baidu_site_verification'] = sanitize_text_field($input['searchengines']['baidu_site_verification']);
1912 }
1913 if (isset($input['searchengines']['alexa_site_verification'])) {
1914 $new_input['searchengines']['alexa_site_verification'] = sanitize_text_field($input['searchengines']['alexa_site_verification']);
1915 }
1916 if (isset($input['searchengines']['yandex_site_verification'])) {
1917 $new_input['searchengines']['yandex_site_verification'] = sanitize_text_field($input['searchengines']['yandex_site_verification']);
1918 }
1919 if (isset($input['searchengines']['google_site_verification'])) {
1920 $new_input['searchengines']['google_site_verification'] = sanitize_text_field($input['searchengines']['google_site_verification']);
1921 }
1922 if (isset($input['searchengines']['pinterest_site_verification'])) {
1923 $new_input['searchengines']['pinterest_site_verification'] = sanitize_text_field($input['searchengines']['pinterest_site_verification']);
1924 }
1925 if (isset($input['searchengines']['norton_save_site_verification'])) {
1926 $new_input['searchengines']['norton_save_site_verification'] = sanitize_text_field($input['searchengines']['norton_save_site_verification']);
1927 }
1928
1929 // Local Business SEO Settings
1930 if (isset($input['localseo']['local_seo_person_organization'])) {
1931 $new_input['localseo']['local_seo_person_organization'] = sanitize_text_field($input['localseo']['local_seo_person_organization']);
1932 }
1933 if (isset($input['localseo']['local_seo_name'])) {
1934 $new_input['localseo']['local_seo_name'] = sanitize_text_field($input['localseo']['local_seo_name']);
1935 }
1936 if (isset($input['localseo']['local_seo_logo'])) {
1937 $new_input['localseo']['local_seo_logo'] = sanitize_url($input['localseo']['local_seo_logo']);
1938 }
1939 if (isset($input['localseo']['local_seo_url'])) {
1940 $new_input['localseo']['local_seo_url'] = sanitize_url($input['localseo']['local_seo_url']);
1941 }
1942 if (isset($input['localseo']['local_seo_email'])) {
1943 $new_input['localseo']['local_seo_email'] = sanitize_email($input['localseo']['local_seo_email']);
1944 }
1945 if (isset($input['localseo']['local_seo_phone'])) {
1946 $new_input['localseo']['local_seo_phone'] = sanitize_text_field($input['localseo']['local_seo_phone']);
1947 }
1948 if (isset($input['localseo']['address']['street'])) {
1949 $new_input['localseo']['address']['street'] = sanitize_text_field($input['localseo']['address']['street']);
1950 }
1951 if (isset($input['localseo']['address']['locality'])) {
1952 $new_input['localseo']['address']['locality'] = sanitize_text_field($input['localseo']['address']['locality']);
1953 }
1954 if (isset($input['localseo']['address']['region'])) {
1955 $new_input['localseo']['address']['region'] = sanitize_text_field($input['localseo']['address']['region']);
1956 }
1957 if (isset($input['localseo']['address']['postancode'])) {
1958 $new_input['localseo']['address']['postancode'] = sanitize_text_field($input['localseo']['address']['postancode']);
1959 }
1960 if (isset($input['localseo']['address']['country'])) {
1961 $new_input['localseo']['address']['country'] = sanitize_text_field($input['localseo']['address']['country']);
1962 }
1963 if (isset($input['localseo']['local_seo_business_type'])) {
1964 $new_input['localseo']['local_seo_business_type'] = sanitize_text_field($input['localseo']['local_seo_business_type']);
1965 }
1966 if (isset($input['localseo']['local_seo_hours_format'])) {
1967 $new_input['localseo']['local_seo_hours_format'] = sanitize_text_field($input['localseo']['local_seo_hours_format']);
1968 }
1969 if (isset($input['localseo']['days'])) {
1970 $new_input['localseo']['days'] = sanitize_text_field($input['localseo']['days']);
1971 }
1972 if (isset($input['localseo']['times'])) {
1973 $new_input['localseo']['times'] = sanitize_text_field($input['localseo']['times']);
1974 }
1975 if (isset($input['localseo']['phonetype'])) {
1976 $new_input['localseo']['phonetype'] = sanitize_text_field($input['localseo']['phonetype']);
1977 }
1978 if (isset($input['localseo']['phonenumber'])) {
1979 $new_input['localseo']['phonenumber'] = sanitize_text_field($input['localseo']['phonenumber']);
1980 }
1981 if (isset($input['localseo']['local_seo_price_range'])) {
1982 $new_input['localseo']['local_seo_price_range'] = sanitize_text_field($input['localseo']['local_seo_price_range']);
1983 }
1984 if (isset($input['localseo']['local_seo_about_page'])) {
1985 $new_input['localseo']['local_seo_about_page'] = sanitize_text_field($input['localseo']['local_seo_about_page']);
1986 }
1987 if (isset($input['localseo']['local_seo_contact_page'])) {
1988 $new_input['localseo']['local_seo_contact_page'] = sanitize_text_field($input['localseo']['local_seo_contact_page']);
1989 }
1990 if (isset($input['localseo']['local_seo_map_key'])) {
1991 $new_input['localseo']['local_seo_map_key'] = sanitize_text_field($input['localseo']['local_seo_map_key']);
1992 }
1993 if (isset($input['localseo']['local_seo_geo_coordinates'])) {
1994 $new_input['localseo']['local_seo_geo_coordinates'] = sanitize_text_field($input['localseo']['local_seo_geo_coordinates']);
1995 }
1996
1997 // Breadcrumbs Settings
1998 if (isset($input['breadcrumbs'])) {
1999 $new_input['breadcrumbs']['enabled'] = !empty($input['breadcrumbs']['enabled']);
2000 $new_input['breadcrumbs']['show_current_page'] = !empty($input['breadcrumbs']['show_current_page']);
2001 $new_input['breadcrumbs']['disable_schema'] = !empty($input['breadcrumbs']['disable_schema']);
2002 if (isset($input['breadcrumbs']['separator'])) {
2003 $new_input['breadcrumbs']['separator'] = sanitize_text_field($input['breadcrumbs']['separator']);
2004 }
2005 if (isset($input['breadcrumbs']['home_label'])) {
2006 $new_input['breadcrumbs']['home_label'] = sanitize_text_field($input['breadcrumbs']['home_label']);
2007 }
2008 if (isset($input['breadcrumbs']['home_url'])) {
2009 $new_input['breadcrumbs']['home_url'] = esc_url_raw($input['breadcrumbs']['home_url']);
2010 }
2011 if (isset($input['breadcrumbs']['prefix_text'])) {
2012 $new_input['breadcrumbs']['prefix_text'] = sanitize_text_field($input['breadcrumbs']['prefix_text']);
2013 }
2014 if (isset($input['breadcrumbs']['archive_label_format'])) {
2015 $new_input['breadcrumbs']['archive_label_format'] = sanitize_text_field($input['breadcrumbs']['archive_label_format']);
2016 }
2017 }
2018
2019 // Code Snippets Settings
2020 if (isset($input['codesnippets']['header_snippet'])) {
2021 $new_input['codesnippets']['header_snippet'] = sanitize_text_field($input['codesnippets']['header_snippet']);
2022 }
2023 if (isset($input['codesnippets']['footer_snippet'])) {
2024 $new_input['codesnippets']['footer_snippet'] = sanitize_text_field($input['codesnippets']['footer_snippet']);
2025 }
2026
2027
2028 // Optimal Settings
2029 if (isset($input['optimal_settings']['no_index_posts'])) {
2030 $new_input['optimal_settings']['no_index_posts'] = boolval($input['optimal_settings']['no_index_posts']);
2031 }
2032 if (isset($input['optimal_settings']['no_follow_links'])) {
2033 $new_input['optimal_settings']['no_follow_links'] = boolval($input['optimal_settings']['no_follow_links']);
2034 }
2035 if (isset($input['optimal_settings']['open_external_links'])) {
2036 $new_input['optimal_settings']['open_external_links'] = boolval($input['optimal_settings']['open_external_links']);
2037 }
2038 if (isset($input['optimal_settings']['add_alt_image_tags'])) {
2039 $new_input['optimal_settings']['add_alt_image_tags'] = boolval($input['optimal_settings']['add_alt_image_tags']);
2040 }
2041 if (isset($input['optimal_settings']['add_title_image_tags'])) {
2042 $new_input['optimal_settings']['add_title_image_tags'] = boolval($input['optimal_settings']['add_title_image_tags']);
2043 }
2044
2045 // Site Information - Optimal Settings
2046 if (isset($input['optimal_settings']['site_info']['type'])) {
2047 $new_input['optimal_settings']['site_info']['type'] = sanitize_text_field($input['optimal_settings']['site_info']['type']);
2048 }
2049 if (isset($input['optimal_settings']['site_info']['business_type'])) {
2050 $new_input['optimal_settings']['site_info']['business_type'] = sanitize_text_field($input['optimal_settings']['site_info']['business_type']);
2051 }
2052 if (isset($input['optimal_settings']['site_info']['company_name'])) {
2053 $new_input['optimal_settings']['site_info']['company_name'] = sanitize_text_field($input['optimal_settings']['site_info']['company_name']);
2054 }
2055 if (isset($input['optimal_settings']['site_info']['google_logo'])) {
2056 $new_input['optimal_settings']['site_info']['google_logo'] = sanitize_url($input['optimal_settings']['site_info']['google_logo']);
2057 }
2058 if (isset($input['optimal_settings']['site_info']['social_share_image'])) {
2059 $new_input['optimal_settings']['site_info']['social_share_image'] = sanitize_url($input['optimal_settings']['site_info']['social_share_image']);
2060 }
2061
2062 // Common Setting - Global Settings
2063 $common_robots_field = isset($input['common_robots_meta']) ? 'common_robots_meta' : 'common_robots_mata';
2064
2065 if (isset($input[$common_robots_field]['index'])) {
2066 $new_input['common_robots_meta']['index'] = boolval($input[$common_robots_field]['index']);
2067 }
2068 if (isset($input[$common_robots_field]['noindex'])) {
2069 $new_input['common_robots_meta']['noindex'] = boolval($input[$common_robots_field]['noindex']);
2070 }
2071 if (isset($input[$common_robots_field]['nofollow'])) {
2072 $new_input['common_robots_meta']['nofollow'] = boolval($input[$common_robots_field]['nofollow']);
2073 }
2074 if (isset($input[$common_robots_field]['noarchive'])) {
2075 $new_input['common_robots_meta']['noarchive'] = boolval($input[$common_robots_field]['noarchive']);
2076 }
2077 if (isset($input[$common_robots_field]['noimageindex'])) {
2078 $new_input['common_robots_meta']['noimageindex'] = boolval($input[$common_robots_field]['noimageindex']);
2079 }
2080 if (isset($input[$common_robots_field]['nosnippet'])) {
2081 $new_input['common_robots_meta']['nosnippet'] = boolval($input[$common_robots_field]['nosnippet']);
2082 }
2083
2084 // Advance Setting - Global Settings
2085 $advance_robots_field = isset($input['advance_robots_meta']) ? 'advance_robots_meta' : 'advance_robots_mata';
2086
2087 if (isset($input[$advance_robots_field]['max-snippet']['enable'])) {
2088 $new_input['advance_robots_meta']['max-snippet']['enable'] = boolval($input[$advance_robots_field]['max-snippet']['enable']);
2089 }
2090 if (isset($input[$advance_robots_field]['max-snippet']['length'])) {
2091 $new_input['advance_robots_meta']['max-snippet']['length'] = sanitize_text_field($input[$advance_robots_field]['max-snippet']['length']);
2092 }
2093 if (isset($input[$advance_robots_field]['max-video-preview']['enable'])) {
2094 $new_input['advance_robots_meta']['max-video-preview']['enable'] = boolval($input[$advance_robots_field]['max-video-preview']['enable']);
2095 }
2096 if (isset($input[$advance_robots_field]['max-video-preview']['length'])) {
2097 $new_input['advance_robots_meta']['max-video-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-video-preview']['length']);
2098 }
2099 if (isset($input[$advance_robots_field]['max-image-preview']['enable'])) {
2100 $new_input['advance_robots_meta']['max-image-preview']['enable'] = boolval($input[$advance_robots_field]['max-image-preview']['enable']);
2101 }
2102 if (isset($input[$advance_robots_field]['max-image-preview']['length'])) {
2103 $new_input['advance_robots_meta']['max-image-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-image-preview']['length']);
2104 }
2105
2106 // Social meta settings
2107 if (isset($input['social_meta']['facebook_page_url'])) {
2108 $new_input['social_meta']['facebook_page_url'] = sanitize_text_field($input['social_meta']['facebook_page_url']);
2109 }
2110 if (isset($input['social_meta']['facebook_authorship'])) {
2111 $new_input['social_meta']['facebook_authorship'] = sanitize_text_field($input['social_meta']['facebook_authorship']);
2112 }
2113 if (isset($input['social_meta']['facebook_admin'])) {
2114 $new_input['social_meta']['facebook_admin'] = sanitize_text_field($input['social_meta']['facebook_admin']);
2115 }
2116 if (isset($input['social_meta']['facebook_app'])) {
2117 $new_input['social_meta']['facebook_app'] = sanitize_text_field($input['social_meta']['facebook_app']);
2118 }
2119 if (isset($input['social_meta']['facebook_secret'])) {
2120 $new_input['social_meta']['facebook_secret'] = sanitize_text_field($input['social_meta']['facebook_secret']);
2121 }
2122 if (isset($input['social_meta']['twitter_username'])) {
2123 $new_input['social_meta']['twitter_username'] = sanitize_text_field($input['social_meta']['twitter_username']);
2124 }
2125
2126 // Common Meta Settings — Open Graph / article toggle checkboxes.
2127 // Unchecked HTML checkboxes are absent from $input, so an explicit
2128 // 'true'/'false' round-trip is required to persist opt-outs.
2129 if (isset($input['common_meta_settings'])) {
2130 $og_toggle_fields = [
2131 'og_image_dimensions',
2132 'article_timestamps',
2133 'article_author',
2134 'article_section',
2135 'article_tags',
2136 'twitter_image_alt',
2137 ];
2138 foreach ($og_toggle_fields as $field) {
2139 $new_input['common_meta_settings'][$field] = (isset($input['common_meta_settings'][$field]) && $input['common_meta_settings'][$field] === 'true') ? 'true' : 'false';
2140 }
2141 }
2142
2143 # Handle whitelabel URL fields with improved empty value handling
2144 if (isset($input['whitelabel'])) {
2145 $existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
2146
2147 $general_input = $input['general'] ?? [];
2148 $plugin_name = isset($general_input['white_label_plugin_name']) ? trim((string) $general_input['white_label_plugin_name']) : '';
2149 $logo_light = isset($input['whitelabel']['logo_light']) ? trim((string) $input['whitelabel']['logo_light']) : '';
2150 $logo_dark = isset($input['whitelabel']['logo_dark']) ? trim((string) $input['whitelabel']['logo_dark']) : '';
2151 $author = isset($general_input['white_label_plugin_author']) ? trim((string) $general_input['white_label_plugin_author']) : '';
2152 $author_uri = isset($general_input['white_label_plugin_author_uri']) ? trim((string) $general_input['white_label_plugin_author_uri']) : '';
2153 $plugin_uri = isset($general_input['white_label_plugin_uri']) ? trim((string) $general_input['white_label_plugin_uri']) : '';
2154 $domain = isset($input['whitelabel']['domain']) ? trim((string) $input['whitelabel']['domain']) : '';
2155 $description = isset($general_input['white_label_plugin_description']) ? trim((string) $general_input['white_label_plugin_description']) : '';
2156
2157 $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));
2158 $has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
2159
2160 if ($has_optional_only && !$has_core_field) {
2161 add_settings_error(
2162 'metasync_options',
2163 'whitelabel_partial',
2164 'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.',
2165 'error'
2166 );
2167 $new_input['whitelabel'] = $existing_whitelabel;
2168 $whitelabel_validation_failed = true;
2169 } else {
2170 $new_input['whitelabel'] = $existing_whitelabel;
2171
2172 if (isset($input['whitelabel']['logo'])) {
2173 $logo_value = trim($input['whitelabel']['logo']);
2174
2175 if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
2176 $new_input['whitelabel']['logo'] = esc_url_raw($logo_value);
2177
2178 } else {
2179 $new_input['whitelabel']['logo'] = '';
2180
2181 }
2182 }
2183
2184 if (isset($input['whitelabel']['logo_light'])) {
2185 $logo_light_value = trim($input['whitelabel']['logo_light']);
2186
2187 if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
2188 $new_input['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
2189 } else {
2190 $new_input['whitelabel']['logo_light'] = '';
2191 }
2192 }
2193
2194 if (isset($input['whitelabel']['logo_dark'])) {
2195 $logo_dark_value = trim($input['whitelabel']['logo_dark']);
2196
2197 if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
2198 $new_input['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
2199 } else {
2200 $new_input['whitelabel']['logo_dark'] = '';
2201 }
2202 }
2203
2204 if (isset($input['whitelabel']['domain'])) {
2205 $domain_value = trim($input['whitelabel']['domain']);
2206
2207 if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
2208 $new_input['whitelabel']['domain'] = esc_url_raw($domain_value);
2209
2210 } else {
2211 $old_domain = $existing_whitelabel['domain'] ?? '';
2212 $new_input['whitelabel']['domain'] = '';
2213
2214 if (!empty($old_domain)) {
2215 $new_input['_trigger_heartbeat_after_save'] = 'Domain cleared from: ' . $old_domain;
2216 }
2217 }
2218 }
2219
2220 if (isset($input['whitelabel']['settings_password'])) {
2221 $password_value = trim($input['whitelabel']['settings_password']);
2222 $new_input['whitelabel']['settings_password'] = sanitize_text_field($password_value);
2223 } else {
2224 if (isset($existing_whitelabel['settings_password'])) {
2225 $new_input['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2226 }
2227 }
2228
2229 if (isset($input['whitelabel']['recovery_email'])) {
2230 $recovery_email = trim($input['whitelabel']['recovery_email']);
2231 if (!empty($recovery_email)) {
2232 $sanitized_email = sanitize_email($recovery_email);
2233 if (is_email($sanitized_email)) {
2234 $new_input['whitelabel']['recovery_email'] = $sanitized_email;
2235 } else {
2236 add_settings_error(
2237 'metasync_messages',
2238 'metasync_message',
2239 __('Invalid recovery email address.', 'metasync'),
2240 'error'
2241 );
2242 }
2243 } else {
2244 $new_input['whitelabel']['recovery_email'] = '';
2245 }
2246 } else {
2247 if (isset($existing_whitelabel['recovery_email'])) {
2248 $new_input['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
2249 }
2250 }
2251
2252 $has_password = !empty($new_input['whitelabel']['settings_password']);
2253 $has_recovery_email = !empty($new_input['whitelabel']['recovery_email']);
2254 if ($has_password && !$has_recovery_email) {
2255 add_settings_error(
2256 'metasync_messages',
2257 'metasync_message',
2258 __('Password Recovery Email is required when a password is set.', 'metasync'),
2259 'error'
2260 );
2261 $new_input['whitelabel']['settings_password'] = '';
2262 }
2263
2264 if (isset($input['whitelabel']['access_control'])) {
2265 $new_input['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($input['whitelabel']['access_control']);
2266 }
2267
2268 // Sanitize quick_links (up to 5 entries)
2269 if (isset($input['whitelabel']['quick_links']) && is_array($input['whitelabel']['quick_links'])) {
2270 $sanitized_links = [];
2271 foreach (array_slice($input['whitelabel']['quick_links'], 0, 5) as $link) {
2272 $url = isset($link['url']) ? esc_url_raw(trim((string) $link['url'])) : '';
2273 if (empty($url)) {
2274 continue; // skip blank rows
2275 }
2276 $sanitized_links[] = [
2277 'label' => sanitize_text_field($link['label'] ?? ''),
2278 'url' => $url,
2279 'external' => !empty($link['external']),
2280 ];
2281 }
2282 $new_input['whitelabel']['quick_links'] = $sanitized_links;
2283 } else {
2284 // Preserve existing links when not submitted
2285 if (isset($existing_whitelabel['quick_links'])) {
2286 $new_input['whitelabel']['quick_links'] = $existing_whitelabel['quick_links'];
2287 }
2288 }
2289
2290 // Sanitize color palette
2291 if (isset($input['whitelabel']['color_palette']) && is_array($input['whitelabel']['color_palette'])) {
2292 $allowed_vars = array(
2293 'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
2294 'dashboard-text-primary', 'dashboard-text-secondary',
2295 'dashboard-accent', 'dashboard-accent-hover',
2296 'dashboard-success', 'dashboard-warning', 'dashboard-error',
2297 'dashboard-border',
2298 'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
2299 'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
2300 );
2301 $sanitized_palette = array();
2302 foreach (array('dark', 'light') as $theme_key) {
2303 if (isset($input['whitelabel']['color_palette'][$theme_key]) && is_array($input['whitelabel']['color_palette'][$theme_key])) {
2304 $sanitized_palette[$theme_key] = array();
2305 foreach ($input['whitelabel']['color_palette'][$theme_key] as $var_name => $color_value) {
2306 if (in_array($var_name, $allowed_vars, true)) {
2307 $color_value = trim(sanitize_text_field($color_value));
2308 if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
2309 $sanitized_palette[$theme_key][$var_name] = $color_value;
2310 }
2311 }
2312 }
2313 }
2314 }
2315 $new_input['whitelabel']['color_palette'] = $sanitized_palette;
2316 } else {
2317 if (isset($existing_whitelabel['color_palette'])) {
2318 $new_input['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
2319 }
2320 }
2321
2322 $new_input['whitelabel']['updated_at'] = time();
2323 }
2324 } else {
2325 $existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
2326 $has_existing_whitelabel = !empty($existing_whitelabel['domain']) || !empty($existing_whitelabel['logo']);
2327
2328 if ($has_existing_whitelabel) {
2329 $new_input['whitelabel'] = [
2330 'is_whitelabel' => false,
2331 'domain' => '',
2332 'logo' => '',
2333 'company_name' => '',
2334 'updated_at' => time()
2335 ];
2336
2337 if (!empty($existing_whitelabel['domain'])) {
2338 delete_transient('metasync_heartbeat_status_cache');
2339 do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel settings cleared - domain changed to default');
2340 }
2341 }
2342 }
2343
2344 // Indexation Control Settings
2345 if (isset($input['seo_controls']['index_date_archives'])) {
2346 $new_input['seo_controls']['index_date_archives'] = boolval($input['seo_controls']['index_date_archives']);
2347 }
2348 if (isset($input['seo_controls']['index_tag_archives'])) {
2349 $new_input['seo_controls']['index_tag_archives'] = boolval($input['seo_controls']['index_tag_archives']);
2350 }
2351 if (isset($input['seo_controls']['index_author_archives'])) {
2352 $new_input['seo_controls']['index_author_archives'] = boolval($input['seo_controls']['index_author_archives']);
2353 }
2354 if (isset($input['seo_controls']['index_format_archives'])) {
2355 $new_input['seo_controls']['index_format_archives'] = boolval($input['seo_controls']['index_format_archives']);
2356 }
2357
2358 // Handle post-save heartbeat trigger for domain changes
2359 if (isset($new_input['_trigger_heartbeat_after_save'])) {
2360 $context = $new_input['_trigger_heartbeat_after_save'];
2361 unset($new_input['_trigger_heartbeat_after_save']);
2362
2363 add_action('updated_option_metasync_options', function() use ($context) {
2364 delete_transient('metasync_heartbeat_status_cache');
2365 do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel domain change - ' . $context);
2366 });
2367 }
2368
2369 $result = array_merge($new_input, $input);
2370
2371 if ($whitelabel_validation_failed) {
2372 $result['whitelabel'] = Metasync::get_option()['whitelabel'] ?? [];
2373 }
2374
2375 delete_transient('metasync_otto_js_detected');
2376
2377 return $result;
2378 }
2379
2380 /**
2381 * AJAX handler for saving general/whitelabel/advanced settings.
2382 *
2383 * Moved from Metasync_Admin – the admin class now delegates here.
2384 */
2385 public function meta_sync_save_settings()
2386 {
2387 if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
2388 wp_send_json_error(array('message' => 'Invalid nonce'));
2389 return;
2390 }
2391
2392 if (!Metasync::current_user_has_plugin_access()) {
2393 wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
2394 return;
2395 }
2396
2397 $text_fields = [
2398 'searchatlas_api_key', 'apikey',
2399 'white_label_plugin_name', 'white_label_plugin_description',
2400 'white_label_plugin_author', 'white_label_plugin_menu_slug',
2401 'white_label_plugin_menu_icon', 'enabled_plugin_css',
2402 'enabled_elementor_plugin_css_color','enabled_elementor_plugin_css',
2403 'otto_pixel_uuid','periodic_clear_otto_cache','periodic_clear_ottopage_cache',
2404 'periodic_clear_ottopost_cache', 'whitelabel_otto_name', 'otto_wp_rocket_compat'
2405 ];
2406
2407 $url_fields = [
2408 'white_label_plugin_author_uri', 'white_label_plugin_uri'
2409 ];
2410
2411 $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'];
2412
2413 $url_fields = ['white_label_plugin_author_uri', 'white_label_plugin_uri'];
2414
2415 $metasync_options = Metasync::get_option();
2416 if (!is_array($metasync_options)) {
2417 $metasync_options = array();
2418 }
2419 if (!isset($metasync_options['general']) || !is_array($metasync_options['general'])) {
2420 $metasync_options['general'] = array();
2421 }
2422
2423 $validation_errors = [];
2424
2425 $active_tab = isset($_POST['active_tab']) ? sanitize_text_field($_POST['active_tab']) : 'general';
2426 $general_tab_submitted = ($active_tab === 'general');
2427 $whitelabel_tab_submitted = ($active_tab === 'whitelabel');
2428
2429 foreach ($text_fields as $field) {
2430
2431 if (isset($_POST['metasync_options']['general'][$field])) {
2432
2433 $value = trim($_POST['metasync_options']['general'][$field]);
2434
2435 $whitelabel_clearable_fields = [
2436 'white_label_plugin_name',
2437 'white_label_plugin_description',
2438 'white_label_plugin_author',
2439 'white_label_plugin_menu_slug',
2440 'white_label_plugin_menu_icon',
2441 'whitelabel_otto_name',
2442 'otto_pixel_uuid'
2443 ];
2444
2445 if ($value === '' && !in_array($field, $whitelabel_clearable_fields)) {
2446 continue;
2447 }
2448
2449 if ($field === 'white_label_plugin_name') {
2450 if (strlen($value) > 16) {
2451 $validation_errors[$field] = 'Plugin name must not exceed 16 characters';
2452 continue;
2453 }
2454 }
2455
2456 if ($field === 'white_label_plugin_menu_icon') {
2457
2458 if ($value === '') {
2459 $metasync_options['general'][$field] = '';
2460 } elseif (filter_var($value, FILTER_VALIDATE_URL)) {
2461
2462 $image_extensions = ['png', 'svg'];
2463
2464 $path = parse_url($value, PHP_URL_PATH);
2465 $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
2466
2467 if (in_array($extension, $image_extensions, true)) {
2468 $metasync_options['general'][$field] = esc_url_raw($value);
2469 } else {
2470 $validation_errors[] = 'Invalid Menu icon format. Only PNG and SVG are allowed.';
2471 }
2472 } else {
2473 $validation_errors[] = 'Invalid Menu icon URL format.';
2474 }
2475 } else {
2476 $metasync_options['general'][$field] = sanitize_text_field($_POST['metasync_options']['general'][$field]);
2477 }
2478 }
2479 }
2480
2481 $textarea_fields = ['otto_bot_whitelist', 'otto_bot_blacklist'];
2482 if ($general_tab_submitted) {
2483 foreach ($textarea_fields as $field) {
2484 if (isset($_POST['metasync_options']['general'][$field])) {
2485 $metasync_options['general'][$field] = sanitize_textarea_field($_POST['metasync_options']['general'][$field]);
2486 } else {
2487 $metasync_options['general'][$field] = '';
2488 }
2489 }
2490 }
2491
2492 if ($general_tab_submitted) {
2493 foreach ($bool_fields as $field) {
2494 if (isset($_POST['metasync_options']['general'][$field])) {
2495 $metasync_options['general'][$field] = filter_var($_POST['metasync_options']['general'][$field], FILTER_VALIDATE_BOOLEAN);
2496 }else {
2497 $metasync_options['general'][$field] = false;
2498 }
2499 }
2500 }
2501
2502 if ($general_tab_submitted || $whitelabel_tab_submitted) {
2503 foreach ($url_fields as $field) {
2504 if (isset($_POST['metasync_options']['general'][$field])) {
2505
2506 $value = trim($_POST['metasync_options']['general'][$field]);
2507
2508 $whitelabel_clearable_url_fields = [
2509 'white_label_plugin_author_uri',
2510 'white_label_plugin_uri'
2511 ];
2512
2513 if ($value === '' && !in_array($field, $whitelabel_clearable_url_fields)) {
2514 continue;
2515 }
2516
2517 if ($value === '' && in_array($field, $whitelabel_clearable_url_fields)) {
2518 $metasync_options['general'][$field] = '';
2519 continue;
2520 }
2521
2522 if (filter_var($value, FILTER_VALIDATE_URL)) {
2523
2524 $parsed_url = parse_url($value);
2525 $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
2526
2527 if (strpos($host, '.') !== false || filter_var($host, FILTER_VALIDATE_IP)) {
2528 $metasync_options['general'][$field] = esc_url_raw($value);
2529 } else {
2530 $field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
2531 $validation_errors[] = 'Invalid ' . $field_name . ' format. Please use a proper domain name (e.g., example.com).';
2532 }
2533 } else {
2534 $field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
2535 $validation_errors[] = 'Invalid ' . $field_name . ' format.';
2536 }
2537 }
2538 }
2539 }
2540
2541 if ($general_tab_submitted) {
2542 if (isset($_POST['metasync_options']['general']['content_genius_sync_roles']) && is_array($_POST['metasync_options']['general']['content_genius_sync_roles'])) {
2543 $metasync_options['general']['content_genius_sync_roles'] = array_map('sanitize_text_field', $_POST['metasync_options']['general']['content_genius_sync_roles']);
2544 } else {
2545 $metasync_options['general']['content_genius_sync_roles'] = array();
2546 }
2547 }
2548
2549 if ($general_tab_submitted) {
2550 $breadcrumbs_input = isset($_POST['metasync_options']['breadcrumbs']) && is_array($_POST['metasync_options']['breadcrumbs'])
2551 ? $_POST['metasync_options']['breadcrumbs']
2552 : array();
2553
2554 if (!isset($metasync_options['breadcrumbs']) || !is_array($metasync_options['breadcrumbs'])) {
2555 $metasync_options['breadcrumbs'] = array();
2556 }
2557
2558 $metasync_options['breadcrumbs']['enabled'] = !empty($breadcrumbs_input['enabled']);
2559 $metasync_options['breadcrumbs']['show_current_page'] = !empty($breadcrumbs_input['show_current_page']);
2560 $metasync_options['breadcrumbs']['separator'] = isset($breadcrumbs_input['separator']) ? sanitize_text_field($breadcrumbs_input['separator']) : '»';
2561 $metasync_options['breadcrumbs']['home_label'] = isset($breadcrumbs_input['home_label']) ? sanitize_text_field($breadcrumbs_input['home_label']) : 'Home';
2562 $metasync_options['breadcrumbs']['home_url'] = isset($breadcrumbs_input['home_url']) ? esc_url_raw($breadcrumbs_input['home_url']) : '';
2563 $metasync_options['breadcrumbs']['prefix_text'] = isset($breadcrumbs_input['prefix_text']) ? sanitize_text_field($breadcrumbs_input['prefix_text']) : '';
2564 $metasync_options['breadcrumbs']['archive_label_format'] = isset($breadcrumbs_input['archive_label_format']) ? sanitize_text_field($breadcrumbs_input['archive_label_format']) : '{name}';
2565 }
2566
2567 if ($general_tab_submitted && isset($_POST['metasync_options']['general']['default_page_builder'])) {
2568 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
2569 $available_builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
2570 $builder_value = sanitize_text_field($_POST['metasync_options']['general']['default_page_builder']);
2571 if (isset($available_builders[$builder_value]) && $available_builders[$builder_value]['detected']) {
2572 $metasync_options['general']['default_page_builder'] = $builder_value;
2573 } else {
2574 $metasync_options['general']['default_page_builder'] = 'gutenberg';
2575 if (isset($available_builders[$builder_value]) && !$available_builders[$builder_value]['detected']) {
2576 $validation_errors[] = sprintf(__('Builder "%s" is not detected on this site. Reverted to Gutenberg.', 'metasync'), $available_builders[$builder_value]['label']);
2577 }
2578 }
2579 }
2580
2581 if ($whitelabel_tab_submitted && isset($_POST['metasync_options']['whitelabel'])) {
2582 $gp = isset($_POST['metasync_options']['general']) && is_array($_POST['metasync_options']['general']) ? $_POST['metasync_options']['general'] : [];
2583 $wp = isset($_POST['metasync_options']['whitelabel']) && is_array($_POST['metasync_options']['whitelabel']) ? $_POST['metasync_options']['whitelabel'] : [];
2584 $plugin_name = isset($gp['white_label_plugin_name']) ? trim((string) $gp['white_label_plugin_name']) : '';
2585 $logo_light = isset($wp['logo_light']) ? trim((string) $wp['logo_light']) : '';
2586 $logo_dark = isset($wp['logo_dark']) ? trim((string) $wp['logo_dark']) : '';
2587 $author = isset($gp['white_label_plugin_author']) ? trim((string) $gp['white_label_plugin_author']) : '';
2588 $author_uri = isset($gp['white_label_plugin_author_uri']) ? trim((string) $gp['white_label_plugin_author_uri']) : '';
2589 $plugin_uri = isset($gp['white_label_plugin_uri']) ? trim((string) $gp['white_label_plugin_uri']) : '';
2590 $domain = isset($wp['domain']) ? trim((string) $wp['domain']) : '';
2591 $description = isset($gp['white_label_plugin_description']) ? trim((string) $gp['white_label_plugin_description']) : '';
2592
2593 $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));
2594 $has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
2595
2596 if ($has_optional_only && !$has_core_field) {
2597 $validation_errors[] = 'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.';
2598 }
2599 }
2600
2601 if (!empty($validation_errors)) {
2602 wp_send_json_error([
2603 'errors' => $validation_errors
2604 ]);
2605 return;
2606 }
2607
2608 $old_options = Metasync::get_option('general') ?? [];
2609 $old_api_key = $old_options['searchatlas_api_key'] ?? '';
2610
2611 $api_key_field_present = isset( $_POST['metasync_options']['general']['searchatlas_api_key'] );
2612 $api_key_validated = null; // null = not checked, true = valid, false = rejected
2613 $is_connected = null;
2614 $api_key_warning = null;
2615 // Value from $metasync_options after the sanitize loop (sanitize_text_field applied at line ~2474)
2616 $proposed_new_api_key = $metasync_options['general']['searchatlas_api_key'] ?? '';
2617
2618 // Only validate when the API key actually changed (new or modified)
2619 if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
2620 $ping = Metasync_Heartbeat_Manager::instance()->validate_api_key( $proposed_new_api_key );
2621
2622 if ( $ping['connected'] === true ) {
2623 // Key validated — accept it and auto-populate UUID if returned
2624 $api_key_validated = true;
2625 $is_connected = true;
2626 if ( ! empty( $ping['otto_pixel_uuid'] ) ) {
2627 $existing_uuid = $metasync_options['general']['otto_pixel_uuid'] ?? '';
2628 if ( empty( $existing_uuid ) || $existing_uuid !== $ping['otto_pixel_uuid'] ) {
2629 $metasync_options['general']['otto_pixel_uuid'] = sanitize_text_field( $ping['otto_pixel_uuid'] );
2630 }
2631 }
2632 } elseif ( isset( $ping['network_error'] ) && $ping['network_error'] === true ) {
2633 // Network/timeout failure — save the key but warn the user
2634 $api_key_warning = 'Could not verify API key (network error). The key was saved — it will be verified on the next heartbeat.';
2635 } else {
2636 // Backend explicitly rejected the key — revert to old key
2637 $metasync_options['general']['searchatlas_api_key'] = $old_api_key;
2638 $api_key_validated = false;
2639 $is_connected = false;
2640 $api_key_warning = 'The API key could not be verified. Please check it and try again.';
2641 }
2642 }
2643
2644 Metasync::set_option($metasync_options);
2645
2646 // Update connection state immediately when ping confirmed
2647 if ( $api_key_validated === true ) {
2648 update_option( 'metasync_last_known_connection_state', true );
2649 set_transient( 'metasync_heartbeat_status_cache', [
2650 'status' => true,
2651 'timestamp' => time(),
2652 'cached_until' => time() + 300,
2653 'updated_by' => 'manual_save_ping_connected',
2654 ], 300 );
2655 }
2656
2657 $data = Metasync::get_option('general');
2658 $new_api_key = $data['searchatlas_api_key'] ?? '';
2659
2660 $api_key_changed = $old_api_key !== $new_api_key;
2661 $api_key_added = empty($old_api_key) && !empty($new_api_key);
2662 $api_key_removed = !empty($old_api_key) && empty($new_api_key);
2663
2664 // Skip heavy SyncCustomerParams when ping already validated the key — schedule
2665 // an async heartbeat instead so the save response returns quickly.
2666 if ( $api_key_validated === true ) {
2667 $dt = new DateTime();
2668 $send_auth_token_timestamp = Metasync::get_option();
2669 $send_auth_token_timestamp['general']['send_auth_token_timestamp'] = $dt->format('M d, Y h:i:s A');
2670 Metasync::set_option($send_auth_token_timestamp);
2671
2672 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
2673 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - validated via ping');
2674 } else {
2675 // No ping was done (key unchanged, cleared, or network failure) — fall back to sync
2676 $sync_request = new Metasync_Sync_Requests();
2677 $response = $sync_request->SyncCustomerParams();
2678 $responseCode = wp_remote_retrieve_response_code($response);
2679
2680 if ($responseCode == 200) {
2681 $dt = new DateTime();
2682 $send_auth_token_timestamp = Metasync::get_option();
2683 $send_auth_token_timestamp['general']['send_auth_token_timestamp'] = $dt->format('M d, Y h:i:s A');
2684 Metasync::set_option($send_auth_token_timestamp);
2685
2686 if ($api_key_added) {
2687 Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
2688 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - new key added');
2689 } elseif ($api_key_removed) {
2690 Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
2691 delete_transient('metasync_heartbeat_status_cache');
2692 } elseif ($api_key_changed && !empty($new_api_key)) {
2693 do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - key changed');
2694 }
2695 }
2696 }
2697
2698 if (isset($_POST['metasync_options']['whitelabel'])) {
2699
2700 $whitelabel_data = $_POST['metasync_options']['whitelabel'];
2701 $existing_whitelabel = $metasync_options['whitelabel'] ?? [];
2702
2703 if (isset($whitelabel_data['logo'])) {
2704 $logo_value = trim($whitelabel_data['logo']);
2705
2706 if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
2707 $metasync_options['whitelabel']['logo'] = esc_url_raw($logo_value);
2708 } else {
2709 $metasync_options['whitelabel']['logo'] = '';
2710 }
2711 }
2712
2713 if (isset($whitelabel_data['logo_light'])) {
2714 $logo_light_value = trim($whitelabel_data['logo_light']);
2715
2716 if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
2717 $metasync_options['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
2718 } else {
2719 $metasync_options['whitelabel']['logo_light'] = '';
2720 }
2721 }
2722
2723 if (isset($whitelabel_data['logo_dark'])) {
2724 $logo_dark_value = trim($whitelabel_data['logo_dark']);
2725
2726 if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
2727 $metasync_options['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
2728 } else {
2729 $metasync_options['whitelabel']['logo_dark'] = '';
2730 }
2731 }
2732
2733 if (isset($whitelabel_data['domain'])) {
2734 $domain_value = trim($whitelabel_data['domain']);
2735 if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
2736 $metasync_options['whitelabel']['domain'] = esc_url_raw($domain_value);
2737 } else {
2738 $metasync_options['whitelabel']['domain'] = '';
2739 }
2740 }
2741
2742 if (isset($whitelabel_data['settings_password'])) {
2743 $password_value = trim($whitelabel_data['settings_password']);
2744
2745 $hide_settings_enabled = isset($whitelabel_data['hide_settings']) && $whitelabel_data['hide_settings'] == '1';
2746
2747 if ($hide_settings_enabled && empty($password_value)) {
2748 if (isset($existing_whitelabel['settings_password'])) {
2749 $metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2750 }
2751 } else {
2752 $metasync_options['whitelabel']['settings_password'] = sanitize_text_field($password_value);
2753 }
2754 } else {
2755 if (isset($existing_whitelabel['settings_password'])) {
2756 $metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
2757 }
2758 }
2759
2760 if (isset($whitelabel_data['hide_settings'])) {
2761 $metasync_options['whitelabel']['hide_settings'] = filter_var($whitelabel_data['hide_settings'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
2762 } else {
2763 $metasync_options['whitelabel']['hide_settings'] = 0;
2764 }
2765
2766 if (isset($whitelabel_data['recovery_email'])) {
2767 $recovery_email = trim($whitelabel_data['recovery_email']);
2768 if (!empty($recovery_email)) {
2769 $sanitized_email = sanitize_email($recovery_email);
2770 if (is_email($sanitized_email)) {
2771 $metasync_options['whitelabel']['recovery_email'] = $sanitized_email;
2772 }
2773 } else {
2774 $metasync_options['whitelabel']['recovery_email'] = '';
2775 }
2776 } else {
2777 if (isset($existing_whitelabel['recovery_email'])) {
2778 $metasync_options['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
2779 }
2780 }
2781
2782 $has_password = !empty($metasync_options['whitelabel']['settings_password']);
2783 $has_recovery_email = !empty($metasync_options['whitelabel']['recovery_email']);
2784 if ($has_password && !$has_recovery_email) {
2785 $metasync_options['whitelabel']['settings_password'] = '';
2786 }
2787
2788 if (isset($whitelabel_data['access_control'])) {
2789 $metasync_options['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($whitelabel_data['access_control']);
2790 }
2791
2792 if (!empty($metasync_options['whitelabel']['hide_settings']) && empty($metasync_options['whitelabel']['settings_password'])) {
2793 $metasync_options['whitelabel']['hide_settings'] = 0;
2794 }
2795
2796 // Sanitize color palette
2797 if (isset($whitelabel_data['color_palette']) && is_array($whitelabel_data['color_palette'])) {
2798 $allowed_vars = array(
2799 'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
2800 'dashboard-text-primary', 'dashboard-text-secondary',
2801 'dashboard-accent', 'dashboard-accent-hover',
2802 'dashboard-success', 'dashboard-warning', 'dashboard-error',
2803 'dashboard-border',
2804 'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
2805 'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
2806 );
2807 $sanitized_palette = array();
2808 foreach (array('dark', 'light') as $theme_key) {
2809 if (isset($whitelabel_data['color_palette'][$theme_key]) && is_array($whitelabel_data['color_palette'][$theme_key])) {
2810 $sanitized_palette[$theme_key] = array();
2811 foreach ($whitelabel_data['color_palette'][$theme_key] as $var_name => $color_value) {
2812 if (in_array($var_name, $allowed_vars, true)) {
2813 $color_value = trim(sanitize_text_field($color_value));
2814 if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
2815 $sanitized_palette[$theme_key][$var_name] = $color_value;
2816 }
2817 }
2818 }
2819 }
2820 }
2821 $metasync_options['whitelabel']['color_palette'] = $sanitized_palette;
2822 } else {
2823 if (isset($existing_whitelabel['color_palette'])) {
2824 $metasync_options['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
2825 }
2826 }
2827
2828 $metasync_options['whitelabel']['updated_at'] = time();
2829
2830 Metasync::set_option($metasync_options);
2831
2832 }
2833
2834 $redirect_url = isset($_GET['tab']) ?
2835 admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings&tab='.$_GET['tab']) :
2836 admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings');
2837
2838 // Also save edge cache settings if present in the payload (section renders inside this form)
2839 if (class_exists('Metasync_Edge_Cache_Settings')) {
2840 Metasync_Edge_Cache_Settings::save_from_post();
2841 }
2842
2843 if ($general_tab_submitted && isset($_POST['metasync_llms_txt_settings'])) {
2844 $llms_input = wp_unslash($_POST['metasync_llms_txt_settings']);
2845 if (is_array($llms_input)) {
2846 update_option('metasync_llms_txt_settings', $this->sanitize_llms_txt_settings($llms_input));
2847 }
2848 }
2849
2850 $response_payload = array(
2851 'message' => 'Settings saved successfully!',
2852 'redirect_url' => $redirect_url,
2853 );
2854
2855 if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
2856 if ( $api_key_validated !== null ) {
2857 $response_payload['api_key_validated'] = $api_key_validated;
2858 $response_payload['is_connected'] = $is_connected;
2859 }
2860 if ( $api_key_warning !== null ) {
2861 $response_payload['warning'] = $api_key_warning;
2862 }
2863 }
2864
2865 delete_transient('metasync_otto_js_detected');
2866
2867 wp_send_json_success( $response_payload );
2868 }
2869
2870 /**
2871 * AJAX handler for saving execution settings.
2872 *
2873 * Moved from Metasync_Admin – the admin class now delegates here.
2874 */
2875 public function ajax_save_execution_settings()
2876 {
2877 if (!isset($_POST['execution_settings_nonce']) || !wp_verify_nonce($_POST['execution_settings_nonce'], 'metasync_execution_settings_nonce')) {
2878 wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
2879 return;
2880 }
2881
2882 if (!Metasync::current_user_has_plugin_access()) {
2883 wp_send_json_error(array('message' => 'Insufficient permissions to save settings.'));
2884 return;
2885 }
2886
2887 $settings = array(
2888 'max_execution_time' => isset($_POST['max_execution_time']) ? absint($_POST['max_execution_time']) : 30,
2889 'max_memory_limit' => isset($_POST['max_memory_limit']) ? absint($_POST['max_memory_limit']) : 256,
2890 'log_batch_size' => isset($_POST['log_batch_size']) ? absint($_POST['log_batch_size']) : 1000,
2891 'action_scheduler_batches' => isset($_POST['action_scheduler_batches']) ? absint($_POST['action_scheduler_batches']) : 1,
2892 'otto_rate_limit' => isset($_POST['otto_rate_limit']) ? absint($_POST['otto_rate_limit']) : 10,
2893 'queue_cleanup_days' => isset($_POST['queue_cleanup_days']) ? absint($_POST['queue_cleanup_days']) : 31
2894 );
2895
2896 $server_limits = Metasync_Settings_Fields::instance()->get_server_limits();
2897
2898 if ($settings['max_execution_time'] < 1 || $settings['max_execution_time'] > 300) {
2899 wp_send_json_error(array('message' => 'Max Execution Time must be between 1 and 300 seconds.'));
2900 return;
2901 }
2902
2903 if ($server_limits['max_execution_time_raw'] != -1 && $settings['max_execution_time'] > $server_limits['max_execution_time_raw']) {
2904 wp_send_json_error(array('message' => sprintf('Max Execution Time exceeds server limit of %d seconds. Please reduce the value.', $server_limits['max_execution_time_raw'])));
2905 return;
2906 }
2907
2908 $can_change_memory = Metasync_Settings_Fields::instance()->can_change_memory_limit();
2909 if ($can_change_memory && ($settings['max_memory_limit'] < 64 || $settings['max_memory_limit'] > 512)) {
2910 wp_send_json_error(array('message' => 'Max Memory Limit must be between 64 and 512 MB.'));
2911 return;
2912 }
2913
2914 if ($can_change_memory && $server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
2915 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'])));
2916 return;
2917 }
2918
2919 if (!$can_change_memory) {
2920 $existing_settings = get_option('metasync_execution_settings', array());
2921 $settings['max_memory_limit'] = isset($existing_settings['max_memory_limit'])
2922 ? $existing_settings['max_memory_limit']
2923 : 256;
2924 }
2925
2926 if ($settings['log_batch_size'] < 100 || $settings['log_batch_size'] > 5000) {
2927 wp_send_json_error(array('message' => 'Log Batch Size must be between 100 and 5000 lines.'));
2928 return;
2929 }
2930
2931 if ($settings['action_scheduler_batches'] < 1 || $settings['action_scheduler_batches'] > 10) {
2932 wp_send_json_error(array('message' => 'Action Scheduler Concurrent Batches must be between 1 and 10.'));
2933 return;
2934 }
2935
2936 if ($settings['otto_rate_limit'] < 1 || $settings['otto_rate_limit'] > 60) {
2937 wp_send_json_error(array('message' => 'OTTO API Calls Per Minute must be between 1 and 60.'));
2938 return;
2939 }
2940
2941 if ($settings['queue_cleanup_days'] < 7 || $settings['queue_cleanup_days'] > 90) {
2942 wp_send_json_error(array('message' => 'Queue Auto-Cleanup Days must be between 7 and 90 days.'));
2943 return;
2944 }
2945
2946 $saved = update_option('metasync_execution_settings', $settings);
2947
2948 if ($saved !== false) {
2949 wp_send_json_success(array(
2950 'message' => 'Execution settings saved successfully!'
2951 ));
2952 } else {
2953 wp_send_json_error(array('message' => 'Failed to save settings. Please try again.'));
2954 }
2955 }
2956
2957 /**
2958 * AJAX handler for saving SEO / Indexation Control settings.
2959 *
2960 * Moved from Metasync_Admin – the admin class now delegates here.
2961 */
2962 public function meta_sync_save_seo_controls()
2963 {
2964 try {
2965 if (!isset($_POST['meta_sync_seo_controls_nonce']) || !wp_verify_nonce($_POST['meta_sync_seo_controls_nonce'], 'meta_sync_seo_controls_nonce')) {
2966 wp_send_json_error(array('message' => 'Invalid nonce'));
2967 return;
2968 }
2969
2970 $current_options = Metasync::get_option();
2971
2972 $original_options = json_decode(json_encode($current_options), true);
2973
2974 if (!isset($current_options['seo_controls'])) {
2975 $current_options['seo_controls'] = [];
2976 }
2977
2978 $seo_control_fields = [
2979 'index_date_archives',
2980 'index_tag_archives',
2981 'index_author_archives',
2982 'index_category_archives',
2983 'index_format_archives',
2984 'override_robots_tags',
2985 'add_nofollow_to_external_links',
2986 'enable_googleinstantindex',
2987 'enable_binginstantindex'
2988 ];
2989
2990 foreach ($seo_control_fields as $field) {
2991 if (isset($_POST['metasync_options']['seo_controls'][$field]) && $_POST['metasync_options']['seo_controls'][$field] === 'true') {
2992 $current_options['seo_controls'][$field] = 'true';
2993 } else {
2994 $current_options['seo_controls'][$field] = 'false';
2995 }
2996 }
2997
2998 $bing_save_result = true;
2999 if (isset($_POST['metasync_bing_api_key_inline'])) {
3000 $bing_save_result = $this->save_bing_inline_settings_ajax();
3001 }
3002
3003 // Save Google Instant Indexing post types
3004 if (isset($_POST['metasync_post_types']) && is_array($_POST['metasync_post_types'])) {
3005 $google_post_types = array_values(array_map('sanitize_title', wp_unslash($_POST['metasync_post_types'])));
3006 $google_settings = get_option('metasync_options_instant_indexing', ['post_types' => []]);
3007 $google_settings['post_types'] = $google_post_types;
3008 update_option('metasync_options_instant_indexing', $google_settings);
3009 }
3010
3011 $options_changed = (json_encode($original_options) !== json_encode($current_options));
3012
3013 $result = Metasync::set_option($current_options);
3014
3015 if (!$result && !$options_changed) {
3016 $result = true;
3017 }
3018
3019 if ($result && $bing_save_result) {
3020 $success_message = 'Indexation Control settings saved successfully!';
3021 if (isset($_POST['metasync_bing_api_key_inline'])) {
3022 $success_message = 'Indexation Control and Bing Instant Indexing settings saved successfully!';
3023 }
3024 wp_send_json_success(array(
3025 'message' => $success_message,
3026 'saved_data' => $current_options['seo_controls']
3027 ));
3028 } else {
3029 $error_message = 'Failed to save Indexation Control settings';
3030 if (!$bing_save_result) {
3031 $error_message = 'Failed to save Bing Instant Indexing settings';
3032 }
3033 wp_send_json_error(array('message' => $error_message));
3034 }
3035 } catch (Exception $e) {
3036 error_log('Indexation Control AJAX Exception: ' . $e->getMessage());
3037 error_log('Indexation Control AJAX Stack trace: ' . $e->getTraceAsString());
3038 wp_send_json_error(array('message' => 'Error saving settings: ' . $e->getMessage()));
3039 }
3040 }
3041
3042 /**
3043 * Save Bing IndexNow inline settings from SEO Controls page.
3044 *
3045 * Moved from Metasync_Admin – the admin class now delegates here.
3046 *
3047 * @return bool True on success, false on failure
3048 */
3049 public function save_bing_inline_settings_ajax()
3050 {
3051 $api_key = isset($_POST['metasync_bing_api_key_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_api_key_inline'])) : '';
3052 $endpoint = isset($_POST['metasync_bing_endpoint_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_endpoint_inline'])) : 'indexnow';
3053 $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'])) : [];
3054 $disable_other_plugins = isset($_POST['metasync_bing_disable_other_plugins_inline']) ? true : false;
3055
3056 $existing_settings = get_option('metasync_options_bing_instant_indexing', []);
3057
3058 $new_settings = [
3059 'api_key' => $api_key,
3060 'endpoint' => $endpoint,
3061 'post_types' => array_values($post_types),
3062 'disable_other_plugins' => $disable_other_plugins,
3063 ];
3064
3065 $settings = array_merge($existing_settings, $new_settings);
3066
3067 $settings_changed = (json_encode($existing_settings) !== json_encode($settings));
3068
3069 $result = update_option('metasync_options_bing_instant_indexing', $settings);
3070
3071 if (!$result && !$settings_changed) {
3072 $result = true;
3073 }
3074
3075 if (!empty($api_key)) {
3076 $file_path = ABSPATH . $api_key . '.txt';
3077 $file_result = file_put_contents($file_path, $api_key);
3078
3079 if ($file_result === false) {
3080 error_log('Bing IndexNow: Failed to create API key verification file at ' . $file_path);
3081 return false;
3082 }
3083
3084 $current_options = Metasync::get_option();
3085 if (!isset($current_options['seo_controls']['enable_binginstantindex']) || $current_options['seo_controls']['enable_binginstantindex'] !== 'true') {
3086 $current_options['seo_controls']['enable_binginstantindex'] = 'true';
3087 Metasync::set_option($current_options);
3088 }
3089 }
3090
3091 return $result;
3092 }
3093
3094 /**
3095 * Sanitize the metasync_llms_txt_settings option.
3096 *
3097 * The Settings API delivers raw form data; we coerce it into a predictable
3098 * shape so the generator and MCP tools can rely on the structure.
3099 *
3100 * @param mixed $input Raw form data.
3101 * @return array
3102 */
3103 public function sanitize_llms_txt_settings($input)
3104 {
3105 if (!is_array($input)) {
3106 $input = array();
3107 }
3108
3109 $existing = get_option('metasync_llms_txt_settings', array());
3110 if (!is_array($existing)) {
3111 $existing = array();
3112 }
3113
3114 $output = $existing;
3115
3116 $output['enabled'] = !empty($input['enabled'])
3117 ? (bool) filter_var($input['enabled'], FILTER_VALIDATE_BOOLEAN)
3118 : false;
3119
3120 if (isset($input['post_types']) && is_array($input['post_types'])) {
3121 $output['post_types'] = array_values(array_filter(array_map('sanitize_text_field', $input['post_types'])));
3122 } else {
3123 $output['post_types'] = array('page', 'post');
3124 }
3125
3126 if (isset($input['max_posts'])) {
3127 $max = absint($input['max_posts']);
3128 if ($max < 1) {
3129 $max = 1;
3130 }
3131 if ($max > 500) {
3132 $max = 500;
3133 }
3134 $output['max_posts'] = $max;
3135 }
3136
3137 if (isset($input['excluded_ids'])) {
3138 if (is_array($input['excluded_ids'])) {
3139 $ids = $input['excluded_ids'];
3140 } else {
3141 $ids = preg_split('/[\s,]+/', (string) $input['excluded_ids']);
3142 }
3143 $output['excluded_ids'] = array_values(array_filter(array_map('absint', (array) $ids)));
3144 }
3145
3146 if (isset($input['custom_description'])) {
3147 $output['custom_description'] = sanitize_text_field((string) $input['custom_description']);
3148 }
3149
3150 $output['llms_full_enabled'] = !empty($input['llms_full_enabled'])
3151 ? (bool) filter_var($input['llms_full_enabled'], FILTER_VALIDATE_BOOLEAN)
3152 : false;
3153
3154 if (isset($input['max_posts_full'])) {
3155 $max_full = absint($input['max_posts_full']);
3156 if ($max_full < 1) {
3157 $max_full = 1;
3158 }
3159 if ($max_full > 500) {
3160 $max_full = 500;
3161 }
3162 $output['max_posts_full'] = $max_full;
3163 }
3164
3165 return $output;
3166 }
3167 }
3168