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 +72 -52 1.0.2 → 2.9.0 View file →
@@ -15,13 +15,21 @@
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 +
27 +// Prevent direct access
28 +if (!defined('ABSPATH')) {
29 + exit;
30 +}
31 +
24 32 /**
25 33 * Social Media API Endpoints Class
26 34 *
27 35 * Provides REST API endpoints for social media operations including
@@ -31,8 +39,10 @@
31 39 * @since 1.0.0
32 40 */
33 41 class Social_Media_Endpoint extends WP_REST_Controller {
34 42
43 + use Context_Authorization;
44 +
35 45 /**
36 46 * Social Meta Manager instance
37 47 *
38 48 * @since 1.0.0
@@ -78,9 +88,10 @@
78 88 [
79 89 [
80 90 'methods' => 'GET',
81 91 'callback' => [$this, 'get_settings'],
82 - 'permission_callback' => [$this, 'check_read_permissions']
92 + 'permission_callback' => [$this, 'check_read_permissions'],
93 + 'args' => $this->get_context_route_args()
83 94 ],
84 95 [
85 96 'methods' => 'POST',
86 97 'callback' => [$this, 'update_settings'],
@@ -186,14 +197,20 @@
186 197 *
187 198 * @since 1.0.0
188 199 *
189 200 * @param WP_REST_Request $request Request object
190 - * @return WP_REST_Response Response object
201 + * @return WP_REST_Response|WP_Error Response object, or the context error
191 202 */
192 - public function get_settings(WP_REST_Request $request): WP_REST_Response {
203 + public function get_settings(WP_REST_Request $request) {
193 204 try {
194 - $context_type = $request->get_param('context_type') ?? 'site';
195 - $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;
196 213
197 214 // Get settings from Social Meta Manager
198 215 $settings = $this->social_manager->get_settings($context_type, $context_id);
199 216
@@ -225,8 +242,10 @@
225 242 * @since 1.0.0
226 243 *
227 244 * @param WP_REST_Request $request Request object
228 245 * @return WP_REST_Response|WP_Error Response object or error
246 + *
247 + * @throws \Exception On failure.
229 248 */
230 249 public function update_settings(WP_REST_Request $request) {
231 250 try {
232 251 $settings = $request->get_param('settings');
@@ -240,8 +259,32 @@
240 259 'error' => 'Settings data is required'
241 260 ], 400);
242 261 }
243 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 +
274 + // Drop unrecognized keys so arbitrary client-supplied keys aren't
275 + // persisted (storage bloat / settings drift). The known set is the
276 + // context's default settings, exposed through a filter for add-ons.
277 + $known = array_keys($this->social_manager->get_default_settings($context_type));
278 + $known = apply_filters('thinkrank_social_known_setting_keys', $known, $context_type);
279 + $settings = array_intersect_key($settings, array_flip($known));
280 + if (empty($settings)) {
281 + return new WP_REST_Response([
282 + 'success' => false,
283 + 'error' => 'No recognized social settings were provided'
284 + ], 400);
285 + }
286 +
244 287 // Validate settings with context
245 288 $validation = $this->social_manager->validate_settings($settings, $validation_context);
246 289 if (!$validation['valid']) {
247 290 return new WP_Error(
@@ -291,11 +334,13 @@
291 334 *
292 335 * @param WP_REST_Request $request Request object
293 336 * @return WP_REST_Response|WP_Error Response object
294 337 */
295 - public function validate_settings(WP_REST_Request $request): WP_REST_Response|WP_Error {
338 + public function validate_settings(WP_REST_Request $request) {
296 339 try {
297 - $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');
298 343 $context_type = $request->get_param('context_type') ?? 'site';
299 344 $validation_context = $request->get_param('validation_context') ?? 'all';
300 345
301 346 // Validate settings using the enhanced Social Meta Manager with tab-specific context
@@ -408,15 +453,13 @@
408 453 try {
409 454 $context_type = $request->get_param('context_type');
410 455 $context_id = (int) $request->get_param('context_id');
411 456
412 - // Validate context
413 - if (!$this->validate_context($context_type, $context_id)) {
414 - return new WP_Error(
415 - 'invalid_context',
416 - 'Invalid context type or ID provided',
417 - ['status' => 400]
418 - );
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;
419 462 }
420 463
421 464 // Get social meta data
422 465 $social_meta = $this->social_manager->get_output_data($context_type, $context_id);
@@ -442,8 +485,10 @@
442 485 * @since 1.0.0
443 486 *
444 487 * @param WP_REST_Request $request Request object
445 488 * @return WP_REST_Response|WP_Error Response object or error
489 + *
490 + * @throws \Exception On failure.
446 491 */
447 492 public function save_social_meta(WP_REST_Request $request) {
448 493 try {
449 494 $context_type = $request->get_param('context_type');
@@ -449,19 +494,19 @@
449 494 $context_type = $request->get_param('context_type');
450 495 $context_id = (int) $request->get_param('context_id');
451 496 $social_data = $request->get_param('social_data') ?? [];
452 497
453 - // Validate context
454 - if (!$this->validate_context($context_type, $context_id)) {
455 - return new WP_Error(
456 - 'invalid_context',
457 - 'Invalid context type or ID provided',
458 - ['status' => 400]
459 - );
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;
460 504 }
461 505
462 - // Save social meta data
463 - $result = $this->social_manager->save_settings($social_data, $context_type, $context_id);
506 + // Save social meta data (manager signature is
507 + // save_settings(context_type, context_id, settings)).
508 + $result = $this->social_manager->save_settings($context_type, $context_id, $social_data);
464 509
465 510 if ($result) {
466 511 return new WP_REST_Response([
467 512 'success' => true,
@@ -471,9 +516,11 @@
471 516 } else {
472 517 throw new \Exception('Failed to save social meta data');
473 518 }
474 519
475 - } catch (\Exception $e) {
520 + } catch (\Throwable $e) {
521 + // Catch \Throwable (not just \Exception) so a future TypeError
522 + // degrades to a JSON error instead of a fatal.
476 523 return new WP_Error(
477 524 'save_failed',
478 525 'Social meta save failed: ' . $e->getMessage(),
479 526 ['status' => 500]
@@ -562,36 +609,9 @@
562 609 *
563 610 * @return bool Permission status
564 611 */
565 612 public function check_manage_permissions(): bool {
566 - return current_user_can('manage_options');
567 - }
568 -
569 - /**
570 - * Validate context type and ID
571 - *
572 - * @since 1.0.0
573 - *
574 - * @param string $context_type Context type
575 - * @param int|null $context_id Context ID
576 - * @return bool Validation status
577 - */
578 - private function validate_context(string $context_type, ?int $context_id): bool {
579 - $valid_types = ['site', 'post', 'page', 'product'];
580 -
581 - if (!in_array($context_type, $valid_types, true)) {
582 - return false;
583 - }
584 -
585 - if ($context_type !== 'site' && (!$context_id || $context_id <= 0)) {
586 - return false;
587 - }
588 -
589 - if ($context_id && !get_post($context_id)) {
590 - return false;
591 - }
592 -
593 - return true;
613 + return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_social_media');
594 614 }
595 615
596 616 /**
597 617 * Get arguments for preview endpoint