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

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

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