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
← All changes | google-index/class-google-index-admin.php +24 -225 2.7.0 → 2.7.2 View file →
@@ -21,13 +21,8 @@
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 - /**
30 25 * Placeholder the redacted display JSON uses in place of the private key.
31 26 *
32 27 * Its presence means the textarea still holds what the server rendered, so
33 28 * the submission carries no new credential and must leave storage alone.
@@ -51,13 +46,8 @@
51 46 add_action('admin_notices', array($this, 'display_admin_notices'));
52 47
53 48 // AJAX handlers
54 49 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 50 }
61 51
62 52
63 53 /**
@@ -114,9 +104,9 @@
114 104 if (!function_exists('google_index_direct')) {
115 105 if (file_exists(plugin_dir_path(__FILE__) . 'google-index-init.php')) {
116 106 require_once plugin_dir_path(__FILE__) . 'google-index-init.php';
117 107 } else {
118 - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(__FILE__) . 'google-index-init.php');
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
119 109 return;
120 110 }
121 111 }
122 112
@@ -141,11 +131,11 @@
141 131 */
142 132 public function process_google_index_settings()
143 133 {
144 134 // Only process if our fields are present in the request
145 - if (!isset($_POST['google_index_service_account_json']) &&
146 - !isset($_POST['google_index_clear_config']) &&
147 - !isset($_FILES['google_index_service_account_file'])) {
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
148 138 return; // No Google Index data to process
149 139 }
150 140
151 141 if (!$this->authorize_settings_request()) {
@@ -152,9 +142,9 @@
152 142 return;
153 143 }
154 144
155 145 // Clearing does not need to load or inspect credential contents.
156 - if (isset($_POST['google_index_clear_config'])) {
146 + if (isset($_POST['google_index_clear_config'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- authorize_settings_request() verified nonce and capability
157 147 $this->clear_stored_service_account();
158 148 $this->add_settings_notice('Service account configuration cleared successfully!', 'success');
159 149 return;
160 150 }
@@ -185,13 +175,11 @@
185 175 /**
186 176 * Read the submitted service account from $_POST/$_FILES, validate it, and
187 177 * persist it when it is a genuinely new credential.
188 178 *
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.
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.
194 182 *
195 183 * @return array{status:string, message:string} status is one of:
196 184 * 'saved' - a new credential was written
197 185 * 'unchanged' - nothing to write (empty, or the redacted display text)
@@ -207,9 +195,9 @@
207 195
208 196 // A partial deploy can leave the file present but the function
209 197 // undefined, so re-check rather than assuming the require worked.
210 198 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');
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
212 200
213 201 // Deliberately NOT 'error'. This optional sub-module failing to
214 202 // load must not abort the parent settings save — the embedded
215 203 // caller would turn that into a wp_send_json_error() and take
@@ -220,18 +208,18 @@
220 208
221 209 $service_account_json = '';
222 210
223 211 // Get JSON from textarea
224 - if (isset($_POST['google_index_service_account_json'])) {
225 - $service_account_json = sanitize_textarea_field(wp_unslash($_POST['google_index_service_account_json']));
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
226 214 }
227 215
228 216 // Override with file upload if provided
229 - if (isset($_FILES['google_index_service_account_file']) &&
230 - !empty($_FILES['google_index_service_account_file']['tmp_name']) &&
231 - file_exists($_FILES['google_index_service_account_file']['tmp_name'])) {
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
232 220
233 - $uploaded_json = file_get_contents($_FILES['google_index_service_account_file']['tmp_name']);
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
234 222 if ($uploaded_json !== false) {
235 223 $service_account_json = $uploaded_json; // Removed unnecessary wp_unslash
236 224 }
237 225 }
@@ -333,67 +321,12 @@
333 321 return ['data' => $data, 'error' => ''];
334 322 }
335 323
336 324 /**
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 325 * Remove the stored service account and any token minted from it.
393 326 *
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.
327 + * A cached access token that outlives the credential it was issued for
328 + * would keep working until it expired.
396 329 */
397 330 private function clear_stored_service_account()
398 331 {
399 332 delete_option('google_index_service_account');
@@ -404,31 +337,8 @@
404 337 $instance->clear_token_cache();
405 338 }
406 339 }
407 340 }
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 341
432 342 /**
433 343 * Add a settings notice to be displayed after AJAX save
434 344 *
@@ -467,10 +377,10 @@
467 377 // Get and display notices
468 378 $notices = get_transient('google_index_admin_notices');
469 379 if (!empty($notices)) {
470 380 foreach ($notices as $notice) {
471 - $class = 'notice notice-' . esc_attr($notice['type']) . ' is-dismissible';
472 - echo '<div class="' . $class . '">';
381 + $class = 'notice notice-' . $notice['type'] . ' is-dismissible';
382 + echo '<div class="' . esc_attr($class) . '">';
473 383 echo '<p><strong>Google Index:</strong> ' . esc_html($notice['message']) . '</p>';
474 384 echo '</div>';
475 385 }
476 386
@@ -494,9 +404,9 @@
494 404 if (!function_exists('google_index_direct')) {
495 405 if (file_exists(plugin_dir_path(__FILE__) . 'google-index-init.php')) {
496 406 require_once plugin_dir_path(__FILE__) . 'google-index-init.php';
497 407 } else {
498 - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(__FILE__) . 'google-index-init.php');
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
499 409 return;
500 410 }
501 411 }
502 412
@@ -559,20 +469,11 @@
559 469 if (strpos($hook, $page_slug) === false) {
560 470 return;
561 471 }
562 472
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'])) : '';
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';
572 - if ($current_tab !== 'general' && strpos($hook, 'seo-controls') === false) {
573 - return;
574 - }
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;
575 476 }
576 477
577 478 // Add inline JavaScript for functionality
578 479 wp_add_inline_script('jquery', $this->get_admin_javascript());
@@ -595,15 +496,8 @@
595 496 // does not also discard a credential the user typed by hand before
596 497 // mis-clicking Choose File.
597 498 var originalJsonValue = $('#google_index_service_account_json').val();
598 499
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 500 // Test Connection builds its results list as an HTML string. The
607 501 // values come from the API/server rather than the page, so escape
608 502 // them before they reach that sink.
609 503 function escHtml(value) {
@@ -619,25 +513,9 @@
619 513 notice.append($('<p>').text(text));
620 514 box.append(notice);
621 515 }
622 516
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 517 function clearSelectedFile(restoreTextarea) {
639 - fileReadPending = false;
640 518 var input = $('#google_index_service_account_file');
641 519 if (input.length) {
642 520 input.val('');
643 521 }
@@ -689,11 +567,9 @@
689 567 originalJsonValue = textarea.val();
690 568 }
691 569
692 570 var reader = new FileReader();
693 - fileReadPending = true;
694 571 reader.onload = function(ev) {
695 - fileReadPending = false;
696 572 try {
697 573 var json = JSON.parse(ev.target.result);
698 574 var jsonString = JSON.stringify(json, null, 2);
699 575 textarea.val(jsonString);
@@ -699,9 +575,9 @@
699 575 textarea.val(jsonString);
700 576
701 577 $('#google-index-selected-file-name').text(file.name);
702 578 $('#google-index-selected-file').show();
703 - showFileMessage('\"' + file.name + '\" loaded. Click Save Configuration to store it.', 'info');
579 + showFileMessage('\"' + file.name + '\" loaded. Save the page to store it.', 'info');
704 580
705 581 // Create and dispatch native events to ensure proper detection
706 582 setTimeout(function() {
707 583 var inputEvent = new Event('input', { bubbles: true, cancelable: true });
@@ -718,9 +594,8 @@
718 594 $('#google_index_service_account_file').val('');
719 595 }
720 596 };
721 597 reader.onerror = function() {
722 - fileReadPending = false;
723 598 showFileMessage('\"' + file.name + '\" could not be read. Try choosing it again.', 'error');
724 599 $('#google-index-selected-file').hide();
725 600 $('#google_index_service_account_file').val('');
726 601 };
@@ -736,55 +611,8 @@
736 611
737 612 // Initialize integration after a small delay to ensure MetaSync is ready
738 613 setTimeout(integrateWithUnsavedChangesDetection, 100);
739 614
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 615 // Handle test connection button
788 616 $('#google-index-test-connection').on('click', function(e) {
789 617 e.preventDefault();
790 618
@@ -849,37 +677,8 @@
849 677 $('#google-index-clear-config').on('click', function(e) {
850 678 e.preventDefault();
851 679
852 680 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 - });
882 681 return;
883 682 }
884 683
885 684 // Embedded contexts: piggyback on the page's settings form.