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-sitemap-endpoint.php +122 -6 2.2.0 → 2.14.2 View file →
@@ -264,8 +264,14 @@
264 264 $timestamp = gmdate('c');
265 265 $settings = $this->sitemap_generator->get_settings('site');
266 266 $settings['last_generated'] = $timestamp;
267 267 $this->sitemap_generator->save_settings('site', null, $settings);
268 +
269 + // This generation wrote the same files the outstanding automatic rebuild
270 + // was queued to write, so clear its marker (and any recorded failure)
271 + // instead of leaving a request-time takeover to repeat the work.
272 + $this->sitemap_generator->mark_regeneration_complete();
273 +
268 274 return $timestamp;
269 275 }
270 276
271 277 /**
@@ -288,8 +294,24 @@
288 294 $this->sitemap_generator->save_settings('site', null, $settings);
289 295 }
290 296
291 297 /**
298 + * Stored sitemap settings with this request's options laid over them.
299 + *
300 + * The generate payload is partial — the admin screen posts the sitemap
301 + * shape, not the whole settings record — so reading `delivery_mode` straight
302 + * off it would resolve to `auto` on every ordinary request and defeat an
303 + * explicit choice. Merging keeps a mode sent in the payload authoritative
304 + * while falling back to what is saved.
305 + *
306 + * @param array $options Request options.
307 + * @return array Effective settings for this generation.
308 + */
309 + private function effective_settings(array $options): array {
310 + return array_merge($this->sitemap_generator->get_settings('site'), $options);
311 + }
312 +
313 + /**
292 314 * Persist a manual-generation auto-promotion into the stored settings.
293 315 *
294 316 * maybe_promote_to_index() may flip use_sitemap_index on and synthesize the
295 317 * segmented sitemap_urls for the current generation. On the automatic path
@@ -361,8 +383,17 @@
361 383 }
362 384
363 385 $sitemap_url = $this->sitemap_generator->get_primary_sitemap_url($settings);
364 386
387 + // Dynamic delivery publishes no file, so there is nothing to ensure and
388 + // nothing to look for on disk. Dropping the rendered documents is what
389 + // makes the saved settings take effect on the next request (#752).
390 + if ('dynamic' === $this->sitemap_generator->resolve_delivery_mode($settings)) {
391 + $this->sitemap_generator->flush_dynamic_cache();
392 +
393 + return $sitemap_url;
394 + }
395 +
365 396 if ($this->sitemap_generator->primary_sitemap_file_exists($settings)) {
366 397 return $sitemap_url;
367 398 }
368 399
@@ -431,8 +462,59 @@
431 462 // generate_and_save() already does this on the automatic path; the
432 463 // manual generate route must match it.
433 464 $this->persist_promoted_mode($options);
434 465
466 + // Dynamic delivery answers the sitemap URLs from PHP, so there is
467 + // nothing to write. Every other sitemap write path already returns
468 + // early here (class-sitemap-generator.php:2189 and :2313); this one
469 + // did not, which broke the feature from both directions: on a
470 + // read-only root — the case dynamic delivery exists for — the button
471 + // reported 500 "Failed to save sitemap: sitemap.xml" while the URL
472 + // was serving correctly, and on a writable root with dynamic chosen
473 + // explicitly it wrote files the web server then served in place of
474 + // the dynamic route.
475 + //
476 + // Regenerating here means dropping the rendered documents so the
477 + // next request rebuilds them, and clearing any file left behind by
478 + // an earlier static generation for the same reason.
479 + if ('dynamic' === $this->sitemap_generator->resolve_delivery_mode($this->effective_settings($options))) {
480 + $this->sitemap_generator->flush_dynamic_cache();
481 +
482 + $removal = $this->sitemap_generator->delete_published_sitemaps();
483 + $stuck = is_array($removal['failed'] ?? null) ? $removal['failed'] : [];
484 +
485 + // A stale file shadows the dynamic route, so this is a real
486 + // failure rather than a tidy-up that did not matter. Worded by
487 + // the generator so this and its own rebuild paths cannot
488 + // describe the same stuck files differently (#764).
489 + if (!empty($stuck)) {
490 + return new WP_Error(
491 + 'sitemap_stale_files',
492 + $this->sitemap_generator->stuck_files_message($stuck),
493 + ['status' => 500]
494 + );
495 + }
496 +
497 + // last_generated deliberately stays untouched: it means "these
498 + // files are on disk", and primary_sitemap_file_exists() callers
499 + // rely on that. clear_generation_record() drops a value left
500 + // over from a previous static generation, so the admin stops
501 + // linking to files that no longer exist.
502 + $this->clear_generation_record();
503 + $this->sitemap_generator->mark_regeneration_complete();
504 +
505 + return new WP_REST_Response([
506 + 'success' => true,
507 + 'data' => [
508 + 'delivery_mode' => 'dynamic',
509 + 'sitemap_url' => $this->sitemap_generator->get_primary_sitemap_url(),
510 + 'generated_at' => gmdate('c'),
511 + 'last_generated' => '',
512 + ],
513 + 'message' => __('Sitemap refreshed. WordPress serves it directly, so no files were written.', 'thinkrank'),
514 + ]);
515 + }
516 +
435 517 // Check if an index (multiple sitemaps) is configured
436 518 if (!empty($options['use_sitemap_index']) || (!empty($options['sitemap_urls']) && count($options['sitemap_urls']) > 1)) {
437 519 // Generate multiple sitemaps
438 520 $results = $this->sitemap_generator->generate_multiple_sitemaps($options);
@@ -466,9 +548,21 @@
466 548 $filename = 'sitemap.xml';
467 549 if (!empty($options['sitemap_urls'][0]['url'])) {
468 550 $filename = basename(wp_parse_url($options['sitemap_urls'][0]['url'], PHP_URL_PATH));
469 551 }
470 - $this->save_sitemap_file($sitemap_xml, $filename);
552 + // A failed write has to surface here the way the index
553 + // branch surfaces one. Discarding it let record_generation()
554 + // advance last_generated and clear the pending marker and
555 + // the recorded failure, so an unwritable site root — the
556 + // exact case this endpoint reports health for — came back
557 + // as a healthy "Generated successfully".
558 + if (!$this->save_sitemap_file($sitemap_xml, $filename)) {
559 + return new WP_Error(
560 + 'sitemap_generation_failed',
561 + 'Failed to save sitemap: ' . $filename,
562 + ['status' => 500]
563 + );
564 + }
471 565
472 566 // Regenerate the standalone local business sitemap on the
473 567 // single-sitemap path too (parity with Rank Math).
474 568 $this->sitemap_generator->regenerate_local_sitemap($options);
@@ -515,10 +609,11 @@
515 609 public function validate_sitemap(WP_REST_Request $request) {
516 610 try {
517 611 $sitemap_url = $request->get_param('sitemap_url') ?? home_url('/sitemap.xml');
518 612
519 - // Validate sitemap URL
520 - if (!filter_var($sitemap_url, FILTER_VALIDATE_URL)) {
613 + // Validate sitemap URL. Url_Validator accepts an internationalised
614 + // domain or a non-ASCII path (home_url() on an IDN site is one).
615 + if (!\ThinkRank\Core\Url_Validator::is_valid($sitemap_url)) {
521 616 return new WP_Error(
522 617 'invalid_url',
523 618 'Invalid sitemap URL provided',
524 619 ['status' => 400]
@@ -524,8 +619,14 @@
524 619 ['status' => 400]
525 620 );
526 621 }
527 622
623 + // Everything from here on works on the ASCII form (punycode host,
624 + // percent-encoded path). wp_http_validate_url() resolves the host
625 + // with gethostbyname(), which cannot resolve a Unicode name, and
626 + // the SSRF check must see exactly the URL that is fetched.
627 + $sitemap_url = \ThinkRank\Core\Url_Validator::to_ascii((string) $sitemap_url);
628 +
528 629 // Block SSRF: this endpoint fetches the URL server-side, so reject
529 630 // loopback/link-local/private hosts and non-http(s) schemes via
530 631 // WordPress's own validator (same guard used in class-schema-endpoint).
531 632 if (!wp_http_validate_url($sitemap_url)) {
@@ -912,9 +1013,24 @@
912 1013 'success' => true,
913 1014 'data' => [
914 1015 'settings' => $settings,
915 1016 'context_type' => $context_type,
916 - 'context_id' => $context_id
1017 + 'context_id' => $context_id,
1018 + // Kept out of `settings` on purpose: this is generator state,
1019 + // not something the settings POST round-trips.
1020 + 'health' => $context_type === 'site'
1021 + ? $this->sitemap_generator->get_regeneration_health()
1022 + : null,
1023 + // `delivery_mode` in `settings` may still be 'auto', which
1024 + // only the server can resolve (it depends on whether the web
1025 + // root is writable). The admin screen needs the answer, not
1026 + // the question: a dynamic site publishes no file, so gating
1027 + // its sitemap links on `last_generated` — which dynamic
1028 + // delivery deliberately never sets — left every link
1029 + // permanently disabled.
1030 + 'resolved_delivery_mode' => $context_type === 'site'
1031 + ? $this->sitemap_generator->resolve_delivery_mode($settings)
1032 + : null
917 1033 ],
918 1034 'message' => 'Sitemap settings retrieved successfully'
919 1035 ], 200);
920 1036
@@ -1304,9 +1420,9 @@
1304 1420 * @since 1.0.0
1305 1421 * @return bool True if lock acquired
1306 1422 */
1307 1423 private function acquire_generation_lock(): bool {
1308 - $lock_key = 'thinkrank_sitemap_generation_lock';
1424 + $lock_key = Sitemap_Generator::GENERATION_LOCK_TRANSIENT;
1309 1425
1310 1426 if (get_transient($lock_key)) {
1311 1427 return false; // Generation already in progress
1312 1428 }
@@ -1321,7 +1437,7 @@
1321 1437 * @since 1.0.0
1322 1438 * @return void
1323 1439 */
1324 1440 private function release_generation_lock(): void {
1325 - delete_transient('thinkrank_sitemap_generation_lock');
1441 + delete_transient(Sitemap_Generator::GENERATION_LOCK_TRANSIENT);
1326 1442 }
1327 1443 }