PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/api/class-integrations-endpoint.php +39 -140 1.28.02.7.0 View file →
@@ -102,9 +102,9 @@
102 102 ],
103 103 [
104 104 'methods' => 'POST',
105 105 'callback' => [$this, 'update_settings'],
106 - 'permission_callback' => [$this, 'check_manage_permissions'],
106 + 'permission_callback' => [$this, 'check_credential_permissions'],
107 107 'args' => $this->get_settings_args()
108 108 ]
109 109 ]
110 110 );
@@ -116,60 +116,13 @@
116 116 [
117 117 [
118 118 'methods' => 'POST',
119 119 'callback' => [$this, 'test_connections'],
120 - 'permission_callback' => [$this, 'check_manage_permissions']
120 + 'permission_callback' => [$this, 'check_credential_permissions']
121 121 ]
122 122 ]
123 123 );
124 124
125 - // Verify GA4 tracking
126 - register_rest_route(
127 - $this->namespace,
128 - '/' . $this->rest_base . '/verify-ga4-tracking',
129 - [
130 - [
131 - 'methods' => 'POST',
132 - 'callback' => [$this, 'verify_ga4_tracking'],
133 - 'permission_callback' => [$this, 'check_manage_permissions'],
134 - 'args' => [
135 - 'measurement_id' => [
136 - // Optional, and an empty string is meaningful:
137 - // verify_tracking() then discovers the ID from the
138 - // live homepage. A `required` + `pattern` arg
139 - // rejected that at the REST layer before the
140 - // handler ran, which is why verification was
141 - // unreachable on sites with no stored ID (#250).
142 - 'required' => false,
143 - 'type' => 'string',
144 - 'default' => '',
145 - // No regex delimiters — WP's REST validator wraps the
146 - // pattern in its own (#...#u), so a leading/trailing
147 - // slash would require literal slashes in the value.
148 - // The empty alternative keeps discovery reachable
149 - // while still rejecting a malformed ID.
150 - 'pattern' => '^(G-[A-Z0-9]{10})?$',
151 - 'sanitize_callback' => 'sanitize_text_field',
152 - 'description' => 'GA4 Measurement ID in format G-XXXXXXXXXX. Omit or leave empty to auto-detect the ID from the site homepage.'
153 - ]
154 - ]
155 - ]
156 - ]
157 - );
158 -
159 - // Detect GA4 conflicts
160 - register_rest_route(
161 - $this->namespace,
162 - '/' . $this->rest_base . '/detect-ga4-conflicts',
163 - [
164 - [
165 - 'methods' => 'GET',
166 - 'callback' => [$this, 'detect_ga4_conflicts'],
167 - 'permission_callback' => [$this, 'check_read_permissions']
168 - ]
169 - ]
170 - );
171 -
172 125 // Get Search Console Sites
173 126 register_rest_route(
174 127 $this->namespace,
175 128 '/' . $this->rest_base . '/search-console/sites',
@@ -192,9 +145,9 @@
192 145 // Disconnect Google Account
193 146 register_rest_route($this->namespace, '/integrations/google/disconnect', [
194 147 'methods' => WP_REST_Server::CREATABLE,
195 148 'callback' => [$this, 'disconnect_google_account'],
196 - 'permission_callback' => [$this, 'check_manage_permissions'] // Changed to check_manage_permissions for consistency
149 + 'permission_callback' => [$this, 'check_credential_permissions']
197 150 ]);
198 151
199 152 // Note: there is no save-google-token route. Tokens are swapped
200 153 // server-to-server in Google_OAuth_Proxy and never pass through the
@@ -338,16 +291,8 @@
338 291 $settings['google_analytics_api_key'] = $this->settings->get('google_analytics_api_key');
339 292 $settings['google_search_console_api_key'] = $this->settings->get('google_search_console_api_key');
340 293 $settings['google_pagespeed_api_key'] = $this->settings->get('google_pagespeed_api_key');
341 294
342 - // Get GA4 tracking settings (let Settings class handle defaults)
343 - $settings['ga4_measurement_id'] = $this->settings->get('ga4_measurement_id');
344 - $settings['ga4_auto_inject'] = $this->settings->get('ga4_auto_inject');
345 - $settings['ga4_anonymize_ip'] = $this->settings->get('ga4_anonymize_ip');
346 - $settings['ga4_exclude_admin'] = $this->settings->get('ga4_exclude_admin');
347 - $settings['ga4_tracking_verified'] = $this->settings->get('ga4_tracking_verified');
348 - $settings['ga4_last_verification'] = $this->settings->get('ga4_last_verification');
349 -
350 295 // Get other integration settings (let Settings class handle defaults)
351 296 $settings['api_timeout'] = $this->settings->get('api_timeout');
352 297 $settings['enable_rate_limiting'] = $this->settings->get('enable_rate_limiting');
353 298 $settings['cache_duration'] = $this->settings->get('cache_duration');
@@ -403,25 +348,33 @@
403 348 $sanitized[$key_field] = sanitize_text_field($settings[$key_field]);
404 349 }
405 350 }
406 351
407 - // Sanitize numeric settings
408 - $sanitized['api_timeout'] = absint($settings['api_timeout'] ?? 30);
409 - $sanitized['cache_duration'] = absint($settings['cache_duration'] ?? 3600);
352 + // A key the payload never mentioned is left alone rather than being
353 + // reset to a hard-coded default. These fallbacks used to fire on every
354 + // save, so a partial payload — or a setting the admin has no control
355 + // for, like retry_failed_requests — silently reverted to the default a
356 + // site owner had deliberately changed in code (#297).
357 + $numeric = ['api_timeout', 'cache_duration'];
410 358
411 - // Sanitize GA4 tracking settings
412 - $sanitized['ga4_measurement_id'] = sanitize_text_field($settings['ga4_measurement_id'] ?? '');
413 - $sanitized['ga4_auto_inject'] = isset($settings['ga4_auto_inject']) ? (bool) $settings['ga4_auto_inject'] : false;
414 - $sanitized['ga4_anonymize_ip'] = isset($settings['ga4_anonymize_ip']) ? (bool) $settings['ga4_anonymize_ip'] : false;
415 - $sanitized['ga4_exclude_admin'] = isset($settings['ga4_exclude_admin']) ? (bool) $settings['ga4_exclude_admin'] : false;
416 - $sanitized['ga4_tracking_verified'] = isset($settings['ga4_tracking_verified']) ? (bool) $settings['ga4_tracking_verified'] : false;
417 - $sanitized['ga4_last_verification'] = sanitize_text_field($settings['ga4_last_verification'] ?? '');
359 + foreach ($numeric as $key) {
360 + if (array_key_exists($key, $settings)) {
361 + $sanitized[$key] = absint($settings[$key]);
362 + }
363 + }
418 364
419 - // Sanitize boolean settings
420 - $sanitized['enable_rate_limiting'] = isset($settings['enable_rate_limiting']) ? (bool) $settings['enable_rate_limiting'] : true;
421 - $sanitized['auto_test_connections'] = isset($settings['auto_test_connections']) ? (bool) $settings['auto_test_connections'] : true;
422 - $sanitized['retry_failed_requests'] = isset($settings['retry_failed_requests']) ? (bool) $settings['retry_failed_requests'] : true;
365 + $booleans = [
366 + 'enable_rate_limiting',
367 + 'auto_test_connections',
368 + 'retry_failed_requests',
369 + ];
423 370
371 + foreach ($booleans as $key) {
372 + if (array_key_exists($key, $settings)) {
373 + $sanitized[$key] = (bool) $settings[$key];
374 + }
375 + }
376 +
424 377 return $sanitized;
425 378 }
426 379
427 380 /**
@@ -802,9 +755,9 @@
802 755 * @since 1.0.0
803 756 * @param WP_REST_Request $request Request object
804 757 * @return WP_REST_Response|WP_Error Response object
805 758 */
806 - public function disconnect_google_account(WP_REST_Request $request): WP_REST_Response|WP_Error {
759 + public function disconnect_google_account(WP_REST_Request $request) {
807 760 try {
808 761 // Best-effort revoke at Google so the refresh token (which never
809 762 // auto-expires) can't keep querying on the admin's behalf after
810 763 // disconnect. Failure here must not block local cleanup.
@@ -891,80 +844,24 @@
891 844 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_settings');
892 845 }
893 846
894 847 /**
895 - * Verify GA4 tracking
896 - * Following ThinkRank API response patterns
848 + * Check permissions for credential-managing operations.
897 849 *
898 - * @since 1.0.0
899 - * @param WP_REST_Request $request Request object
900 - * @return WP_REST_Response|WP_Error Response object
850 + * Writing provider API keys, disconnecting Google (a server-side token
851 + * revoke) and running live connection tests manage the site's third-party
852 + * credentials, so they require an administrator — `thinkrank_settings` is
853 + * delegatable to non-admin roles through the Role Manager. Mirrors the
854 + * pattern used by the AI-insights settings writes.
855 + *
856 + * @since 1.29.0
857 + * @return bool Permission status
901 858 */
902 - public function verify_ga4_tracking(WP_REST_Request $request): WP_REST_Response|WP_Error {
903 - try {
904 - // Empty is allowed and meaningful: verify_tracking() then reads the
905 - // homepage and discovers whichever GA4 ID is actually serving. The
906 - // old 400 made verification impossible on OAuth-connected sites
907 - // that never typed an ID in — precisely the reported case (#250).
908 - $measurement_id = (string) ( $request->get_param('measurement_id') ?? '' );
909 -
910 - // Load tracking manager
911 - if (!class_exists('ThinkRank\\Frontend\\Google_Analytics_Tracking_Manager')) {
912 - require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/class-google-analytics-tracking-manager.php';
913 - }
914 -
915 - $tracking_manager = new \ThinkRank\Frontend\Google_Analytics_Tracking_Manager();
916 - $verification_result = $tracking_manager->verify_tracking($measurement_id);
917 -
918 - return new WP_REST_Response([
919 - 'success' => true,
920 - 'data' => $verification_result,
921 - 'message' => 'Tracking verification completed'
922 - ], 200);
923 - } catch (\Exception $e) {
924 - return new WP_Error(
925 - 'verification_failed',
926 - 'Tracking verification failed: ' . $e->getMessage(),
927 - ['status' => 500]
928 - );
929 - }
859 + public function check_credential_permissions(): bool {
860 + return current_user_can('manage_options');
930 861 }
931 862
932 863 /**
933 - * Detect GA4 conflicts
934 - * Following ThinkRank API response patterns
935 - *
936 - * @since 1.0.0
937 - * @param WP_REST_Request $request Request object
938 - * @return WP_REST_Response|WP_Error Response object
939 - */
940 - public function detect_ga4_conflicts(WP_REST_Request $request): WP_REST_Response|WP_Error {
941 - try {
942 - // Load tracking manager
943 - if (!class_exists('ThinkRank\\Frontend\\Google_Analytics_Tracking_Manager')) {
944 - require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/class-google-analytics-tracking-manager.php';
945 - }
946 -
947 - $tracking_manager = new \ThinkRank\Frontend\Google_Analytics_Tracking_Manager();
948 - $conflicts = $tracking_manager->detect_existing_tracking();
949 -
950 - return new WP_REST_Response([
951 - 'success' => true,
952 - 'data' => [
953 - 'conflicts' => $conflicts,
954 - 'has_conflicts' => !empty($conflicts)
955 - ],
956 - 'message' => 'Conflict detection completed'
957 - ], 200);
958 - } catch (\Exception $e) {
959 - return new WP_Error(
960 - 'conflict_detection_failed',
961 - 'Conflict detection failed: ' . $e->getMessage(),
962 - ['status' => 500]
963 - );
964 - }
965 - }
966 - /**
967 864 * Fingerprint the currently connected Google account.
968 865 *
969 866 * Prefers the refresh token: it is issued once per authorization grant and
970 867 * survives every access-token rotation, so the cache stays warm for a whole
@@ -1001,8 +898,10 @@
1001 898 *
1002 899 * @since 1.0.0
1003 900 * @param WP_REST_Request $request Request object
1004 901 * @return WP_REST_Response Response object
902 + *
903 + * @throws \Exception On failure.
1005 904 */
1006 905 public function get_search_console_sites(WP_REST_Request $request): WP_REST_Response {
1007 906 try {
1008 907 // Ensure Analytics_Manager is loaded for proactive token refresh