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

class-google-index-admin.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at google-index/class-google-index-admin.php

714 lines 30.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Google Index - Admin Integration
5 *
6 * Integrates Google Index settings into MetaSync admin general settings
7 *
8 * @package GoogleIndexDirect
9 * @version 1.0.0
10 */
11
12 // Prevent direct access
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 class Google_Index_Admin
18 {
19 /**
20 * Section ID for Google Index settings
21 */
22 private const SECTION_GOOGLE_INDEX = 'google_index_direct_settings';
23
24 /**
25 * Placeholder the redacted display JSON uses in place of the private key.
26 *
27 * Its presence means the textarea still holds what the server rendered, so
28 * the submission carries no new credential and must leave storage alone.
29 */
30 private const REDACTED_MARKER = '-----REDACTED-----';
31
32 /**
33 * Initialize admin functionality
34 */
35 public function __construct()
36 {
37 // Hook into MetaSync admin initialization
38 add_action('admin_init', array($this, 'add_settings_to_metasync'), 20);
39 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
40
41 // Hook into MetaSync's AJAX settings processing
42 add_action('wp_ajax_meta_sync_save_settings', array($this, 'process_google_index_settings'), 5);
43 add_action('wp_ajax_meta_sync_save_seo_controls', array($this, 'process_google_index_settings'), 5);
44
45 // Display admin notices after redirect
46 add_action('admin_notices', array($this, 'display_admin_notices'));
47
48 // AJAX handlers
49 add_action('wp_ajax_metasync_google_index_direct_test', array($this, 'ajax_test_connection'));
50 }
51
52
53 /**
54 * Add Google Index settings to MetaSync admin
55 */
56 public function add_settings_to_metasync()
57 {
58 // Only add if MetaSync Admin class exists
59 if (!class_exists('Metasync_Admin')) {
60 return;
61 }
62
63 // Get MetaSync page slug
64 $page_slug = $this->get_metasync_page_slug();
65 if (!$page_slug) {
66 return;
67 }
68
69 // Add settings section for Google Index
70 add_settings_section(
71 self::SECTION_GOOGLE_INDEX,
72 '', // Empty title - we'll use dashboard card styling
73 function(){}, // Empty callback
74 $page_slug . '_general'
75 );
76
77 // Add the main settings field
78 add_settings_field(
79 'google_index_direct_config',
80 'Google Index API',
81 array($this, 'render_settings_field'),
82 $page_slug . '_general',
83 self::SECTION_GOOGLE_INDEX
84 );
85 }
86
87 /**
88 * Get MetaSync page slug
89 */
90 private function get_metasync_page_slug()
91 {
92 if (class_exists('Metasync_Admin') && property_exists('Metasync_Admin', 'page_slug')) {
93 return Metasync_Admin::$page_slug;
94 }
95 return 'searchatlas'; // fallback
96 }
97
98 /**
99 * Render the Google Index settings field
100 */
101 public function render_settings_field()
102 {
103 // Load Google Index functionality
104 if (!function_exists('google_index_direct')) {
105 if (file_exists(plugin_dir_path(__FILE__) . 'google-index-init.php')) {
106 require_once plugin_dir_path(__FILE__) . 'google-index-init.php';
107 } else {
108 error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(__FILE__) . 'google-index-init.php'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
109 return;
110 }
111 }
112
113 // Get current service account info (safe - doesn't expose private key)
114 $google_index = google_index_direct();
115 if (!$google_index) {
116 // Nothing to render against, and calling through would be fatal.
117 return;
118 }
119 $service_info = $google_index->get_service_account_info();
120 $is_configured = !isset($service_info['error']);
121
122 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
123
124 // Include the settings field view
125 include plugin_dir_path(__FILE__) . '../views/metasync-google-index-api-settings.php';
126 }
127
128 /**
129 * Process Google Index settings during MetaSync AJAX save
130 * This runs early in the AJAX processing chain (priority 5)
131 */
132 public function process_google_index_settings()
133 {
134 // Only process if our fields are present in the request
135 if (!isset($_POST['google_index_service_account_json']) && // phpcs:ignore WordPress.Security.NonceVerification.Missing -- early-return checks, a nonce is verified before any write
136 !isset($_POST['google_index_clear_config']) && // phpcs:ignore WordPress.Security.NonceVerification.Missing -- early-return checks, a nonce is verified before any write
137 !isset($_FILES['google_index_service_account_file'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- early-return checks, a nonce is verified before any write
138 return; // No Google Index data to process
139 }
140
141 if (!$this->authorize_settings_request()) {
142 return;
143 }
144
145 // Clearing does not need to load or inspect credential contents.
146 if (isset($_POST['google_index_clear_config'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- authorize_settings_request() verified nonce and capability
147 $this->clear_stored_service_account();
148 $this->add_settings_notice('Service account configuration cleared successfully!', 'success');
149 return;
150 }
151
152 $result = $this->persist_submitted_service_account();
153
154 if ($result['status'] === 'error') {
155 // The parent save is still mid-flight; aborting here with an error
156 // keeps a bad credential from being reported as a successful save.
157 wp_send_json_error([
158 'errors' => ['Google Index: ' . $result['message']],
159 'message' => 'Google Index: ' . $result['message'],
160 ]);
161 }
162
163 if ($result['status'] === 'unavailable') {
164 // The sub-module is missing. Let the rest of the settings save
165 // finish and surface this as a notice instead of failing the page.
166 $this->add_settings_notice($result['message'], 'warning');
167 return;
168 }
169
170 if ($result['status'] === 'saved') {
171 $this->add_settings_notice('Google Index service account configured successfully!', 'success');
172 }
173 }
174
175 /**
176 * Read the submitted service account from $_POST/$_FILES, validate it, and
177 * persist it when it is a genuinely new credential.
178 *
179 * Returns a status rather than emitting a response because the caller runs
180 * inside another form's save: it must abort the parent save on a bad
181 * credential, not answer for the whole request.
182 *
183 * @return array{status:string, message:string} status is one of:
184 * 'saved' - a new credential was written
185 * 'unchanged' - nothing to write (empty, or the redacted display text)
186 * 'error' - validation or persistence failed; message explains why
187 */
188 private function persist_submitted_service_account()
189 {
190 // Load Google Index functionality
191 if (!$this->module_function_available('google_index_save_service_account')) {
192 if (file_exists(plugin_dir_path(__FILE__) . 'google-index-init.php')) {
193 require_once plugin_dir_path(__FILE__) . 'google-index-init.php';
194 }
195
196 // A partial deploy can leave the file present but the function
197 // undefined, so re-check rather than assuming the require worked.
198 if (!$this->module_function_available('google_index_save_service_account')) {
199 error_log('MetaSync Google Index: google_index_save_service_account() unavailable; expected google-index-init.php at ' . plugin_dir_path(__FILE__) . 'google-index-init.php'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
200
201 // Deliberately NOT 'error'. This optional sub-module failing to
202 // load must not abort the parent settings save — the embedded
203 // caller would turn that into a wp_send_json_error() and take
204 // the whole General Settings / Indexation Control page down.
205 return ['status' => 'unavailable', 'message' => 'The Google Index module could not be loaded, so the service account was not saved.'];
206 }
207 }
208
209 $service_account_json = '';
210
211 // Get JSON from textarea
212 if (isset($_POST['google_index_service_account_json'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- private, callers verify a nonce and capability first
213 $service_account_json = sanitize_textarea_field(wp_unslash($_POST['google_index_service_account_json'])); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- private, callers verify a nonce and capability first
214 }
215
216 // Override with file upload if provided
217 if (isset($_FILES['google_index_service_account_file']) && // phpcs:ignore WordPress.Security.NonceVerification.Missing -- private, callers verify a nonce and capability first
218 !empty($_FILES['google_index_service_account_file']['tmp_name']) && // phpcs:ignore WordPress.Security.NonceVerification.Missing -- private, callers verify a nonce and capability first
219 file_exists($_FILES['google_index_service_account_file']['tmp_name'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- private, callers verify a nonce and capability first
220
221 $uploaded_json = file_get_contents($_FILES['google_index_service_account_file']['tmp_name']); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- private, callers verify a nonce and capability first
222 if ($uploaded_json !== false) {
223 $service_account_json = $uploaded_json; // Removed unnecessary wp_unslash
224 }
225 }
226
227 if (empty($service_account_json) || trim($service_account_json) === '') {
228 return ['status' => 'unchanged', 'message' => ''];
229 }
230
231 // Skip if it contains redacted private key (user didn't paste new JSON)
232 if (strpos($service_account_json, self::REDACTED_MARKER) !== false) {
233 return ['status' => 'unchanged', 'message' => ''];
234 }
235
236 $validation = $this->validate_service_account_json($service_account_json);
237 if (!empty($validation['error'])) {
238 return ['status' => 'error', 'message' => $validation['error']];
239 }
240
241 if (!google_index_save_service_account($validation['data'])) {
242 return [
243 'status' => 'error',
244 'message' => 'Failed to save service account configuration. Please try again or check your JSON format.',
245 ];
246 }
247
248 return ['status' => 'saved', 'message' => 'Google Index service account configured successfully!'];
249 }
250
251 /**
252 * Whether a function the optional Google Index sub-module provides exists.
253 *
254 * Taking the name as a parameter keeps the check opaque to static analysis,
255 * which otherwise treats the second call in a load-then-verify pair as dead
256 * because it cannot see that the intervening require_once may define it.
257 *
258 * Genuinely impure: the same argument yields a different answer once the
259 * sub-module has been required, which is precisely why it is called twice.
260 *
261 * @phpstan-impure
262 *
263 * @param string $name Function name to look for.
264 * @return bool
265 */
266 private function module_function_available($name)
267 {
268 return function_exists($name);
269 }
270
271 /**
272 * Decode and structurally validate a service account JSON string.
273 *
274 * @param string $json Raw JSON text.
275 * @return array{data:array|null, error:string} error is '' when valid.
276 */
277 private function validate_service_account_json($json)
278 {
279 $data = json_decode($json, true);
280
281 if (json_last_error() !== JSON_ERROR_NONE) {
282 $message = 'Invalid JSON format';
283 switch (json_last_error()) {
284 case JSON_ERROR_SYNTAX:
285 $message .= ' - Syntax error in JSON';
286 break;
287 case JSON_ERROR_UTF8:
288 $message .= ' - Invalid UTF-8 encoding';
289 break;
290 default:
291 $message .= ' - ' . json_last_error_msg();
292 break;
293 }
294
295 return ['data' => null, 'error' => $message . '. Please check your service account JSON.'];
296 }
297
298 if (!is_array($data)) {
299 return [
300 'data' => null,
301 'error' => 'JSON must be an object/array. Please check your service account JSON format.',
302 ];
303 }
304
305 $required_fields = ['type', 'project_id', 'private_key_id', 'private_key', 'client_email', 'client_id', 'auth_uri', 'token_uri'];
306 $missing_fields = [];
307
308 foreach ($required_fields as $field) {
309 if (!isset($data[$field]) || empty($data[$field])) {
310 $missing_fields[] = $field;
311 }
312 }
313
314 if (!empty($missing_fields)) {
315 return [
316 'data' => null,
317 'error' => 'Missing required fields - ' . implode(', ', $missing_fields) . '. Please ensure you have a complete service account JSON.',
318 ];
319 }
320
321 return ['data' => $data, 'error' => ''];
322 }
323
324 /**
325 * Remove the stored service account and any token minted from it.
326 *
327 * A cached access token that outlives the credential it was issued for
328 * would keep working until it expired.
329 */
330 private function clear_stored_service_account()
331 {
332 delete_option('google_index_service_account');
333
334 if (function_exists('google_index_direct')) {
335 $instance = google_index_direct();
336 if ($instance) {
337 $instance->clear_token_cache();
338 }
339 }
340 }
341
342 /**
343 * Add a settings notice to be displayed after AJAX save
344 *
345 * @param string $message Notice message
346 * @param string $type Notice type: 'success', 'error', 'warning', 'info'
347 */
348 private function add_settings_notice($message, $type = 'info')
349 {
350 $notices = get_transient('google_index_admin_notices') ?: [];
351 $notices[] = [
352 'message' => $message,
353 'type' => $type,
354 'time' => time()
355 ];
356
357 // Store notices for 30 seconds (enough time for page redirect)
358 set_transient('google_index_admin_notices', $notices, 30);
359 }
360
361 /**
362 * Display admin notices for Google Index settings
363 */
364 public function display_admin_notices()
365 {
366 // Only show notices on MetaSync settings pages
367 $page_slug = $this->get_metasync_page_slug();
368 if ( ! function_exists( 'get_current_screen' ) ) {
369 return;
370 }
371 $current_screen = get_current_screen();
372
373 if (!$current_screen || strpos($current_screen->id, $page_slug) === false) {
374 return;
375 }
376
377 // Get and display notices
378 $notices = get_transient('google_index_admin_notices');
379 if (!empty($notices)) {
380 foreach ($notices as $notice) {
381 $class = 'notice notice-' . $notice['type'] . ' is-dismissible';
382 echo '<div class="' . esc_attr($class) . '">';
383 echo '<p><strong>Google Index:</strong> ' . esc_html($notice['message']) . '</p>';
384 echo '</div>';
385 }
386
387 // Clear notices after displaying
388 delete_transient('google_index_admin_notices');
389 }
390 }
391
392 /**
393 * AJAX handler for testing connection
394 */
395 public function ajax_test_connection()
396 {
397 // Check nonce and permissions
398 if (!current_user_can('manage_options') ||
399 !wp_verify_nonce(wp_unslash($_POST['nonce'] ?? ''), 'metasync_google_index_direct_test')) {
400 wp_die('Security check failed');
401 }
402
403 // Load Google Index functionality
404 if (!function_exists('google_index_direct')) {
405 if (file_exists(plugin_dir_path(__FILE__) . 'google-index-init.php')) {
406 require_once plugin_dir_path(__FILE__) . 'google-index-init.php';
407 } else {
408 error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(__FILE__) . 'google-index-init.php'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
409 return;
410 }
411 }
412
413 try {
414 // Test the connection
415 $google_index = google_index_direct();
416 $test_results = $google_index->test_connection();
417
418 wp_send_json_success([
419 'message' => 'Connection test completed',
420 'results' => $test_results
421 ]);
422
423 } catch (Exception $e) {
424 wp_send_json_error([
425 'message' => 'Test failed: ' . $e->getMessage()
426 ]);
427 }
428 }
429
430 /**
431 * Authorize the shared MetaSync settings AJAX action before secret handling.
432 */
433 private function authorize_settings_request()
434 {
435 if (!current_user_can('manage_options')) {
436 wp_send_json_error(array('message' => 'Insufficient permissions.'), 403);
437 }
438
439 $action = isset($_POST['action']) ? sanitize_key(wp_unslash($_POST['action'])) : '';
440 $nonce_field = '';
441 $nonce_action = '';
442
443 if ($action === 'meta_sync_save_settings') {
444 $nonce_field = 'meta_sync_nonce';
445 $nonce_action = 'meta_sync_general_setting_nonce';
446 } elseif ($action === 'meta_sync_save_seo_controls') {
447 $nonce_field = 'meta_sync_seo_controls_nonce';
448 $nonce_action = 'meta_sync_seo_controls_nonce';
449 }
450
451 if (
452 $nonce_field === ''
453 || !isset($_POST[$nonce_field])
454 || !wp_verify_nonce(wp_unslash($_POST[$nonce_field]), $nonce_action)
455 ) {
456 wp_send_json_error(array('message' => 'Invalid nonce'), 403);
457 }
458
459 return true;
460 }
461
462 /**
463 * Enqueue admin scripts and styles
464 */
465 public function enqueue_admin_scripts($hook)
466 {
467 // Only load on MetaSync settings pages
468 $page_slug = $this->get_metasync_page_slug();
469 if (strpos($hook, $page_slug) === false) {
470 return;
471 }
472
473 $current_tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check to enqueue scripts
474 if ($current_tab !== 'general' && strpos($hook, 'seo-controls') === false) {
475 return;
476 }
477
478 // Add inline JavaScript for functionality
479 wp_add_inline_script('jquery', $this->get_admin_javascript());
480 }
481
482 /**
483 * Get JavaScript for admin functionality
484 *
485 * @return string JavaScript code
486 */
487 private function get_admin_javascript()
488 {
489 $nonce = wp_create_nonce('metasync_google_index_direct_test');
490
491 return "
492 jQuery(document).ready(function($) {
493 // What the textarea held immediately before a file overwrote it.
494 // Seeded from the server-rendered value and re-snapshotted on each
495 // file pick, so 'Remove' undoes exactly the file selection — it
496 // does not also discard a credential the user typed by hand before
497 // mis-clicking Choose File.
498 var originalJsonValue = $('#google_index_service_account_json').val();
499
500 // Test Connection builds its results list as an HTML string. The
501 // values come from the API/server rather than the page, so escape
502 // them before they reach that sink.
503 function escHtml(value) {
504 return $('<div>').text(value === undefined || value === null ? '' : value).html();
505 }
506
507 function showFileMessage(text, type) {
508 var box = $('#google-index-file-messages');
509 if (!box.length) { return; }
510 box.empty();
511 if (!text) { return; }
512 var notice = $('<div>').addClass('notice notice-' + type + ' inline');
513 notice.append($('<p>').text(text));
514 box.append(notice);
515 }
516
517 function clearSelectedFile(restoreTextarea) {
518 var input = $('#google_index_service_account_file');
519 if (input.length) {
520 input.val('');
521 }
522 $('#google-index-selected-file').hide();
523 $('#google-index-selected-file-name').text('');
524 showFileMessage('', 'info');
525
526 if (restoreTextarea) {
527 var textarea = $('#google_index_service_account_json');
528 textarea.val(originalJsonValue);
529 textarea.trigger('input').trigger('change');
530 $('#metaSyncGeneralSetting, #metaSyncSeoControlsForm').trigger('change');
531 }
532 }
533
534 // Integration with MetaSync's unsaved changes detection
535 function integrateWithUnsavedChangesDetection() {
536 // Monitor textarea for changes (avoid recursion)
537 $('#google_index_service_account_json').on('input change paste keyup', function(e) {
538 // Trigger MetaSync's change detection (works on both Settings and Indexation Control pages)
539 $('#metaSyncGeneralSetting, #metaSyncSeoControlsForm').trigger('change');
540 });
541
542 // Handle file upload with auto-populate and change detection
543 $('#google_index_service_account_file').on('change', function(e) {
544 var file = e.target.files[0];
545 var textarea = $('#google_index_service_account_json');
546
547 if (!file) {
548 clearSelectedFile(true);
549 return;
550 }
551
552 if (!file.name.toLowerCase().endsWith('.json')) {
553 // Inline feedback instead of the old blocking alert().
554 showFileMessage('\"' + file.name + '\" is not a .json file. Choose the service account key file you downloaded from Google Cloud.', 'error');
555 $('#google-index-selected-file').hide();
556 $('#google_index_service_account_file').val('');
557 return;
558 }
559
560 // Snapshot only when no file is currently selected, i.e. at
561 // the START of a pick sequence. Re-snapshotting on every
562 // pick would make Remove restore file A's contents after
563 // the user picked A then B then removed — leaving a
564 // credential in the box while the UI says no file is
565 // selected.
566 if (!$('#google-index-selected-file').is(':visible')) {
567 originalJsonValue = textarea.val();
568 }
569
570 var reader = new FileReader();
571 reader.onload = function(ev) {
572 try {
573 var json = JSON.parse(ev.target.result);
574 var jsonString = JSON.stringify(json, null, 2);
575 textarea.val(jsonString);
576
577 $('#google-index-selected-file-name').text(file.name);
578 $('#google-index-selected-file').show();
579 showFileMessage('\"' + file.name + '\" loaded. Save the page to store it.', 'info');
580
581 // Create and dispatch native events to ensure proper detection
582 setTimeout(function() {
583 var inputEvent = new Event('input', { bubbles: true, cancelable: true });
584 var changeEvent = new Event('change', { bubbles: true, cancelable: true });
585
586 textarea[0].dispatchEvent(inputEvent);
587 textarea[0].dispatchEvent(changeEvent);
588
589 textarea.trigger('input').trigger('change');
590 }, 100);
591 } catch (err) {
592 showFileMessage('\"' + file.name + '\" is not valid JSON. Re-download the key file from Google Cloud and try again.', 'error');
593 $('#google-index-selected-file').hide();
594 $('#google_index_service_account_file').val('');
595 }
596 };
597 reader.onerror = function() {
598 showFileMessage('\"' + file.name + '\" could not be read. Try choosing it again.', 'error');
599 $('#google-index-selected-file').hide();
600 $('#google_index_service_account_file').val('');
601 };
602 reader.readAsText(file);
603 });
604
605 // Remove/reselect
606 $('#google-index-remove-file').on('click', function(e) {
607 e.preventDefault();
608 clearSelectedFile(true);
609 });
610 }
611
612 // Initialize integration after a small delay to ensure MetaSync is ready
613 setTimeout(integrateWithUnsavedChangesDetection, 100);
614
615 // Handle test connection button
616 $('#google-index-test-connection').on('click', function(e) {
617 e.preventDefault();
618
619 var button = $(this);
620 var resultDiv = $('#google-index-test-results');
621
622 button.prop('disabled', true).text('🔄 Testing...');
623 resultDiv.html('<div class=\"notice notice-info inline\"><p>Testing connection...</p></div>');
624
625 $.ajax({
626 url: ajaxurl,
627 type: 'POST',
628 data: {
629 action: 'metasync_google_index_direct_test',
630 nonce: '{$nonce}'
631 },
632 success: function(response) {
633 if (response.success) {
634 var results = response.data.results;
635 var html = '<div class=\"notice notice-success inline\">';
636 html += '<p><strong>�
637 Connection Test Results:</strong></p>';
638 html += '<ul>';
639
640 if (results.token_test) {
641 html += '<li><strong>Token Generation:</strong> ' + (results.token_test.success ? '�
642 Success' : '❌ Failed') + '</li>';
643 html += '<li><strong>Token Cached:</strong> ' + (results.token_test.cached ? '�
644 Yes' : '⚪ No') + '</li>';
645 }
646
647 if (results.credentials_test) {
648 html += '<li><strong>Service Account:</strong> ' + escHtml(results.credentials_test.client_email) + '</li>';
649 html += '<li><strong>Project ID:</strong> ' + escHtml(results.credentials_test.project_id) + '</li>';
650 html += '<li><strong>Private Key:</strong> ' + (results.credentials_test.has_private_key ? '�
651 Present' : '❌ Missing') + '</li>';
652 }
653
654 if (results.homepage_test) {
655 if (results.homepage_test.success) {
656 html += '<li><strong>Homepage Status:</strong> �
657 Success</li>';
658 } else {
659 html += '<li><strong>Homepage Status:</strong> ⚠️ ' + escHtml(results.homepage_test.error.message) + '</li>';
660 if (results.homepage_test.note) {
661 html += '<li><strong>Note:</strong> ' + escHtml(results.homepage_test.note) + '</li>';
662 }
663 }
664 }
665
666 html += '</ul></div>';
667 resultDiv.html(html);
668 } else {
669 resultDiv.html('<div class=\"notice notice-error inline\"><p><strong>❌ Test Failed:</strong> ' + escHtml(response.data.message) + '</p></div>');
670 }
671 },
672 error: function() {
673 resultDiv.html('<div class=\"notice notice-error inline\"><p><strong>❌ Connection Error:</strong> Unable to perform test.</p></div>');
674 },
675 complete: function() {
676 button.prop('disabled', false).text('🧪 Test Connection');
677 }
678 });
679 });
680
681 // Handle clear configuration button
682 $('#google-index-clear-config').on('click', function(e) {
683 e.preventDefault();
684
685 if (!confirm('Are you sure you want to clear the service account configuration?')) {
686 return;
687 }
688
689 // Embedded contexts: piggyback on the page's settings form.
690 var input = $('<input>')
691 .attr('type', 'hidden')
692 .attr('name', 'google_index_clear_config')
693 .attr('value', '1');
694
695 // Append to the correct form (Indexation Control vs General Settings) and submit via AJAX
696 var clrForm = $('#metaSyncSeoControlsForm').length ? $('#metaSyncSeoControlsForm') : $('#metaSyncGeneralSetting');
697 var clrAction = $('#metaSyncSeoControlsForm').length ? 'meta_sync_save_seo_controls' : 'meta_sync_save_settings';
698 clrForm.append(input);
699 var clrData = clrForm.serialize() + '&action=' + clrAction;
700
701 // Submit the clear request directly via AJAX, then reload
702 $.ajax({ url: ajaxurl || metaSync.ajax_url, type: 'POST', data: clrData, complete: function() { window.location.reload(); } });
703 });
704
705 });
706 ";
707 }
708 }
709
710 // Initialize admin functionality
711 if (is_admin()) {
712 new Google_Index_Admin();
713 }
714