PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.1.0 1.10.0 All 48 releases
← All changes | includes/api/class-sitemap-endpoint.php +84 -13 2.0.02.7.0 View file →
@@ -14,17 +14,29 @@
14 14 declare(strict_types=1);
15 15
16 16 namespace ThinkRank\API;
17 17
18 +// Prevent direct access
19 +if (!defined('ABSPATH')) {
20 + exit;
21 +}
22 +
18 23 use ThinkRank\SEO\Sitemap_Generator;
19 24 use ThinkRank\API\Traits\CSRF_Protection;
25 +use ThinkRank\API\Traits\Context_Authorization;
20 26 use WP_REST_Controller;
21 27 use WP_REST_Request;
22 28 use WP_REST_Response;
23 29 use WP_Error;
24 30
31 +// Prevent direct access
32 +if (!defined('ABSPATH')) {
33 + exit;
34 +}
35 +
25 36 // Load CSRF Protection trait
26 37 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-csrf-protection.php';
38 +require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-context-authorization.php';
27 39
28 40 /**
29 41 * Sitemap API Endpoints Class
30 42 *
@@ -35,8 +47,9 @@
35 47 * @since 1.0.0
36 48 */
37 49 class Sitemap_Endpoint extends WP_REST_Controller {
38 50 use CSRF_Protection;
51 + use Context_Authorization;
39 52
40 53 /**
41 54 * Sitemap Generator instance
42 55 *
@@ -164,9 +177,10 @@
164 177 [
165 178 [
166 179 'methods' => 'GET',
167 180 'callback' => [$this, 'get_sitemap_settings'],
168 - 'permission_callback' => [$this, 'check_read_permissions']
181 + 'permission_callback' => [$this, 'check_read_permissions'],
182 + 'args' => $this->get_context_route_args()
169 183 ],
170 184 [
171 185 'methods' => 'POST',
172 186 'callback' => [$this, 'update_sitemap_settings'],
@@ -250,8 +264,14 @@
250 264 $timestamp = gmdate('c');
251 265 $settings = $this->sitemap_generator->get_settings('site');
252 266 $settings['last_generated'] = $timestamp;
253 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 +
254 274 return $timestamp;
255 275 }
256 276
257 277 /**
@@ -452,9 +472,21 @@
452 472 $filename = 'sitemap.xml';
453 473 if (!empty($options['sitemap_urls'][0]['url'])) {
454 474 $filename = basename(wp_parse_url($options['sitemap_urls'][0]['url'], PHP_URL_PATH));
455 475 }
456 - $this->save_sitemap_file($sitemap_xml, $filename);
476 + // A failed write has to surface here the way the index
477 + // branch surfaces one. Discarding it let record_generation()
478 + // advance last_generated and clear the pending marker and
479 + // the recorded failure, so an unwritable site root — the
480 + // exact case this endpoint reports health for — came back
481 + // as a healthy "Generated successfully".
482 + if (!$this->save_sitemap_file($sitemap_xml, $filename)) {
483 + return new WP_Error(
484 + 'sitemap_generation_failed',
485 + 'Failed to save sitemap: ' . $filename,
486 + ['status' => 500]
487 + );
488 + }
457 489
458 490 // Regenerate the standalone local business sitemap on the
459 491 // single-sitemap path too (parity with Rank Math).
460 492 $this->sitemap_generator->regenerate_local_sitemap($options);
@@ -663,16 +695,39 @@
663 695 return current_user_can('edit_posts');
664 696 }
665 697
666 698 /**
667 - * Check manage permissions
699 + * Check manage permissions for the state-changing routes.
668 700 *
701 + * Every route using this callback is a POST that writes something —
702 + * /generate, /submit, /ping, /settings, /cleanup — so it is nonce-gated as
703 + * well as capability-gated, matching Schema_Endpoint, Setup_Wizard_Endpoint
704 + * and Email_Report_Endpoint. The class already `use`d CSRF_Protection but
705 + * never called it, leaving this controller the odd one out.
706 + *
669 707 * @since 1.0.0
670 708 *
671 - * @return bool Permission status
709 + * @param WP_REST_Request $request Request object
710 + * @return bool|WP_Error Permission status
672 711 */
673 - public function check_manage_permissions(): bool {
674 - return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_crawling');
712 + public function check_manage_permissions(WP_REST_Request $request) {
713 + if (!\ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_crawling')) {
714 + return new WP_Error(
715 + 'rest_forbidden',
716 + __('You do not have permission to manage sitemaps.', 'thinkrank'),
717 + ['status' => 403]
718 + );
719 + }
720 +
721 + if (!$this->verify_request_nonce($request)) {
722 + return new WP_Error(
723 + 'rest_forbidden',
724 + __('Invalid security token. Please refresh the page and try again.', 'thinkrank'),
725 + ['status' => 403]
726 + );
727 + }
728 +
729 + return true;
675 730 }
676 731
677 732 /**
678 733 * Save sitemap to file
@@ -859,10 +914,15 @@
859 914 * @return WP_REST_Response|WP_Error Response object or error
860 915 */
861 916 public function get_sitemap_settings(WP_REST_Request $request) {
862 917 try {
863 - $context_type = $request->get_param('context_type') ?? 'site';
864 - $context_id = $request->get_param('context_id') ?? null;
918 + // SECURITY: the settings are stored per context, so the object has
919 + // to be authorised before it is read (#385).
920 + $context = $this->resolve_request_context($request);
921 + if (is_wp_error($context)) {
922 + return $context;
923 + }
924 + [$context_type, $context_id] = $context;
865 925
866 926 // Get settings from Sitemap_Generator
867 927 $settings = $this->sitemap_generator->get_settings($context_type, $context_id);
868 928
@@ -870,9 +930,14 @@
870 930 'success' => true,
871 931 'data' => [
872 932 'settings' => $settings,
873 933 'context_type' => $context_type,
874 - 'context_id' => $context_id
934 + 'context_id' => $context_id,
935 + // Kept out of `settings` on purpose: this is generator state,
936 + // not something the settings POST round-trips.
937 + 'health' => $context_type === 'site'
938 + ? $this->sitemap_generator->get_regeneration_health()
939 + : null
875 940 ],
876 941 'message' => 'Sitemap settings retrieved successfully'
877 942 ], 200);
878 943
@@ -895,11 +960,17 @@
895 960 */
896 961 public function update_sitemap_settings(WP_REST_Request $request) {
897 962 try {
898 963 $settings = $request->get_param('settings') ?? [];
899 - $context_type = $request->get_param('context_type') ?? 'site';
900 - $context_id = $request->get_param('context_id') ?? null;
901 964
965 + // SECURITY: this write is keyed by the context, so the object has to
966 + // be authorised before anything is persisted (#385).
967 + $context = $this->resolve_request_context($request);
968 + if (is_wp_error($context)) {
969 + return $context;
970 + }
971 + [$context_type, $context_id] = $context;
972 +
902 973 if (empty($settings)) {
903 974 return new WP_Error(
904 975 'missing_settings',
905 976 'Settings data is required',
@@ -1256,9 +1327,9 @@
1256 1327 * @since 1.0.0
1257 1328 * @return bool True if lock acquired
1258 1329 */
1259 1330 private function acquire_generation_lock(): bool {
1260 - $lock_key = 'thinkrank_sitemap_generation_lock';
1331 + $lock_key = Sitemap_Generator::GENERATION_LOCK_TRANSIENT;
1261 1332
1262 1333 if (get_transient($lock_key)) {
1263 1334 return false; // Generation already in progress
1264 1335 }
@@ -1273,7 +1344,7 @@
1273 1344 * @since 1.0.0
1274 1345 * @return void
1275 1346 */
1276 1347 private function release_generation_lock(): void {
1277 - delete_transient('thinkrank_sitemap_generation_lock');
1348 + delete_transient(Sitemap_Generator::GENERATION_LOCK_TRANSIENT);
1278 1349 }
1279 1350 }