PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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.1, at google-index/class-google-index-admin.php

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