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/seo/class-site-identity-manager.php +109 -11 2.14.0 → 2.14.2 View file →
@@ -334,8 +334,20 @@
334 334 * @var string
335 335 */
336 336 public const ROBOTS_RESYNC_OPTION = 'thinkrank_robots_txt_resync_pending';
337 337
338 + /**
339 + * Flag set when a plugin change may have altered the sitemap index.
340 + *
341 + * Separate from ROBOTS_RESYNC_OPTION because the two files exist
342 + * independently: that flag is only set when a physical robots.txt exists,
343 + * and the sitemap index needs rebuilding whether or not it does.
344 + *
345 + * @since 2.15.0
346 + * @var string
347 + */
348 + public const SITEMAP_RESYNC_OPTION = 'thinkrank_sitemap_contributors_changed';
349 +
338 350 public function __construct() {
339 351 parent::__construct('site_identity');
340 352
341 353 if (!self::$icon_sizes_listener_registered) {
@@ -357,8 +369,17 @@
357 369 // HTML to anything that followed it.
358 370 add_action('activated_plugin', [self::class, 'flag_robots_txt_resync']);
359 371 add_action('deactivated_plugin', [self::class, 'flag_robots_txt_resync']);
360 372 add_action('init', [self::class, 'maybe_resync_robots_txt'], 99);
373 +
374 + // The sitemap index is a second static file listing the same
375 + // contributors, with its own rebuild path. #835 / #859 resynced
376 + // robots.txt only, so after Pro was deactivated the index kept
377 + // advertising news-sitemap.xml, which then served the home page
378 + // as HTML (#920).
379 + add_action('activated_plugin', [self::class, 'flag_sitemap_resync']);
380 + add_action('deactivated_plugin', [self::class, 'flag_sitemap_resync']);
381 + add_action('init', [self::class, 'maybe_resync_sitemap'], 99);
361 382 }
362 383 }
363 384
364 385 /**
@@ -408,8 +429,57 @@
408 429 (new self())->sync_robots_txt_file();
409 430 }
410 431
411 432 /**
433 + * Note that the set of sitemap contributors may have changed.
434 + *
435 + * Unconditional, unlike flag_robots_txt_resync(): the sitemap files exist
436 + * whether or not robots.txt does. The rebuild waits for the next request
437 + * for the same reason as the robots.txt one, since `deactivated_plugin`
438 + * still runs with the outgoing plugin's `thinkrank_additional_sitemaps`
439 + * callback attached.
440 + *
441 + * @since 2.15.0
442 + * @return void
443 + */
444 + public static function flag_sitemap_resync(): void {
445 + update_option(self::SITEMAP_RESYNC_OPTION, 1, false);
446 + }
447 +
448 + /**
449 + * Queue a sitemap rebuild once, on the request after a contributor change.
450 + *
451 + * Goes through schedule_regeneration(), the debounced and lock-protected
452 + * path a sitemap settings save uses, so a burst of plugin changes (a bulk
453 + * deactivate, say) still produces one rebuild. That path also drops the
454 + * cached dynamic documents, so sites serving the sitemap from PHP drop the
455 + * entry as well.
456 + *
457 + * @since 2.15.0
458 + * @return void
459 + */
460 + public static function maybe_resync_sitemap(): void {
461 + if (!get_option(self::SITEMAP_RESYNC_OPTION)) {
462 + return;
463 + }
464 +
465 + // Cleared first, so a rebuild that fatals cannot be retried on every
466 + // request for the rest of the site's life.
467 + delete_option(self::SITEMAP_RESYNC_OPTION);
468 +
469 + $generator = new Sitemap_Generator(false);
470 + $settings = $generator->get_settings('site');
471 +
472 + // A disabled sitemap has no files to correct. Enabling it later builds
473 + // from the contributors present at that time.
474 + if (empty($settings['enabled'])) {
475 + return;
476 + }
477 +
478 + $generator->schedule_regeneration();
479 + }
480 +
481 + /**
412 482 * Save settings, then refresh what a new canonical scheme invalidates.
413 483 *
414 484 * The static sitemap files are written with the scheme in force when they
415 485 * were built, and nothing else rebuilds them until a post or term changes.
@@ -1566,9 +1636,9 @@
1566 1636 $optimization['suggestions'][] = 'Add a site logo for better branding and professional appearance';
1567 1637 $optimization['score'] -= 20;
1568 1638 } else {
1569 1639 // Validate logo URL and dimensions
1570 - if (!filter_var($logo_url, FILTER_VALIDATE_URL)) {
1640 + if (!\ThinkRank\Core\Url_Validator::is_http_url($logo_url)) {
1571 1641 $optimization['warnings'][] = 'Logo URL format is invalid';
1572 1642 $optimization['score'] -= 15;
1573 1643 }
1574 1644 }
@@ -1587,9 +1657,9 @@
1587 1657 $optimization['score'] -= 10;
1588 1658 }
1589 1659
1590 1660 // Additional logo analysis for local images
1591 - if (!empty($logo_url) && filter_var($logo_url, FILTER_VALIDATE_URL)) {
1661 + if (!empty($logo_url) && \ThinkRank\Core\Url_Validator::is_http_url($logo_url)) {
1592 1662 $attachment_id = Attachment_Lookup::id_from_url($logo_url);
1593 1663 if ($attachment_id) {
1594 1664 $image_meta = wp_get_attachment_metadata($attachment_id);
1595 1665 // The configured file's own size — a logo picked at a generated
@@ -2075,12 +2145,37 @@
2075 2145 $validation['suggestions'][] = 'Consider making site description longer (120-160 characters)';
2076 2146 }
2077 2147 }
2078 2148
2079 - // Validate logo URL
2080 - if (isset($settings['logo_url']) && !empty($settings['logo_url'])) {
2081 - if (!filter_var($settings['logo_url'], FILTER_VALIDATE_URL)) {
2082 - $validation['errors'][] = 'Logo URL must be a valid URL';
2149 + // Image and link URLs. These are written into src and href
2150 + // attributes (the schema logo, the admin previews, the hero section),
2151 + // so only web URLs are accepted. is_valid() takes any scheme, and
2152 + // "javascript://%0Aalert(1)" passed it and was stored as
2153 + // "javascript://alert(1)" once sanitize_text_field() dropped the %0A.
2154 + // The logo and default social image must be absolute: they are
2155 + // published in schema and Open Graph, which require it. The others
2156 + // may also be a path on this site.
2157 + $url_fields = [
2158 + 'logo_url' => ['Logo URL', false],
2159 + 'default_social_image' => ['Default social image URL', false],
2160 + 'favicon_url' => ['Favicon URL', true],
2161 + 'apple_touch_icon_url' => ['Apple touch icon URL', true],
2162 + 'hero_background_image' => ['Hero background image URL', true],
2163 + 'hero_cta_url' => ['Call-to-action URL', true],
2164 + ];
2165 + foreach ($url_fields as $key => [$label, $allow_path]) {
2166 + if (!isset($settings[$key]) || '' === $settings[$key] || null === $settings[$key]) {
2167 + continue;
2168 + }
2169 +
2170 + $ok = $allow_path
2171 + ? \ThinkRank\Core\Url_Validator::is_http_url_or_path($settings[$key])
2172 + : \ThinkRank\Core\Url_Validator::is_http_url($settings[$key]);
2173 +
2174 + if (!$ok) {
2175 + $validation['errors'][] = $allow_path
2176 + ? sprintf('%s must be an http or https URL, or a path starting with /', $label)
2177 + : sprintf('%s must be an http or https URL', $label);
2083 2178 $validation['valid'] = false;
2084 2179 }
2085 2180 }
2086 2181
@@ -2518,9 +2613,9 @@
2518 2613 }
2519 2614
2520 2615 // CTA URL validation
2521 2616 if (!empty($settings['hero_cta_url'])) {
2522 - if (filter_var($settings['hero_cta_url'], FILTER_VALIDATE_URL) || strpos($settings['hero_cta_url'], '/') === 0) {
2617 + if (\ThinkRank\Core\Url_Validator::is_http_url_or_path($settings['hero_cta_url'])) {
2523 2618 $field_details[] = [
2524 2619 'field' => 'hero_cta_url',
2525 2620 'label' => 'Call-to-action URL is properly configured.',
2526 2621 'status' => 'valid',
@@ -2561,9 +2656,9 @@
2561 2656 }
2562 2657
2563 2658 // Site Logo validation (from Site Assets section)
2564 2659 if (!empty($settings['logo_url'])) {
2565 - if (filter_var($settings['logo_url'], FILTER_VALIDATE_URL)) {
2660 + if (\ThinkRank\Core\Url_Validator::is_http_url($settings['logo_url'])) {
2566 2661 $field_details[] = [
2567 2662 'field' => 'logo_url',
2568 2663 'label' => 'Site logo is properly configured.',
2569 2664 'status' => 'valid',
@@ -4032,9 +4127,12 @@
4032 4127 $validation['warnings'][] = "Path '{$value}' should start with '/'";
4033 4128 }
4034 4129 break;
4035 4130 case 'sitemap':
4036 - if (!filter_var($value, FILTER_VALIDATE_URL)) {
4131 + // Url_Validator, not the raw PHP filter: on a site with an
4132 + // internationalised domain the site's own sitemap URL is
4133 + // non-ASCII and the raw filter refused it.
4134 + if (!\ThinkRank\Core\Url_Validator::is_valid($value)) {
4037 4135 $validation['errors'][] = "Invalid sitemap URL: {$value}";
4038 4136 $validation['valid'] = false;
4039 4137 }
4040 4138 break;
@@ -4433,10 +4531,10 @@
4433 4531 return $optimization;
4434 4532 }
4435 4533
4436 4534 // Validate URL
4437 - if (!filter_var($value, FILTER_VALIDATE_URL)) {
4438 - $optimization['validation']['errors'][] = "{$element} must be a valid URL";
4535 + if (!\ThinkRank\Core\Url_Validator::is_http_url($value)) {
4536 + $optimization['validation']['errors'][] = "{$element} must be an http or https URL";
4439 4537 $optimization['validation']['valid'] = false;
4440 4538 return $optimization;
4441 4539 }
4442 4540