PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.2
2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.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 All 57 releases
← All changes | includes/api/class-seo-analytics-endpoint.php +71 -220 1.30.0 → 2.14.2 View file →
@@ -145,21 +145,8 @@
145 145 ]
146 146 ]
147 147 );
148 148
149 - // Get indexing status
150 - register_rest_route(
151 - $this->namespace,
152 - '/' . $this->rest_base . '/indexing-status',
153 - [
154 - [
155 - 'methods' => 'GET',
156 - 'callback' => [$this, 'get_indexing_status'],
157 - 'permission_callback' => [$this, 'check_permissions'],
158 - ]
159 - ]
160 - );
161 -
162 149 // Refresh cached data
163 150 register_rest_route(
164 151 $this->namespace,
165 152 '/' . $this->rest_base . '/refresh',
@@ -184,54 +171,8 @@
184 171 ]
185 172 ]
186 173 );
187 174
188 - // ========================================
189 - // SEO Intelligence Enhancement Endpoints
190 - // ========================================
191 -
192 - // Get intelligent dashboard data with trends and insights
193 - register_rest_route(
194 - $this->namespace,
195 - '/' . $this->rest_base . '/intelligent-dashboard',
196 - [
197 - [
198 - 'methods' => 'GET',
199 - 'callback' => [$this, 'get_intelligent_dashboard'],
200 - 'permission_callback' => [$this, 'check_data_permissions'],
201 - 'args' => $this->get_dashboard_args()
202 - ]
203 - ]
204 - );
205 -
206 - // Get intelligent SEO opportunities with prioritization
207 - register_rest_route(
208 - $this->namespace,
209 - '/' . $this->rest_base . '/intelligent-opportunities',
210 - [
211 - [
212 - 'methods' => 'GET',
213 - 'callback' => [$this, 'get_intelligent_opportunities'],
214 - 'permission_callback' => [$this, 'check_data_permissions'],
215 - 'args' => $this->get_opportunities_args()
216 - ]
217 - ]
218 - );
219 -
220 - // Get SEO insights
221 - register_rest_route(
222 - $this->namespace,
223 - '/' . $this->rest_base . '/insights',
224 - [
225 - [
226 - 'methods' => 'GET',
227 - 'callback' => [$this, 'get_seo_insights'],
228 - 'permission_callback' => [$this, 'check_data_permissions'],
229 - 'args' => $this->get_dashboard_args()
230 - ]
231 - ]
232 - );
233 -
234 175 // Get Search Console totals for custom date range
235 176 register_rest_route(
236 177 $this->namespace,
237 178 '/' . $this->rest_base . '/search-totals',
@@ -336,9 +277,9 @@
336 277 *
337 278 * @param WP_REST_Request $request Request object
338 279 * @return WP_REST_Response|WP_Error Response object
339 280 */
340 - public function test_connections(WP_REST_Request $request): WP_REST_Response|WP_Error {
281 + public function test_connections(WP_REST_Request $request) {
341 282 try {
342 283 $connection_results = $this->analytics_manager->test_connections();
343 284
344 285 return new WP_REST_Response([
@@ -360,9 +301,9 @@
360 301 *
361 302 * @param WP_REST_Request $request Request object
362 303 * @return WP_REST_Response|WP_Error Response object
363 304 */
364 - public function get_dashboard_data(WP_REST_Request $request): WP_REST_Response|WP_Error {
305 + public function get_dashboard_data(WP_REST_Request $request) {
365 306 try {
366 307 $date_range = $request->get_param('date_range');
367 308 $dashboard_data = $this->analytics_manager->get_dashboard_data($date_range);
368 309
@@ -385,9 +326,9 @@
385 326 *
386 327 * @param WP_REST_Request $request Request object
387 328 * @return WP_REST_Response|WP_Error Response object
388 329 */
389 - public function get_seo_opportunities(WP_REST_Request $request): WP_REST_Response|WP_Error {
330 + public function get_seo_opportunities(WP_REST_Request $request) {
390 331 try {
391 332 $date_range = $request->get_param('date_range');
392 333 $opportunities = $this->analytics_manager->get_seo_opportunities($date_range);
393 334
@@ -410,9 +351,9 @@
410 351 *
411 352 * @param WP_REST_Request $request Request object
412 353 * @return WP_REST_Response|WP_Error Response object
413 354 */
414 - public function setup_search_console(WP_REST_Request $request): WP_REST_Response|WP_Error {
355 + public function setup_search_console(WP_REST_Request $request) {
415 356 try {
416 357 $site_url = $request->get_param('site_url');
417 358 $setup_result = $this->analytics_manager->setup_search_console_verification($site_url);
418 359
@@ -430,38 +371,14 @@
430 371 }
431 372 }
432 373
433 374 /**
434 - * Get indexing status
435 - *
436 - * @param WP_REST_Request $request Request object
437 - * @return WP_REST_Response|WP_Error Response object
438 - */
439 - public function get_indexing_status(WP_REST_Request $request): WP_REST_Response|WP_Error {
440 - try {
441 - $indexing_status = $this->analytics_manager->get_indexing_status();
442 -
443 - return new WP_REST_Response([
444 - 'success' => true,
445 - 'data' => $indexing_status,
446 - 'message' => 'Indexing status retrieved successfully'
447 - ], 200);
448 - } catch (\Exception $e) {
449 - return new WP_Error(
450 - 'indexing_status_failed',
451 - 'Failed to retrieve indexing status: ' . $e->getMessage(),
452 - ['status' => 500]
453 - );
454 - }
455 - }
456 -
457 - /**
458 375 * Refresh cached analytics data
459 376 *
460 377 * @param WP_REST_Request $request Request object
461 378 * @return WP_REST_Response|WP_Error Response object
462 379 */
463 - public function refresh_data(WP_REST_Request $request): WP_REST_Response|WP_Error {
380 + public function refresh_data(WP_REST_Request $request) {
464 381 try {
465 382 $refresh_result = $this->analytics_manager->refresh_data();
466 383
467 384 // Bust the cached Search Console passthrough responses (search-totals,
@@ -487,9 +404,9 @@
487 404 *
488 405 * @param WP_REST_Request $request Request object
489 406 * @return WP_REST_Response|WP_Error Response object
490 407 */
491 - public function get_client_status(WP_REST_Request $request): WP_REST_Response|WP_Error {
408 + public function get_client_status(WP_REST_Request $request) {
492 409 try {
493 410 $client_status = $this->analytics_manager->get_client_status();
494 411
495 412 return new WP_REST_Response([
@@ -550,11 +467,11 @@
550 467 return [
551 468 'site_url' => [
552 469 'required' => true,
553 470 'type' => 'string',
554 - 'sanitize_callback' => 'esc_url_raw',
471 + 'sanitize_callback' => [$this, 'sanitize_site_url'],
555 472 'validate_callback' => [$this, 'validate_site_url'],
556 - 'description' => 'Site URL to verify in Search Console'
473 + 'description' => 'Search Console property: a URL-prefix property (https://example.com/) or a domain property (sc-domain:example.com)'
557 474 ]
558 475 ];
559 476 }
560 477
@@ -564,9 +481,21 @@
564 481 *
565 482 * @param string $site_url Site URL to validate
566 483 * @return bool|WP_Error Validation result
567 484 */
568 - public function validate_site_url(string $site_url): bool|WP_Error {
485 + public function validate_site_url($site_url) {
486 + // Not a `string` type hint: this is a validate_callback, so it runs on
487 + // the raw pre-sanitize parameter. `?site_url[]=x` handed it an array
488 + // and PHP raised an uncaught TypeError — a 500 where the API owes the
489 + // caller a 400 (#394).
490 + if (!is_string($site_url)) {
491 + return new WP_Error(
492 + 'invalid_site_url',
493 + 'Site URL must be a string',
494 + ['status' => 400]
495 + );
496 + }
497 +
569 498 if (empty($site_url)) {
570 499 return new WP_Error(
571 500 'invalid_site_url',
572 501 'Site URL is required',
@@ -573,9 +502,26 @@
573 502 ['status' => 400]
574 503 );
575 504 }
576 505
577 - if (!filter_var($site_url, FILTER_VALIDATE_URL)) {
506 + // A domain property (sc-domain:example.com) is not a URL, and
507 + // is_valid() refused every one, though the rest of the Search Console
508 + // code reads them. Its host must be a real hostname; an IDN is fine.
509 + if (0 === stripos($site_url, 'sc-domain:')) {
510 + if (null === \ThinkRank\Core\Url_Validator::search_console_domain_property($site_url)) {
511 + return new WP_Error(
512 + 'invalid_site_url',
513 + 'A domain property must be sc-domain: followed by a domain name, such as sc-domain:example.com',
514 + ['status' => 400]
515 + );
516 + }
517 +
518 + return true;
519 + }
520 +
521 + // Url_Validator accepts an internationalised domain, which is a valid
522 + // site URL; the raw PHP filter refuses every non-ASCII byte.
523 + if (!\ThinkRank\Core\Url_Validator::is_valid($site_url)) {
578 524 return new WP_Error(
579 525 'invalid_site_url',
580 526 'Site URL must be a valid URL',
581 527 ['status' => 400]
@@ -585,14 +531,40 @@
585 531 return true;
586 532 }
587 533
588 534 /**
535 + * Sanitize the Search Console property sent to setup.
536 + *
537 + * esc_url_raw() for a URL-prefix property, as before. A domain property
538 + * is not a URL: esc_url_raw() drops "sc-domain:" as an unknown protocol
539 + * and returns "", so it is normalised to its punycode form instead, which
540 + * is what verify_site() matches against the account's property list.
541 + *
542 + * @since 2.14.2
543 + *
544 + * @param mixed $site_url Raw parameter, already through validate_site_url().
545 + * @return string
546 + */
547 + public function sanitize_site_url($site_url): string {
548 + if (!is_string($site_url)) {
549 + return '';
550 + }
551 +
552 + $domain = \ThinkRank\Core\Url_Validator::search_console_domain_property($site_url);
553 + if (null !== $domain) {
554 + return $domain;
555 + }
556 +
557 + return esc_url_raw($site_url);
558 + }
559 +
560 + /**
589 561 * Get Search Console totals for a custom date range
590 562 *
591 563 * @param WP_REST_Request $request Request object
592 564 * @return WP_REST_Response|WP_Error Response object
593 565 */
594 - public function get_search_totals(WP_REST_Request $request): WP_REST_Response|WP_Error {
566 + public function get_search_totals(WP_REST_Request $request) {
595 567 try {
596 568 $start_date = $request->get_param('start_date');
597 569 $end_date = $request->get_param('end_date');
598 570
@@ -643,9 +615,9 @@
643 615 *
644 616 * @param WP_REST_Request $request Request object
645 617 * @return WP_REST_Response|WP_Error Response object
646 618 */
647 - public function get_search_daily(WP_REST_Request $request): WP_REST_Response|WP_Error {
619 + public function get_search_daily(WP_REST_Request $request) {
648 620 try {
649 621 $date_range = $request->get_param('date_range') ?: '30d';
650 622 $days = (int) preg_replace('/[^0-9]/', '', $date_range);
651 623 if ($days <= 0 || $days > 90) {
@@ -737,9 +709,9 @@
737 709 *
738 710 * @param WP_REST_Request $request Request object
739 711 * @return WP_REST_Response|WP_Error
740 712 */
741 - public function get_branded(WP_REST_Request $request): WP_REST_Response|WP_Error {
713 + public function get_branded(WP_REST_Request $request) {
742 714 try {
743 715 $date_range = $request->get_param('date_range') ?: '30d';
744 716 $brand_name = $request->get_param('brand_name') ?: '';
745 717
@@ -777,9 +749,9 @@
777 749 *
778 750 * @param WP_REST_Request $request Request object
779 751 * @return WP_REST_Response|WP_Error Response object
780 752 */
781 - public function get_countries(WP_REST_Request $request): WP_REST_Response|WP_Error {
753 + public function get_countries(WP_REST_Request $request) {
782 754 try {
783 755 $date_range = $request->get_param('date_range') ?: '30d';
784 756
785 757 $context = $this->resolve_search_console();
@@ -818,9 +790,9 @@
818 790 *
819 791 * @since 1.0.0
820 792 * @return array{0: \ThinkRank\Integrations\Google_Search_Console_Client, 1: string}|WP_Error
821 793 */
822 - private function resolve_search_console(): array|WP_Error {
794 + private function resolve_search_console() {
823 795 $client = $this->analytics_manager->get_search_console_client();
824 796
825 797 if (!$client) {
826 798 return new WP_Error(
@@ -925,9 +897,9 @@
925 897 * gated so the feature can always be (re-)enabled.
926 898 *
927 899 * @return bool|WP_Error True when allowed, false or WP_Error otherwise
928 900 */
929 - public function check_data_permissions(): bool|WP_Error {
901 + public function check_data_permissions() {
930 902 if (!$this->check_permissions()) {
931 903 return false;
932 904 }
933 905
@@ -939,127 +911,6 @@
939 911 );
940 912 }
941 913
942 914 return true;
943 - }
944 -
945 - // ========================================
946 - // SEO Intelligence Enhancement Endpoints
947 - // ========================================
948 -
949 - /**
950 - * Get intelligent dashboard data with trends and insights
951 - *
952 - * @param WP_REST_Request $request Request object
953 - * @return WP_REST_Response|WP_Error Response object
954 - */
955 - public function get_intelligent_dashboard(WP_REST_Request $request): WP_REST_Response|WP_Error {
956 - try {
957 - $date_range = $request->get_param('date_range');
958 -
959 - // Cache the intelligence computation (trend analysis + generators are
960 - // expensive) so the AI-insights panel doesn't recompute every load.
961 - $response = $this->cached_response(
962 - 'intelligent_dashboard',
963 - function () use ($date_range) {
964 - $intelligent_data = $this->analytics_manager->get_intelligent_dashboard_data($date_range);
965 -
966 - return [
967 - 'success' => isset($intelligent_data['success']) ? $intelligent_data['success'] : false,
968 - 'data' => $intelligent_data['data'] ?? null,
969 - 'message' => $intelligent_data['message'] ?? 'Intelligent dashboard data retrieved',
970 - 'timestamp' => current_time('mysql'),
971 - ];
972 - },
973 - ['date_range' => $date_range]
974 - );
975 -
976 - // Always return 200 for successful API calls, even when no data available.
977 - return new WP_REST_Response($response, 200);
978 -
979 - } catch (\Exception $e) {
980 - return new WP_Error(
981 - 'intelligent_dashboard_error',
982 - 'Failed to retrieve intelligent dashboard data: ' . $e->getMessage(),
983 - ['status' => 500]
984 - );
985 - }
986 - }
987 -
988 - /**
989 - * Get intelligent SEO opportunities with prioritization
990 - *
991 - * @param WP_REST_Request $request Request object
992 - * @return WP_REST_Response|WP_Error Response object
993 - */
994 - public function get_intelligent_opportunities(WP_REST_Request $request): WP_REST_Response|WP_Error {
995 - try {
996 - $date_range = $request->get_param('date_range');
997 -
998 - // Cache the opportunity detection so it doesn't recompute every load.
999 - $response = $this->cached_response(
1000 - 'intelligent_opportunities',
1001 - function () use ($date_range) {
1002 - $intelligent_opportunities = $this->analytics_manager->get_intelligent_seo_opportunities($date_range);
1003 -
1004 - return [
1005 - 'success' => isset($intelligent_opportunities['success']) ? $intelligent_opportunities['success'] : false,
1006 - 'data' => $intelligent_opportunities['data'] ?? null,
1007 - 'message' => $intelligent_opportunities['message'] ?? 'Intelligent opportunities retrieved',
1008 - 'timestamp' => current_time('mysql'),
1009 - ];
1010 - },
1011 - ['date_range' => $date_range]
1012 - );
1013 -
1014 - // Always return 200 for successful API calls, even when no data available.
1015 - return new WP_REST_Response($response, 200);
1016 -
1017 - } catch (\Exception $e) {
1018 - return new WP_Error(
1019 - 'intelligent_opportunities_error',
1020 - 'Failed to retrieve intelligent opportunities: ' . $e->getMessage(),
1021 - ['status' => 500]
1022 - );
1023 - }
1024 - }
1025 -
1026 - /**
1027 - * Get SEO insights
1028 - *
1029 - * @param WP_REST_Request $request Request object
1030 - * @return WP_REST_Response|WP_Error Response object
1031 - */
1032 - public function get_seo_insights(WP_REST_Request $request): WP_REST_Response|WP_Error {
1033 - try {
1034 - $date_range = $request->get_param('date_range');
1035 -
1036 - // Endpoint-level cache for consistency with the other two intelligence
1037 - // calls (the manager also caches insights internally).
1038 - $response = $this->cached_response(
1039 - 'seo_insights',
1040 - function () use ($date_range) {
1041 - $insights = $this->analytics_manager->get_seo_insights($date_range);
1042 -
1043 - return [
1044 - 'success' => isset($insights['success']) ? $insights['success'] : false,
1045 - 'data' => $insights['data'] ?? null,
1046 - 'cached' => $insights['cached'] ?? false,
1047 - 'message' => $insights['message'] ?? 'SEO insights retrieved',
1048 - 'timestamp' => current_time('mysql'),
1049 - ];
1050 - },
1051 - ['date_range' => $date_range]
1052 - );
1053 -
1054 - // Always return 200 for successful API calls, even when no data available.
1055 - return new WP_REST_Response($response, 200);
1056 -
1057 - } catch (\Exception $e) {
1058 - return new WP_Error(
1059 - 'seo_insights_error',
1060 - 'Failed to retrieve SEO insights: ' . $e->getMessage(),
1061 - ['status' => 500]
1062 - );
1063 - }
1064 915 }
1065 916 }