PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.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 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 All 50 releases
← All changes | includes/frontend/class-seo-manager.php +114 -1 2.7.0 → 2.9.0 View file →
@@ -266,8 +266,16 @@
266 266 // LLMs_Txt_Manager for the static file) guarantees an explicit UTF-8
267 267 // charset. Priority 8 keeps it ahead of redirect_canonical().
268 268 add_action('template_redirect', [$this, 'maybe_serve_llms_txt'], 8);
269 269
270 + // Serve the sitemap from PHP on sites whose web root cannot be written.
271 + // ThinkRank publishes sitemaps as real files, so where that is possible
272 + // the web server answers first and this never runs; where it is not,
273 + // this is the only thing that answers at all, and without it the
274 + // feature was simply unavailable (#752). Same priority 8, and for the
275 + // same reason: ahead of redirect_canonical().
276 + add_action('template_redirect', [$this, 'maybe_serve_sitemap'], 8);
277 +
270 278 // Take WordPress core's own sitemap offline while ThinkRank's is active.
271 279 // Two sitemap indexes on one site is a crawl conflict: core keeps
272 280 // /wp-sitemap.xml served and injects its own "Sitemap:" line into
273 281 // robots.txt (WP_Sitemaps::add_robots, priority 0). Until now that line
@@ -3526,8 +3534,102 @@
3526 3534 * @since 1.32.0
3527 3535 *
3528 3536 * @return void
3529 3537 */
3538 + /**
3539 + * Serve a ThinkRank sitemap document for this request, when it is one.
3540 + *
3541 + * Only acts in dynamic delivery mode. In static mode a real file exists and
3542 + * the web server returns it without WordPress ever loading, so answering
3543 + * here as well would mean two sources for the same bytes.
3544 + *
3545 + * @since 2.9.0
3546 + *
3547 + * @return void
3548 + */
3549 + public function maybe_serve_sitemap(): void {
3550 + $filename = $this->requested_sitemap_filename();
3551 + if ('' === $filename) {
3552 + return;
3553 + }
3554 +
3555 + try {
3556 + // Read-only instance: passing false keeps it from registering a
3557 + // second copy of the auto-generation hooks.
3558 + $generator = new \ThinkRank\SEO\Sitemap_Generator(false);
3559 + $settings = $generator->get_settings('site');
3560 +
3561 + if (empty($settings['enabled'])) {
3562 + return;
3563 + }
3564 +
3565 + if ('dynamic' !== $generator->resolve_delivery_mode($settings)) {
3566 + return;
3567 + }
3568 +
3569 + if (!$generator->publishes_document_name($filename, $settings)) {
3570 + return;
3571 + }
3572 +
3573 + $xml = $generator->render_document($filename, $settings);
3574 + } catch (\Throwable $e) {
3575 + // A failed render must not replace the sitemap with a fatal. Leave
3576 + // the request alone so WordPress answers as it otherwise would.
3577 + return;
3578 + }
3579 +
3580 + if (!is_string($xml) || '' === trim($xml)) {
3581 + return;
3582 + }
3583 +
3584 + status_header(200);
3585 + header('Content-Type: application/xml; charset=UTF-8');
3586 + header('X-Robots-Tag: noindex, follow', true);
3587 +
3588 + // Built XML, escaped by the builders as they assemble it; escaping the
3589 + // document here would corrupt it.
3590 + echo $xml; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
3591 + exit;
3592 + }
3593 +
3594 + /**
3595 + * The sitemap file name this request is asking for, if it looks like one.
3596 + *
3597 + * Deliberately a cheap shape test. Whether the site actually publishes the
3598 + * name is settled by the caller against the generator, so that a request
3599 + * for someone else's sitemap is never answered here.
3600 + *
3601 + * @since 2.9.0
3602 + *
3603 + * @return string File name, or '' when this is not a sitemap request.
3604 + */
3605 + private function requested_sitemap_filename(): string {
3606 + if (empty($_SERVER['REQUEST_URI'])) {
3607 + return '';
3608 + }
3609 +
3610 + $path = wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_PATH);
3611 + if (!is_string($path) || '' === $path) {
3612 + return '';
3613 + }
3614 +
3615 + // Strip the install's home path so subdirectory installs match too.
3616 + $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH);
3617 + if ('' !== $home_path && '/' !== $home_path && 0 === strpos($path, $home_path)) {
3618 + $path = substr($path, strlen($home_path));
3619 + }
3620 +
3621 + $candidate = strtolower(trim($path, '/'));
3622 +
3623 + // One path segment ending in .xml. Anything nested is not a file we
3624 + // publish to the web root.
3625 + if ('' === $candidate || strpos($candidate, '/') !== false) {
3626 + return '';
3627 + }
3628 +
3629 + return substr($candidate, -4) === '.xml' ? $candidate : '';
3630 + }
3631 +
3530 3632 public function maybe_serve_llms_txt(): void {
3531 3633 if (!$this->is_llms_txt_request()) {
3532 3634 return;
3533 3635 }
@@ -3728,11 +3830,22 @@
3728 3830 // second copy of the save_post/term auto-generation hooks.
3729 3831 $generator = new \ThinkRank\SEO\Sitemap_Generator(false);
3730 3832 $settings = $generator->get_settings('site');
3731 3833
3834 + // "Can ThinkRank actually answer its sitemap URL right now?" In
3835 + // static mode that means the file is on disk; in dynamic mode
3836 + // maybe_serve_sitemap() answers it, so there is nothing to look
3837 + // for. Keeping the file test as the only answer would have left
3838 + // core's sitemap in place on every dynamic site, which is the
3839 + // crawl conflict this suppression exists to prevent (#752).
3840 + // The #346 behaviour is unchanged: a static site with nothing
3841 + // published still falls through to core rather than 404ing.
3842 + $can_serve = 'dynamic' === $generator->resolve_delivery_mode($settings)
3843 + || $generator->primary_sitemap_file_exists($settings);
3844 +
3732 3845 $this->thinkrank_sitemap_enabled = !empty($settings['enabled'])
3733 3846 && !$this->publishes_at_core_sitemap_url($settings)
3734 - && $generator->primary_sitemap_file_exists($settings);
3847 + && $can_serve;
3735 3848
3736 3849 if ($this->thinkrank_sitemap_enabled) {
3737 3850 $this->thinkrank_sitemap_url = $generator->get_primary_sitemap_url($settings);
3738 3851 }