PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 2.8.17 2.8.18 All 259 releases
wpfunnels / includes / core / rest-api / Controllers / McpSettingsController.php

McpSettingsController.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.1, at includes/core/rest-api/Controllers/McpSettingsController.php

348 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * McpSettingsController — REST API controller for the WPFunnels MCP settings tab.
4 *
5 * Routes:
6 * GET /wpfunnels/v1/settings/mcp — MCP enable/endpoint/tool count
7 * POST /wpfunnels/v1/settings/mcp — Toggle MCP on/off
8 * GET /wpfunnels/v1/settings/mcp/app-passwords — List WPFunnels-scoped app passwords
9 * POST /wpfunnels/v1/settings/mcp/app-passwords — Create a new app password
10 * DELETE /wpfunnels/v1/settings/mcp/app-passwords/{uuid} — Revoke an app password
11 *
12 * @package WPFunnels\Rest\Controllers
13 * @since 3.13.0
14 */
15
16 namespace WPFunnels\Rest\Controllers;
17
18 defined( 'ABSPATH' ) || exit;
19
20 use WP_Application_Passwords;
21 use WP_Error;
22 use WP_REST_Request;
23 use WP_REST_Response;
24 use WP_REST_Server;
25 use WPFunnels\MCP\AbilitiesRegistrar;
26 use WPFunnels\MCP\MCPInit;
27
28 /**
29 * Class McpSettingsController
30 */
31 class McpSettingsController extends Wpfnl_REST_Controller {
32
33 /**
34 * Endpoint namespace.
35 *
36 * @var string
37 */
38 protected $namespace = 'wpfunnels/v1';
39
40 /**
41 * Route base.
42 *
43 * @var string
44 */
45 protected $rest_base = 'settings/mcp';
46
47 /**
48 * Name prefix used for application passwords created by this manager, so
49 * listing/revoking never touches credentials created for unrelated plugins
50 * or in the user's normal profile screen.
51 *
52 * @var string
53 */
54 const APP_PASSWORD_PREFIX = 'WPFunnels · ';
55
56 /**
57 * Check permissions for MCP settings endpoints. Same gate as AiChatController
58 * (logged-in + wpf_manage_funnels or manage_options) — WPFunnels' controllers
59 * don't share permission logic via inheritance today, so this is duplicated
60 * intentionally rather than reaching into a sibling controller.
61 *
62 * @param WP_REST_Request $request Request.
63 * @return bool|WP_Error
64 */
65 public function check_permission( $request ) {
66 if ( ! is_user_logged_in() ) {
67 return new WP_Error(
68 'wpfunnels_rest_cannot_access',
69 __( 'You must be logged in to access this endpoint.', 'wpfnl' ),
70 [ 'status' => rest_authorization_required_code() ]
71 );
72 }
73
74 if ( ! current_user_can( 'wpf_manage_funnels' ) && ! current_user_can( 'manage_options' ) ) {
75 return new WP_Error(
76 'wpfunnels_rest_cannot_access',
77 __( 'Sorry, you do not have permission to access this endpoint.', 'wpfnl' ),
78 [ 'status' => rest_authorization_required_code() ]
79 );
80 }
81
82 return true;
83 }
84
85 /**
86 * Permission gate for the Application Password manager. Creating/revoking
87 * credentials for external MCP clients is more sensitive than everyday
88 * copilot use, so this additionally requires `manage_options` and a working
89 * Abilities API — app passwords are pointless without the MCP surface up.
90 *
91 * @param WP_REST_Request $request Request.
92 * @return bool|WP_Error
93 */
94 public function check_app_password_permission( $request ) {
95 $base = $this->check_permission( $request );
96 if ( is_wp_error( $base ) ) {
97 return $base;
98 }
99
100 if ( ! current_user_can( 'manage_options' ) ) {
101 return new WP_Error(
102 'wpfunnels_rest_cannot_access',
103 __( 'Sorry, only site administrators can manage MCP application passwords.', 'wpfnl' ),
104 [ 'status' => rest_authorization_required_code() ]
105 );
106 }
107
108 if ( ! MCPInit::abilitiesApiAvailable() ) {
109 return new WP_Error(
110 'wpfunnels_mcp_unavailable',
111 __( 'The MCP surface requires the WordPress Abilities API, which is not available on this site. Application passwords are not needed without it.', 'wpfnl' ),
112 [ 'status' => 400 ]
113 );
114 }
115
116 if ( ! class_exists( '\WP_Application_Passwords' ) || ! wp_is_application_passwords_available() ) {
117 return new WP_Error(
118 'wpfunnels_app_passwords_unavailable',
119 __( 'Application Passwords are not available on this site.', 'wpfnl' ),
120 [ 'status' => 400 ]
121 );
122 }
123
124 return true;
125 }
126
127 /**
128 * Register REST routes.
129 *
130 * @return void
131 */
132 public function register_routes() {
133 // MCP toggle/endpoint/tool count: GET|POST /settings/mcp
134 register_rest_route(
135 $this->namespace,
136 '/' . $this->rest_base,
137 [
138 [
139 'methods' => WP_REST_Server::READABLE,
140 'callback' => [ $this, 'get_mcp_settings' ],
141 'permission_callback' => [ $this, 'check_permission' ],
142 ],
143 [
144 'methods' => WP_REST_Server::CREATABLE,
145 'callback' => [ $this, 'save_mcp_settings' ],
146 'permission_callback' => [ $this, 'check_permission' ],
147 ],
148 ]
149 );
150
151 // Application password manager collection: GET (list), POST (create)
152 register_rest_route(
153 $this->namespace,
154 '/' . $this->rest_base . '/app-passwords',
155 [
156 [
157 'methods' => WP_REST_Server::READABLE,
158 'callback' => [ $this, 'list_app_passwords' ],
159 'permission_callback' => [ $this, 'check_app_password_permission' ],
160 ],
161 [
162 'methods' => WP_REST_Server::CREATABLE,
163 'callback' => [ $this, 'create_app_password' ],
164 'permission_callback' => [ $this, 'check_app_password_permission' ],
165 ],
166 ]
167 );
168
169 // Revoke a single application password: DELETE /settings/mcp/app-passwords/{uuid}
170 register_rest_route(
171 $this->namespace,
172 '/' . $this->rest_base . '/app-passwords/(?P<uuid>[\w-]+)',
173 [
174 [
175 'methods' => WP_REST_Server::DELETABLE,
176 'callback' => [ $this, 'delete_app_password' ],
177 'permission_callback' => [ $this, 'check_app_password_permission' ],
178 ],
179 ]
180 );
181 }
182
183 /**
184 * Build the MCP settings payload shared by GET and POST.
185 *
186 * @return array
187 */
188 protected function mcp_settings_payload() {
189 $definitions = AbilitiesRegistrar::getDefinitions();
190
191 return [
192 'success' => true,
193 'enabled' => 'no' !== get_option( '_wpfnl_mcp_enabled', 'yes' ),
194 'endpoint_url' => MCPInit::endpointUrl(),
195 'tool_count' => is_array( $definitions ) ? count( $definitions ) : 0,
196 'abilities_api_available' => MCPInit::abilitiesApiAvailable(),
197 ];
198 }
199
200 /**
201 * Current MCP state: enabled, endpoint URL, tool count, Abilities API support.
202 *
203 * @param WP_REST_Request $request Request.
204 * @return WP_REST_Response
205 */
206 public function get_mcp_settings( $request ) {
207 return rest_ensure_response( $this->mcp_settings_payload() );
208 }
209
210 /**
211 * Persist the MCP enable toggle.
212 *
213 * This writes the same `_wpfnl_mcp_enabled` option that
214 * AiChatController::save_ai_settings() already writes as a convenience fold —
215 * that path is left as-is for backward compatibility with any existing
216 * frontend calls; this is the dedicated settings/mcp counterpart per the plan.
217 *
218 * @param WP_REST_Request $request Request.
219 * @return WP_REST_Response
220 */
221 public function save_mcp_settings( $request ) {
222 $params = (array) $request->get_json_params();
223 if ( empty( $params ) ) {
224 $params = (array) $request->get_params();
225 }
226
227 if ( array_key_exists( 'enabled', $params ) ) {
228 update_option( '_wpfnl_mcp_enabled', rest_sanitize_boolean( $params['enabled'] ) ? 'yes' : 'no' );
229 }
230
231 return rest_ensure_response( $this->mcp_settings_payload() );
232 }
233
234 /**
235 * List WPFunnels-scoped application passwords for the current user.
236 *
237 * Only entries whose stored name starts with self::APP_PASSWORD_PREFIX are
238 * returned, so credentials created for unrelated plugins never surface here.
239 * The plaintext password is never stored by WordPress core past creation, so
240 * it is never part of this response.
241 *
242 * @param WP_REST_Request $request Request.
243 * @return WP_REST_Response|WP_Error
244 */
245 public function list_app_passwords( $request ) {
246 $user_id = get_current_user_id();
247 $all = WP_Application_Passwords::get_user_application_passwords( $user_id );
248
249 $items = [];
250 foreach ( (array) $all as $item ) {
251 if ( empty( $item['name'] ) || 0 !== strpos( $item['name'], self::APP_PASSWORD_PREFIX ) ) {
252 continue;
253 }
254
255 $items[] = [
256 'uuid' => $item['uuid'],
257 'name' => substr( $item['name'], strlen( self::APP_PASSWORD_PREFIX ) ),
258 'created' => isset( $item['created'] ) ? (int) $item['created'] : 0,
259 'last_used' => ! empty( $item['last_used'] ) ? (int) $item['last_used'] : null,
260 'last_ip' => ! empty( $item['last_ip'] ) ? $item['last_ip'] : '',
261 ];
262 }
263
264 return rest_ensure_response(
265 [
266 'success' => true,
267 'app_passwords' => $items,
268 ]
269 );
270 }
271
272 /**
273 * Create a new WPFunnels-scoped application password.
274 *
275 * The plaintext password is returned exactly once, in this response — WordPress
276 * core only stores its hash, so it can never be re-displayed after this call.
277 *
278 * @param WP_REST_Request $request Request.
279 * @return WP_REST_Response|WP_Error
280 */
281 public function create_app_password( $request ) {
282 $user_id = get_current_user_id();
283 $label = sanitize_text_field( (string) $request->get_param( 'name' ) );
284 if ( '' === $label ) {
285 $label = __( 'MCP Client', 'wpfnl' );
286 }
287
288 $created = WP_Application_Passwords::create_new_application_password(
289 $user_id,
290 [ 'name' => self::APP_PASSWORD_PREFIX . $label ]
291 );
292
293 if ( is_wp_error( $created ) ) {
294 return $created;
295 }
296
297 list( $password, $item ) = $created;
298
299 return rest_ensure_response(
300 [
301 'success' => true,
302 'password' => $password,
303 'app_password' => [
304 'uuid' => $item['uuid'],
305 'name' => $label,
306 'created' => isset( $item['created'] ) ? (int) $item['created'] : time(),
307 ],
308 ]
309 );
310 }
311
312 /**
313 * Revoke a WPFunnels-scoped application password.
314 *
315 * Scoped to the current user and to entries carrying the WPFunnels name
316 * prefix, so this manager cannot revoke a password created by another plugin
317 * or by the user's normal profile screen.
318 *
319 * @param WP_REST_Request $request Request.
320 * @return WP_REST_Response|WP_Error
321 */
322 public function delete_app_password( $request ) {
323 $user_id = get_current_user_id();
324 $uuid = sanitize_text_field( (string) $request->get_param( 'uuid' ) );
325
326 $existing = WP_Application_Passwords::get_user_application_password( $user_id, $uuid );
327 if ( ! $existing || empty( $existing['name'] ) || 0 !== strpos( $existing['name'], self::APP_PASSWORD_PREFIX ) ) {
328 return new WP_Error(
329 'wpfunnels_app_password_not_found',
330 __( 'Application password not found.', 'wpfnl' ),
331 [ 'status' => 404 ]
332 );
333 }
334
335 $deleted = WP_Application_Passwords::delete_application_password( $user_id, $uuid );
336 if ( is_wp_error( $deleted ) ) {
337 return $deleted;
338 }
339
340 return rest_ensure_response(
341 [
342 'success' => (bool) $deleted,
343 'uuid' => $uuid,
344 ]
345 );
346 }
347 }
348