PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / trunk
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO vtrunk
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 1.11.0 All 47 releases
thinkrank / includes / api / traits / trait-context-authorization.php

trait-context-authorization.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO trunk, at includes/api/traits/trait-context-authorization.php

156 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Context Authorization Trait
4 *
5 * Every ThinkRank REST route that accepts a `context_type` / `context_id` pair
6 * addresses a specific object, and a section capability — `thinkrank_schema`,
7 * `thinkrank_social_media`, `thinkrank_crawling` and friends — only authorises
8 * entry to that section. It is not permission to read or write SEO data for
9 * every post on the site. Without an object-level check a delegated user can
10 * walk `context_id` and reach other authors' drafts (#364, #366, #385).
11 *
12 * The check lived as a private copy in two endpoint classes, so handlers that
13 * never called it were missed twice over. It lives here now, and any endpoint
14 * accepting a context uses this trait.
15 *
16 * @package ThinkRank
17 * @subpackage API\Traits
18 * @since 2.0.1
19 */
20
21 declare(strict_types=1);
22
23 namespace ThinkRank\API\Traits;
24
25 use WP_Error;
26
27 // Prevent direct access
28 if (!defined('ABSPATH')) {
29 exit;
30 }
31
32 /**
33 * Context Authorization Trait
34 *
35 * @since 2.0.1
36 */
37 trait Context_Authorization {
38
39 /**
40 * Context types the ThinkRank REST API addresses objects with.
41 *
42 * @since 2.0.1
43 * @var string[]
44 */
45 private static $context_types = ['site', 'post', 'page', 'product'];
46
47 /**
48 * Validate a context type and ID, and the caller's access to that object.
49 *
50 * @since 2.0.1
51 *
52 * @param string $context_type Context type.
53 * @param int|null $context_id Context ID.
54 * @return true|WP_Error True when the caller may use this context, WP_Error
55 * otherwise (400 for a shape error, 403 for authorization).
56 */
57 protected function validate_context(string $context_type, ?int $context_id) {
58 $invalid = new WP_Error(
59 'invalid_context',
60 'Invalid context type or ID provided',
61 ['status' => 400]
62 );
63
64 if (!in_array($context_type, self::$context_types, true)) {
65 return $invalid;
66 }
67
68 if ($context_type !== 'site' && (!$context_id || $context_id <= 0)) {
69 return $invalid;
70 }
71
72 if ($context_id && !get_post($context_id)) {
73 return $invalid;
74 }
75
76 // SECURITY: everything above establishes that the context *exists*, not
77 // that this caller may see it. `edit_post` is a meta capability, so
78 // map_meta_cap() resolves authorship, published state and
79 // edit_others_posts for this specific post — the same check
80 // Schema_Input_Validator::validate_context_ownership() and
81 // Settings_Management_Endpoint::authorize_settings_context() make on
82 // their write paths.
83 //
84 // Site context is deliberately left to the route's capability gate.
85 // The write paths demand manage_options for it, but applying that here
86 // would stop a delegated Schema Manager reading site-level schema at
87 // all, which is the point of delegating the section.
88 if ($context_type !== 'site' && !current_user_can('edit_post', $context_id)) {
89 return new WP_Error(
90 'rest_forbidden',
91 'You are not allowed to access this content.',
92 ['status' => 403]
93 );
94 }
95
96 return true;
97 }
98
99 /**
100 * Read the context pair off a request and authorise it in one step.
101 *
102 * Handlers that only need "is this allowed, and what are the resolved
103 * values" use this rather than repeating the read/cast/validate dance.
104 *
105 * @since 2.0.1
106 *
107 * @param \WP_REST_Request $request Request object.
108 * @return array{0: string, 1: int|null}|WP_Error The resolved
109 * [$context_type, $context_id], or the validation error.
110 */
111 protected function resolve_request_context(\WP_REST_Request $request) {
112 $context_type = $request->get_param('context_type') ?? 'site';
113 $context_id = $request->get_param('context_id');
114 $context_id = (null === $context_id || '' === $context_id) ? null : (int) $context_id;
115
116 $validation = $this->validate_context((string) $context_type, $context_id);
117
118 if (is_wp_error($validation)) {
119 return $validation;
120 }
121
122 return [(string) $context_type, $context_id];
123 }
124
125 /**
126 * REST args declaring the context pair, so the values arrive typed.
127 *
128 * Named apart from Social_Media_Endpoint's own private get_context_args():
129 * a class method silently wins over a trait method of the same name, and
130 * that one marks both parameters required — correct for the
131 * /social-media/meta/{context} routes it was written for, and a 400 on
132 * every site-context read if it captured these registrations.
133 *
134 * @since 2.0.1
135 *
136 * @return array<string, array<string, mixed>> Argument definitions.
137 */
138 protected function get_context_route_args(): array {
139 return [
140 'context_type' => [
141 'required' => false,
142 'type' => 'string',
143 'enum' => self::$context_types,
144 'default' => 'site',
145 'description' => 'Context type',
146 ],
147 'context_id' => [
148 'required' => false,
149 'type' => 'integer',
150 'minimum' => 1,
151 'description' => 'Context ID (required for non-site contexts)',
152 ],
153 ];
154 }
155 }
156