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/ai/class-content-brief-generator.php +29 -4 2.14.0 → 2.14.2 View file →
@@ -959,8 +959,32 @@
959 959 }
960 960
961 961 return $normalized;
962 962 }
963 + /**
964 + * The URL to check and fetch for a competitor URL the user entered, or
965 + * null when it is not a valid URL.
966 + *
967 + * A competitor page on an internationalised domain, or with a Bengali or
968 + * Arabic slug, is a valid URL, so the syntax check is Url_Validator's.
969 + * What comes back is the ASCII form (punycode host, percent-encoded path),
970 + * and both the SSRF guard and the fetch use it: the guard cannot resolve
971 + * a Unicode host name, and it must check exactly the URL that is then
972 + * requested.
973 + *
974 + * @since 2.14.2
975 + *
976 + * @param string $url Trimmed URL as entered.
977 + * @return string|null ASCII URL, or null when invalid.
978 + */
979 + private function competitor_fetch_url(string $url): ?string {
980 + if ('' === $url || !\ThinkRank\Core\Url_Validator::is_valid($url)) {
981 + return null;
982 + }
983 +
984 + return \ThinkRank\Core\Url_Validator::to_ascii($url);
985 + }
986 +
963 987 private function analyze_competitor_urls(array $urls): string {
964 988 $analysis_results = [];
965 989 $failed_urls = [];
966 990
@@ -968,9 +992,10 @@
968 992 $urls = array_slice($urls, 0, 3);
969 993
970 994 foreach ($urls as $url) {
971 995 $url = trim($url);
972 - if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
996 + $fetch_url = $this->competitor_fetch_url($url);
997 + if (null === $fetch_url) {
973 998 $failed_urls[] = $url . " (invalid URL)";
974 999 continue;
975 1000 }
976 1001
@@ -976,14 +1001,14 @@
976 1001
977 1002 // SSRF guard: only fetch public http/https hosts. Blocks loopback,
978 1003 // link-local (cloud metadata), private and reserved ranges before any
979 1004 // request is made.
980 - if (!$this->is_safe_public_url($url)) {
1005 + if (!$this->is_safe_public_url($fetch_url)) {
981 1006 $failed_urls[] = $url . " (blocked: non-public host)";
982 1007 continue;
983 1008 }
984 1009
985 - $content_data = $this->scrape_competitor_content($url);
1010 + $content_data = $this->scrape_competitor_content($fetch_url);
986 1011 if ($content_data) {
987 1012 $analysis_results[] = $this->format_competitor_analysis($url, $content_data);
988 1013 } else {
989 1014 $failed_urls[] = $url . " (failed to scrape)";
@@ -1229,9 +1254,9 @@
1229 1254 // Add heading structure
1230 1255 if (!empty($content_data['headings'])) {
1231 1256 $analysis .= "\nCONTENT STRUCTURE:\n";
1232 1257 foreach ($content_data['headings'] as $level => $headings) {
1233 - $analysis .= "- " . strtoupper($level) . " ({count}): " . implode(', ', array_slice($headings, 0, 3));
1258 + $analysis .= "- " . strtoupper($level) . " (" . count($headings) . "): " . implode(', ', array_slice($headings, 0, 3));
1234 1259 if (count($headings) > 3) {
1235 1260 $analysis .= "... (+" . (count($headings) - 3) . " more)";
1236 1261 }
1237 1262 $analysis .= "\n";