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
← All changes | google-index/class-google-index-admin.php +212 -11 2.7.2 → 2.7.1 View file →
@@ -21,8 +21,13 @@
21 21 */
22 22 private const SECTION_GOOGLE_INDEX = 'google_index_direct_settings';
23 23
24 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 + /**
25 30 * Placeholder the redacted display JSON uses in place of the private key.
26 31 *
27 32 * Its presence means the textarea still holds what the server rendered, so
28 33 * the submission carries no new credential and must leave storage alone.
@@ -46,8 +51,13 @@
46 51 add_action('admin_notices', array($this, 'display_admin_notices'));
47 52
48 53 // AJAX handlers
49 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'));
50 60 }
51 61
52 62
53 63 /**
@@ -175,11 +185,13 @@
175 185 /**
176 186 * Read the submitted service account from $_POST/$_FILES, validate it, and
177 187 * persist it when it is a genuinely new credential.
178 188 *
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.
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.
182 194 *
183 195 * @return array{status:string, message:string} status is one of:
184 196 * 'saved' - a new credential was written
185 197 * 'unchanged' - nothing to write (empty, or the redacted display text)
@@ -321,12 +333,67 @@
321 333 return ['data' => $data, 'error' => ''];
322 334 }
323 335
324 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 + /**
325 392 * Remove the stored service account and any token minted from it.
326 393 *
327 - * A cached access token that outlives the credential it was issued for
328 - * would keep working until it expired.
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.
329 396 */
330 397 private function clear_stored_service_account()
331 398 {
332 399 delete_option('google_index_service_account');
@@ -337,8 +404,31 @@
337 404 $instance->clear_token_cache();
338 405 }
339 406 }
340 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 + }
341 431
342 432 /**
343 433 * Add a settings notice to be displayed after AJAX save
344 434 *
@@ -377,10 +467,10 @@
377 467 // Get and display notices
378 468 $notices = get_transient('google_index_admin_notices');
379 469 if (!empty($notices)) {
380 470 foreach ($notices as $notice) {
381 - $class = 'notice notice-' . $notice['type'] . ' is-dismissible';
382 - echo '<div class="' . esc_attr($class) . '">';
471 + $class = 'notice notice-' . esc_attr($notice['type']) . ' is-dismissible';
472 + echo '<div class="' . $class . '">';
383 473 echo '<p><strong>Google Index:</strong> ' . esc_html($notice['message']) . '</p>';
384 474 echo '</div>';
385 475 }
386 476
@@ -469,11 +559,20 @@
469 559 if (strpos($hook, $page_slug) === false) {
470 560 return;
471 561 }
472 562
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;
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 + }
476 575 }
477 576
478 577 // Add inline JavaScript for functionality
479 578 wp_add_inline_script('jquery', $this->get_admin_javascript());
@@ -496,8 +595,15 @@
496 595 // does not also discard a credential the user typed by hand before
497 596 // mis-clicking Choose File.
498 597 var originalJsonValue = $('#google_index_service_account_json').val();
499 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 +
500 606 // Test Connection builds its results list as an HTML string. The
501 607 // values come from the API/server rather than the page, so escape
502 608 // them before they reach that sink.
503 609 function escHtml(value) {
@@ -513,9 +619,25 @@
513 619 notice.append($('<p>').text(text));
514 620 box.append(notice);
515 621 }
516 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 +
517 638 function clearSelectedFile(restoreTextarea) {
639 + fileReadPending = false;
518 640 var input = $('#google_index_service_account_file');
519 641 if (input.length) {
520 642 input.val('');
521 643 }
@@ -567,9 +689,11 @@
567 689 originalJsonValue = textarea.val();
568 690 }
569 691
570 692 var reader = new FileReader();
693 + fileReadPending = true;
571 694 reader.onload = function(ev) {
695 + fileReadPending = false;
572 696 try {
573 697 var json = JSON.parse(ev.target.result);
574 698 var jsonString = JSON.stringify(json, null, 2);
575 699 textarea.val(jsonString);
@@ -575,9 +699,9 @@
575 699 textarea.val(jsonString);
576 700
577 701 $('#google-index-selected-file-name').text(file.name);
578 702 $('#google-index-selected-file').show();
579 - showFileMessage('\"' + file.name + '\" loaded. Save the page to store it.', 'info');
703 + showFileMessage('\"' + file.name + '\" loaded. Click Save Configuration to store it.', 'info');
580 704
581 705 // Create and dispatch native events to ensure proper detection
582 706 setTimeout(function() {
583 707 var inputEvent = new Event('input', { bubbles: true, cancelable: true });
@@ -594,8 +718,9 @@
594 718 $('#google_index_service_account_file').val('');
595 719 }
596 720 };
597 721 reader.onerror = function() {
722 + fileReadPending = false;
598 723 showFileMessage('\"' + file.name + '\" could not be read. Try choosing it again.', 'error');
599 724 $('#google-index-selected-file').hide();
600 725 $('#google_index_service_account_file').val('');
601 726 };
@@ -611,8 +736,55 @@
611 736
612 737 // Initialize integration after a small delay to ensure MetaSync is ready
613 738 setTimeout(integrateWithUnsavedChangesDetection, 100);
614 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 +
615 787 // Handle test connection button
616 788 $('#google-index-test-connection').on('click', function(e) {
617 789 e.preventDefault();
618 790
@@ -677,8 +849,37 @@
677 849 $('#google-index-clear-config').on('click', function(e) {
678 850 e.preventDefault();
679 851
680 852 if (!confirm('Are you sure you want to clear the service account configuration?')) {
853 + return;
854 + }
855 +
856 + var dedicatedNonce = $('#metasync_google_index_account_nonce').val();
857 +
858 + if (dedicatedNonce) {
859 + // Standalone Instant Indexing page: there is no settings
860 + // form to serialize, so post to the dedicated endpoint.
861 + $.ajax({
862 + url: (typeof ajaxurl !== 'undefined') ? ajaxurl : metaSync.ajax_url,
863 + type: 'POST',
864 + data: {
865 + action: 'metasync_google_index_clear_account',
866 + metasync_google_index_account_nonce: dedicatedNonce
867 + },
868 + success: function(response) {
869 + if (response && response.success) {
870 + window.location.reload();
871 + return;
872 + }
873 + var msg = (response && response.data && response.data.message)
874 + ? response.data.message
875 + : 'Failed to clear the service account.';
876 + showSaveMessage(msg, 'error');
877 + },
878 + error: function() {
879 + showSaveMessage('Network error while clearing. Please try again.', 'error');
880 + }
881 + });
681 882 return;
682 883 }
683 884
684 885 // Embedded contexts: piggyback on the page's settings form.