PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.3
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.3
2.14.3 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 All 58 releases
thinkrank / includes / core / class-role-manager.php

class-role-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.14.3, at includes/core/class-role-manager.php

146 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace ThinkRank\Core;
6
7 use WP_Error;
8
9 // Prevent direct access
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 /**
15 * Role Manager
16 *
17 * Wires up ThinkRank's role-based access control:
18 * - keeps the administrator role's capabilities in sync;
19 * - centrally guards the whole `/thinkrank/v1/` REST namespace via a single
20 * `rest_pre_dispatch` filter (route prefix → capability), so no per-endpoint
21 * permission callback needs to change.
22 *
23 * Menu-access gating (the admin page capability) and the SPA nav filtering are
24 * handled by the Admin manager and the React app respectively, both reading
25 * from {@see Capability_Manager}.
26 *
27 * @since 1.12.0
28 */
29 class Role_Manager {
30
31 /**
32 * Initialize hooks.
33 *
34 * @return void
35 */
36 public function init(): void {
37 add_action('init', [Capability_Manager::class, 'ensure']);
38 add_filter('user_has_cap', [$this, 'grant_admin_caps'], 10, 2);
39 add_filter('rest_pre_dispatch', [$this, 'gate_rest'], 10, 3);
40 }
41
42 /**
43 * Give every ThinkRank capability to anyone who can `manage_options`.
44 *
45 * The bypass already existed, but only where the plugin could see it.
46 * Capability_Manager::current_user_can() is a plugin static, invisible to
47 * WordPress core — and add_menu_page() evaluates its capability argument
48 * with core's current_user_can(). Meanwhile Capability_Manager::ensure()
49 * grants the capabilities to the `administrator` role by name.
50 *
51 * So a user holding manage_options through some other role — a multisite
52 * super admin, or a custom admin role from Members / User Role Editor —
53 * passed every REST check and saw no ThinkRank menu at all: the plugin was
54 * invisible in wp-admin while fully reachable over the API (#575).
55 * Expressing the bypass as a core filter closes that split and makes the
56 * explicit check in Capability_Manager belt-and-braces rather than the only
57 * path.
58 *
59 * Uses `+` rather than array_merge: an entry already present keeps its
60 * value, so an explicit denial elsewhere is not resurrected here.
61 *
62 * @since 2.1.3
63 *
64 * @param array $allcaps Capabilities the user holds.
65 * @param array $caps Capabilities being checked (unused).
66 * @return array
67 */
68 public function grant_admin_caps(array $allcaps, array $caps): array {
69 if (empty($allcaps['manage_options'])) {
70 return $allcaps;
71 }
72
73 // Built once per request. Uses slugs() rather than capabilities():
74 // core fires this filter from wp_set_current_user() during
75 // wp-settings.php, before `init`, so the __() calls behind the labelled
76 // map are both wasted (only the keys are used) and early enough to earn
77 // a _load_textdomain_just_in_time notice on every request (#580).
78 static $thinkrank_caps = null;
79 if (null === $thinkrank_caps) {
80 $thinkrank_caps = array_fill_keys(
81 Capability_Manager::slugs(),
82 true
83 );
84 }
85
86 return $allcaps + $thinkrank_caps;
87 }
88
89 /**
90 * Central capability gate for all ThinkRank REST routes.
91 *
92 * @param mixed $result Existing short-circuit result (or null).
93 * @param \WP_REST_Server $server REST server.
94 * @param \WP_REST_Request $request The request.
95 * @return mixed Null/array to proceed, or WP_Error to block.
96 */
97 public function gate_rest($result, $server, $request) {
98 // Respect an earlier short-circuit.
99 if (null !== $result) {
100 return $result;
101 }
102
103 // WP_REST_Server matches routes case-insensitively ('@^...$@i'), so
104 // /ThinkRank/v1/... reaches the same handler as /thinkrank/v1/...
105 // Compare a lowercased copy, or a change of case skips the gate (#975).
106 // Every route this class names, including the exemptions below, is
107 // lowercase, and lowercasing only lets a variant reach the same handler
108 // its lowercase form already reaches, so no exemption grows.
109 $route = strtolower((string) $request->get_route());
110 // Gate both the free (/thinkrank/v1/) and Pro (/thinkrank-pro/v1/)
111 // namespaces so the Role Manager governs Pro sections too — otherwise the
112 // whole Pro namespace bypasses the capability gate.
113 if (strpos($route, '/thinkrank/v1/') !== 0 && strpos($route, '/thinkrank-pro/v1/') !== 0) {
114 return $result;
115 }
116
117 // MCP + OAuth routes authenticate INSIDE their handlers (Bearer token /
118 // OAuth access token — server-to-server calls with no logged-in user),
119 // so the namespace-wide capability gate must not touch them. The MCP
120 // management routes (/mcp/connection, /mcp/connect, …) stay gated. See
121 // ThinkRank\Mcp\Mcp_Manager.
122 if ('/thinkrank/v1/mcp' === $route || strpos($route, '/thinkrank/v1/mcp/oauth/') === 0) {
123 return $result;
124 }
125
126 if (!Capability_Manager::current_user_can(Capability_Manager::ACCESS)) {
127 return new WP_Error(
128 'thinkrank_forbidden',
129 __('You do not have permission to access ThinkRank.', 'thinkrank'),
130 ['status' => rest_authorization_required_code()]
131 );
132 }
133
134 $capability = Capability_Manager::capability_for_route($route);
135 if ($capability !== Capability_Manager::ACCESS && !Capability_Manager::current_user_can($capability)) {
136 return new WP_Error(
137 'thinkrank_forbidden_section',
138 __('You do not have permission to access this ThinkRank section.', 'thinkrank'),
139 ['status' => rest_authorization_required_code()]
140 );
141 }
142
143 return $result;
144 }
145 }
146