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 / 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 trunk, at includes/core/class-role-manager.php

140 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 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 $route = (string) $request->get_route();
104 // Gate both the free (/thinkrank/v1/) and Pro (/thinkrank-pro/v1/)
105 // namespaces so the Role Manager governs Pro sections too — otherwise the
106 // whole Pro namespace bypasses the capability gate.
107 if (strpos($route, '/thinkrank/v1/') !== 0 && strpos($route, '/thinkrank-pro/v1/') !== 0) {
108 return $result;
109 }
110
111 // MCP + OAuth routes authenticate INSIDE their handlers (Bearer token /
112 // OAuth access token — server-to-server calls with no logged-in user),
113 // so the namespace-wide capability gate must not touch them. The MCP
114 // management routes (/mcp/connection, /mcp/connect, …) stay gated. See
115 // ThinkRank\Mcp\Mcp_Manager.
116 if ('/thinkrank/v1/mcp' === $route || strpos($route, '/thinkrank/v1/mcp/oauth/') === 0) {
117 return $result;
118 }
119
120 if (!Capability_Manager::current_user_can(Capability_Manager::ACCESS)) {
121 return new WP_Error(
122 'thinkrank_forbidden',
123 __('You do not have permission to access ThinkRank.', 'thinkrank'),
124 ['status' => rest_authorization_required_code()]
125 );
126 }
127
128 $capability = Capability_Manager::capability_for_route($route);
129 if ($capability !== Capability_Manager::ACCESS && !Capability_Manager::current_user_can($capability)) {
130 return new WP_Error(
131 'thinkrank_forbidden_section',
132 __('You do not have permission to access this ThinkRank section.', 'thinkrank'),
133 ['status' => rest_authorization_required_code()]
134 );
135 }
136
137 return $result;
138 }
139 }
140