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/api/class-social-media-endpoint.php +47 -59 1.28.0 → 2.9.0 View file →
@@ -15,13 +15,16 @@
15 15
16 16 namespace ThinkRank\API;
17 17
18 18 use ThinkRank\SEO\Social_Meta_Manager;
19 +use ThinkRank\API\Traits\Context_Authorization;
19 20 use WP_REST_Controller;
20 21 use WP_REST_Request;
21 22 use WP_REST_Response;
22 23 use WP_Error;
23 24
25 +require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-context-authorization.php';
26 +
24 27 // Prevent direct access
25 28 if (!defined('ABSPATH')) {
26 29 exit;
27 30 }
@@ -36,8 +39,10 @@
36 39 * @since 1.0.0
37 40 */
38 41 class Social_Media_Endpoint extends WP_REST_Controller {
39 42
43 + use Context_Authorization;
44 +
40 45 /**
41 46 * Social Meta Manager instance
42 47 *
43 48 * @since 1.0.0
@@ -83,9 +88,10 @@
83 88 [
84 89 [
85 90 'methods' => 'GET',
86 91 'callback' => [$this, 'get_settings'],
87 - 'permission_callback' => [$this, 'check_read_permissions']
92 + 'permission_callback' => [$this, 'check_read_permissions'],
93 + 'args' => $this->get_context_route_args()
88 94 ],
89 95 [
90 96 'methods' => 'POST',
91 97 'callback' => [$this, 'update_settings'],
@@ -191,14 +197,20 @@
191 197 *
192 198 * @since 1.0.0
193 199 *
194 200 * @param WP_REST_Request $request Request object
195 - * @return WP_REST_Response Response object
201 + * @return WP_REST_Response|WP_Error Response object, or the context error
196 202 */
197 - public function get_settings(WP_REST_Request $request): WP_REST_Response {
203 + public function get_settings(WP_REST_Request $request) {
198 204 try {
199 - $context_type = $request->get_param('context_type') ?? 'site';
200 - $context_id = $request->get_param('context_id');
205 + // SECURITY: the settings this returns carry the object's social
206 + // title, description and image. update_settings() already authorises
207 + // the object; the read has to as well (#385).
208 + $context = $this->resolve_request_context($request);
209 + if (is_wp_error($context)) {
210 + return $context;
211 + }
212 + [$context_type, $context_id] = $context;
201 213
202 214 // Get settings from Social Meta Manager
203 215 $settings = $this->social_manager->get_settings($context_type, $context_id);
204 216
@@ -230,8 +242,10 @@
230 242 * @since 1.0.0
231 243 *
232 244 * @param WP_REST_Request $request Request object
233 245 * @return WP_REST_Response|WP_Error Response object or error
246 + *
247 + * @throws \Exception On failure.
234 248 */
235 249 public function update_settings(WP_REST_Request $request) {
236 250 try {
237 251 $settings = $request->get_param('settings');
@@ -245,8 +259,19 @@
245 259 'error' => 'Settings data is required'
246 260 ], 400);
247 261 }
248 262
263 + // SECURITY: this route writes the same per-object social overrides
264 + // as save_social_meta(), so it needs the same object-level guard —
265 + // the section-level thinkrank_social_media capability alone would
266 + // let a delegated user write to any post (IDOR). validate_context()
267 + // carries that guard for every context route in this class.
268 + $context_id = $context_id === null ? null : (int) $context_id;
269 + $context_validation = $this->validate_context($context_type, $context_id);
270 + if (is_wp_error($context_validation)) {
271 + return $context_validation;
272 + }
273 +
249 274 // Drop unrecognized keys so arbitrary client-supplied keys aren't
250 275 // persisted (storage bloat / settings drift). The known set is the
251 276 // context's default settings, exposed through a filter for add-ons.
252 277 $known = array_keys($this->social_manager->get_default_settings($context_type));
@@ -309,11 +334,13 @@
309 334 *
310 335 * @param WP_REST_Request $request Request object
311 336 * @return WP_REST_Response|WP_Error Response object
312 337 */
313 - public function validate_settings(WP_REST_Request $request): WP_REST_Response|WP_Error {
338 + public function validate_settings(WP_REST_Request $request) {
314 339 try {
315 - $settings = $request->get_param('settings') ?? [];
340 + // `settings` is registered required, so REST rejects the request
341 + // before this runs — the old `?? []` default was unreachable.
342 + $settings = $request->get_param('settings');
316 343 $context_type = $request->get_param('context_type') ?? 'site';
317 344 $validation_context = $request->get_param('validation_context') ?? 'all';
318 345
319 346 // Validate settings using the enhanced Social Meta Manager with tab-specific context
@@ -426,15 +453,13 @@
426 453 try {
427 454 $context_type = $request->get_param('context_type');
428 455 $context_id = (int) $request->get_param('context_id');
429 456
430 - // Validate context
431 - if (!$this->validate_context($context_type, $context_id)) {
432 - return new WP_Error(
433 - 'invalid_context',
434 - 'Invalid context type or ID provided',
435 - ['status' => 400]
436 - );
457 + // Validate context and the caller's access to it. Returns true or a
458 + // WP_Error carrying the right status (400 shape, 403 authorization).
459 + $context_validation = $this->validate_context($context_type, $context_id);
460 + if (is_wp_error($context_validation)) {
461 + return $context_validation;
437 462 }
438 463
439 464 // Get social meta data
440 465 $social_meta = $this->social_manager->get_output_data($context_type, $context_id);
@@ -460,8 +485,10 @@
460 485 * @since 1.0.0
461 486 *
462 487 * @param WP_REST_Request $request Request object
463 488 * @return WP_REST_Response|WP_Error Response object or error
489 + *
490 + * @throws \Exception On failure.
464 491 */
465 492 public function save_social_meta(WP_REST_Request $request) {
466 493 try {
467 494 $context_type = $request->get_param('context_type');
@@ -467,28 +494,16 @@
467 494 $context_type = $request->get_param('context_type');
468 495 $context_id = (int) $request->get_param('context_id');
469 496 $social_data = $request->get_param('social_data') ?? [];
470 497
471 - // Validate context
472 - if (!$this->validate_context($context_type, $context_id)) {
473 - return new WP_Error(
474 - 'invalid_context',
475 - 'Invalid context type or ID provided',
476 - ['status' => 400]
477 - );
498 + // Validate context and the caller's access to it. validate_context()
499 + // now carries the object-level edit_post guard for non-site contexts,
500 + // so this write path inherits the same check it used to make inline.
501 + $context_validation = $this->validate_context($context_type, $context_id);
502 + if (is_wp_error($context_validation)) {
503 + return $context_validation;
478 504 }
479 505
480 - // SECURITY: This route writes per-post social overrides, so require
481 - // object-level edit permission — the global thinkrank_social_media
482 - // capability alone would allow writing to any post (IDOR).
483 - if (!current_user_can('edit_post', $context_id)) {
484 - return new WP_Error(
485 - 'rest_forbidden',
486 - 'You are not allowed to edit this content.',
487 - ['status' => 403]
488 - );
489 - }
490 -
491 506 // Save social meta data (manager signature is
492 507 // save_settings(context_type, context_id, settings)).
493 508 $result = $this->social_manager->save_settings($context_type, $context_id, $social_data);
494 509
@@ -595,35 +610,8 @@
595 610 * @return bool Permission status
596 611 */
597 612 public function check_manage_permissions(): bool {
598 613 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_social_media');
599 - }
600 -
601 - /**
602 - * Validate context type and ID
603 - *
604 - * @since 1.0.0
605 - *
606 - * @param string $context_type Context type
607 - * @param int|null $context_id Context ID
608 - * @return bool Validation status
609 - */
610 - private function validate_context(string $context_type, ?int $context_id): bool {
611 - $valid_types = ['site', 'post', 'page', 'product'];
612 -
613 - if (!in_array($context_type, $valid_types, true)) {
614 - return false;
615 - }
616 -
617 - if ($context_type !== 'site' && (!$context_id || $context_id <= 0)) {
618 - return false;
619 - }
620 -
621 - if ($context_id && !get_post($context_id)) {
622 - return false;
623 - }
624 -
625 - return true;
626 614 }
627 615
628 616 /**
629 617 * Get arguments for preview endpoint