| @@ -520,9 +520,9 @@ | ||
| 520 | 520 | |
| 521 | 521 | register_rest_route(self::NAMESPACE, '/schema/enable-for-post', [ |
| 522 | 522 | 'methods' => 'POST', |
| 523 | 523 | 'callback' => [$this, 'enable_schema_for_post'], |
| 524 | - 'permission_callback' => [$this, 'check_admin_permissions'], | |
| 524 | + 'permission_callback' => [$this, 'check_schema_permissions'], | |
| 525 | 525 | 'args' => [ |
| 526 | 526 | 'post_id' => [ |
| 527 | 527 | 'type' => 'integer', |
| 528 | 528 | 'required' => true, |
| @@ -533,9 +533,9 @@ | ||
| 533 | 533 | |
| 534 | 534 | register_rest_route(self::NAMESPACE, '/ai/test-connection', [ |
| 535 | 535 | 'methods' => 'POST', |
| 536 | 536 | 'callback' => [$this, 'test_ai_connection'], |
| 537 | - 'permission_callback' => [$this, 'check_admin_permissions'], | |
| 537 | + 'permission_callback' => [$this, 'check_ai_tools_permissions'], | |
| 538 | 538 | 'args' => [ |
| 539 | 539 | 'api_key' => [ |
| 540 | 540 | 'type' => 'string', |
| 541 | 541 | 'required' => false, |
| @@ -573,9 +573,9 @@ | ||
| 573 | 573 | // simply leaves the user typing the id by hand (#721). |
| 574 | 574 | register_rest_route(self::NAMESPACE, '/ai/models', [ |
| 575 | 575 | 'methods' => 'POST', |
| 576 | 576 | 'callback' => [$this, 'list_endpoint_models'], |
| 577 | - 'permission_callback' => [$this, 'check_admin_permissions'], | |
| 577 | + 'permission_callback' => [$this, 'check_ai_tools_permissions'], | |
| 578 | 578 | 'args' => [ |
| 579 | 579 | 'base_url' => [ |
| 580 | 580 | 'type' => 'string', |
| 581 | 581 | 'required' => false, |
| @@ -844,8 +844,70 @@ | ||
| 844 | 844 | return true; |
| 845 | 845 | } |
| 846 | 846 | |
| 847 | 847 | /** |
| 848 | + * Check Schema Manager section permissions. | |
| 849 | + * | |
| 850 | + * Delegable via Role Manager: route_map() maps the `schema` prefix to | |
| 851 | + * thinkrank_schema. /schema/enable-for-post is what the editor's "enable | |
| 852 | + * structured data" suggestion posts to, so gating it on manage_options made | |
| 853 | + * that button fail for exactly the roles the Schema grant was meant to | |
| 854 | + * serve (#844). | |
| 855 | + * | |
| 856 | + * @param \WP_REST_Request $request Request object | |
| 857 | + * @return bool|\WP_Error Permission status | |
| 858 | + */ | |
| 859 | + public function check_schema_permissions(\WP_REST_Request $request) { | |
| 860 | + return $this->check_mapped_capability('thinkrank_schema'); | |
| 861 | + } | |
| 862 | + | |
| 863 | + /** | |
| 864 | + * Check AI Tools section permissions. | |
| 865 | + * | |
| 866 | + * Delegable via Role Manager: route_map() maps the `ai` prefix to | |
| 867 | + * thinkrank_content_tools. Testing a provider and listing its models are | |
| 868 | + * configuration reads that report whether the stored key works; neither | |
| 869 | + * returns the key. | |
| 870 | + * | |
| 871 | + * @param \WP_REST_Request $request Request object | |
| 872 | + * @return bool|\WP_Error Permission status | |
| 873 | + */ | |
| 874 | + public function check_ai_tools_permissions(\WP_REST_Request $request) { | |
| 875 | + return $this->check_mapped_capability('thinkrank_content_tools'); | |
| 876 | + } | |
| 877 | + | |
| 878 | + /** | |
| 879 | + * Logged in, and holding a ThinkRank capability from the map. | |
| 880 | + * | |
| 881 | + * One body for the per-section callbacks above so they cannot drift apart | |
| 882 | + * the way the hardcoded manage_options checks drifted from the map. | |
| 883 | + * Administrators are unaffected: Role_Manager grants every ThinkRank | |
| 884 | + * capability to manage_options holders through `user_has_cap`. | |
| 885 | + * | |
| 886 | + * @param string $capability ThinkRank capability slug. | |
| 887 | + * @return bool|\WP_Error Permission status | |
| 888 | + */ | |
| 889 | + private function check_mapped_capability(string $capability) { | |
| 890 | + if (!is_user_logged_in()) { | |
| 891 | + return new \WP_Error( | |
| 892 | + 'rest_forbidden', | |
| 893 | + __('You must be logged in to access this endpoint.', 'thinkrank'), | |
| 894 | + ['status' => 401] | |
| 895 | + ); | |
| 896 | + } | |
| 897 | + | |
| 898 | + if (!\ThinkRank\Core\Capability_Manager::current_user_can($capability)) { | |
| 899 | + return new \WP_Error( | |
| 900 | + 'rest_forbidden', | |
| 901 | + __('You do not have permission to access this ThinkRank feature.', 'thinkrank'), | |
| 902 | + ['status' => 403] | |
| 903 | + ); | |
| 904 | + } | |
| 905 | + | |
| 906 | + return true; | |
| 907 | + } | |
| 908 | + | |
| 909 | + /** | |
| 848 | 910 | * Get user capabilities |
| 849 | 911 | * |
| 850 | 912 | * @param \WP_REST_Request $request Request object |
| 851 | 913 | * @return \WP_REST_Response Response object |
| @@ -2140,9 +2202,9 @@ | ||
| 2140 | 2202 | 'model' => $model, |
| 2141 | 2203 | 'model_available' => $model !== '', |
| 2142 | 2204 | 'message' => $model !== '' |
| 2143 | 2205 | /* translators: %s: the model id that was tested. */ |
| 2144 | - ? sprintf(__('OpenAI API connection successful — model "%s" is available.', 'thinkrank'), $model) | |
| 2206 | + ? sprintf(__('OpenAI API connection successful. Model "%s" is available.', 'thinkrank'), $model) | |
| 2145 | 2207 | : __('OpenAI API connection successful!', 'thinkrank'), |
| 2146 | 2208 | 'models_count' => count($data['data']), |
| 2147 | 2209 | ]; |
| 2148 | 2210 | } |
| @@ -2218,9 +2280,9 @@ | ||
| 2218 | 2280 | 'model' => $model, |
| 2219 | 2281 | 'model_available' => $model !== '', |
| 2220 | 2282 | 'message' => $model !== '' |
| 2221 | 2283 | /* translators: %s: the model id that was tested. */ |
| 2222 | - ? sprintf(__('OpenRouter API connection successful — model "%s" is available.', 'thinkrank'), $model) | |
| 2284 | + ? sprintf(__('OpenRouter API connection successful. Model "%s" is available.', 'thinkrank'), $model) | |
| 2223 | 2285 | : __('OpenRouter API connection successful!', 'thinkrank'), |
| 2224 | 2286 | ]; |
| 2225 | 2287 | } |
| 2226 | 2288 | } |
| @@ -2344,9 +2406,9 @@ | ||
| 2344 | 2406 | 'success' => true, |
| 2345 | 2407 | 'model' => $claude_model, |
| 2346 | 2408 | 'model_available' => true, |
| 2347 | 2409 | /* translators: %s: the model id that was tested. */ |
| 2348 | - 'message' => sprintf(__('Claude API connection successful — model "%s" is available.', 'thinkrank'), $claude_model), | |
| 2410 | + 'message' => sprintf(__('Claude API connection successful. Model "%s" is available.', 'thinkrank'), $claude_model), | |
| 2349 | 2411 | ]; |
| 2350 | 2412 | } else { |
| 2351 | 2413 | $error_data = json_decode($response_body, true); |
| 2352 | 2414 | $error_message = $error_data['error']['message'] ?? __('Unknown API error', 'thinkrank'); |
| @@ -2431,9 +2493,9 @@ | ||
| 2431 | 2493 | 'success' => true, |
| 2432 | 2494 | 'model' => $gemini_model, |
| 2433 | 2495 | 'model_available' => true, |
| 2434 | 2496 | /* translators: %s: the model id that was tested. */ |
| 2435 | - 'message' => sprintf(__('Gemini API connection successful — model "%s" is available.', 'thinkrank'), $gemini_model), | |
| 2497 | + 'message' => sprintf(__('Gemini API connection successful. Model "%s" is available.', 'thinkrank'), $gemini_model), | |
| 2436 | 2498 | ]; |
| 2437 | 2499 | } else { |
| 2438 | 2500 | $error_data = json_decode($response_body, true); |
| 2439 | 2501 | $error_message = $error_data['error']['message'] ?? __('Unknown API error', 'thinkrank'); |