PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / wp-mcp-server / tools / class-mcp-tool-redirects.php

class-mcp-tool-redirects.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.0, at wp-mcp-server/tools/class-mcp-tool-redirects.php

449 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tool: Redirect Management
4 *
5 * Provides tools for managing WordPress redirects.
6 *
7 * @package MetaSync
8 * @subpackage MCP_Server/Tools
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 /**
16 * Create Redirect Tool
17 */
18 class MCP_Tool_Create_Redirect extends MCP_Tool_Base {
19
20 public function get_name() {
21 return 'wordpress_create_redirect';
22 }
23
24 public function get_description() {
25 return 'Create a 301 or 302 redirect from one URL to another';
26 }
27
28 public function get_input_schema() {
29 return [
30 'type' => 'object',
31 'properties' => [
32 'source' => [
33 'type' => 'string',
34 'description' => 'Source URL path (e.g., /old-page or https://example.com/old-page)'
35 ],
36 'destination' => [
37 'type' => 'string',
38 'description' => 'Destination URL (full URL or path)'
39 ],
40 'type' => [
41 'type' => 'integer',
42 'description' => 'Redirect HTTP code',
43 'enum' => [301, 302],
44 'default' => 301
45 ],
46 'description' => [
47 'type' => 'string',
48 'description' => 'Optional description of the redirect',
49 'default' => ''
50 ]
51 ],
52 'required' => ['source', 'destination']
53 ];
54 }
55
56 public function execute($params) {
57 $this->validate_params($params);
58 $this->require_capability('manage_options');
59
60 // Load database class
61 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection-database.php';
62 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection-validator.php';
63 $db = new Metasync_Redirection_Database();
64
65 $source = $this->sanitize_url($params['source']);
66 $destination = $this->sanitize_url($params['destination']);
67 $http_code = isset($params['type']) ? $this->sanitize_integer($params['type']) : 301;
68 $description = isset($params['description']) ? $this->sanitize_textarea($params['description']) : '';
69
70 if (!empty($destination) && !Metasync_Redirection_Validator::is_safe_destination_syntax($destination)) {
71 // Backslashes and protocol-relative hosts bypass host validation in
72 // browsers while looking internal to wp_validate_redirect.
73 return [
74 'error' => 'invalid_destination_syntax',
75 'message' => 'Destination URL contains invalid characters (backslashes or protocol-relative hosts are not accepted).',
76 ];
77 }
78 if (!get_option('metasync_allow_external_redirects', 0) && !empty($destination) && wp_validate_redirect($destination, '') !== $destination) {
79 return [
80 'error' => 'external_destination',
81 'message' => 'Destination URL must be on this site. Enable "Allow External Redirects" in plugin settings to permit off-site redirects.',
82 ];
83 }
84
85 // Parse source to get path
86 $source_path = parse_url($source, PHP_URL_PATH) ?: $source;
87
88 // Loop detection — refuse to create a redirect whose chain resolves back to the source
89 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection.php';
90 $db_ref = $db;
91 $redirection_helper = new Metasync_Redirection($db_ref);
92 $loop_chain = [];
93 if ($redirection_helper->would_create_loop($source_path, $destination, $loop_chain)) {
94 return [
95 'error' => 'loop_detected',
96 'chain' => $loop_chain,
97 ];
98 }
99
100 // Create redirect
101 $result = $db->add([
102 'sources_from' => [$source_path],
103 'url_redirect_to' => $destination,
104 'http_code' => $http_code,
105 'description' => $description,
106 'status' => 'active',
107 'pattern_type' => 'exact'
108 ]);
109
110 if ($result === false) {
111 throw new Exception('Failed to create redirect');
112 }
113
114 // Non-blocking destination reachability check — surface a warning, never block creation.
115 $dest_warning = $redirection_helper->destination_resolves_warning($destination, $http_code);
116
117 $response_data = [
118 'redirect_id' => $result,
119 'source' => $source_path,
120 'destination' => $destination,
121 'type' => $http_code,
122 'status' => 'active'
123 ];
124 if ($dest_warning !== null) {
125 $response_data['warning'] = $dest_warning;
126 }
127
128 return $this->success($response_data, 'Redirect created successfully');
129 }
130 }
131
132 /**
133 * List Redirects Tool
134 */
135 class MCP_Tool_List_Redirects extends MCP_Tool_Base {
136
137 public function get_name() {
138 return 'wordpress_list_redirects';
139 }
140
141 public function get_description() {
142 return 'List all redirects with optional status filter';
143 }
144
145 public function get_input_schema() {
146 return [
147 'type' => 'object',
148 'properties' => [
149 'status' => [
150 'type' => 'string',
151 'description' => 'Filter by status',
152 'enum' => ['active', 'inactive', 'all'],
153 'default' => 'active'
154 ],
155 'limit' => [
156 'type' => 'integer',
157 'description' => 'Maximum number of results',
158 'default' => 50,
159 'minimum' => 1,
160 'maximum' => 200
161 ]
162 ],
163 'required' => []
164 ];
165 }
166
167 public function execute($params) {
168 $this->validate_params($params);
169 $this->require_capability('manage_options');
170
171 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection-database.php';
172 $db = new Metasync_Redirection_Database();
173
174 $status = isset($params['status']) ? $params['status'] : 'active';
175
176 // Get redirects
177 if ($status === 'active') {
178 $redirects = $db->getAllActiveRecords();
179 } else {
180 $redirects = $db->getAllRecords();
181 }
182
183 // Filter by status if needed
184 if ($status !== 'all' && $status !== 'active') {
185 $redirects = array_filter($redirects, function($r) use ($status) {
186 return $r->status === $status;
187 });
188 }
189
190 // Limit results
191 $limit = isset($params['limit']) ? $this->sanitize_integer($params['limit']) : 50;
192 $redirects = array_slice($redirects, 0, $limit);
193
194 // Format results (sources_from may be JSON or legacy serialized in DB)
195 $result = array_map(function($redirect) {
196 $sources_raw = $redirect->sources_from;
197 $source = null;
198 if ( ! empty( $sources_raw ) ) {
199 // Try JSON first; fall back to unserialize for legacy DB records
200 $sources = json_decode( $sources_raw, true );
201 if ( $sources === null && $sources_raw !== 'null' ) {
202 $sources = unserialize( $sources_raw, ['allowed_classes' => false] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
203 }
204 if ( is_array( $sources ) && ! empty( $sources ) ) {
205 $keys = array_keys( $sources );
206 $source = is_int( $keys[0] ) ? $sources[ $keys[0] ] : $keys[0];
207 }
208 }
209 return [
210 'id' => $redirect->id,
211 'source' => $source,
212 'destination' => $redirect->url_redirect_to,
213 'type' => $redirect->http_code,
214 'hits' => $redirect->hits_count,
215 'status' => $redirect->status,
216 'pattern_type' => $redirect->pattern_type ?? 'exact',
217 'description' => $redirect->description ?? '',
218 'created_at' => $redirect->created_at ?? null
219 ];
220 }, $redirects);
221
222 return $this->success([
223 'redirects' => $result,
224 'total' => count($result)
225 ]);
226 }
227 }
228
229 /**
230 * Delete Redirect Tool
231 */
232 class MCP_Tool_Delete_Redirect extends MCP_Tool_Base {
233
234 public function get_name() {
235 return 'wordpress_delete_redirect';
236 }
237
238 public function get_description() {
239 return 'Delete a redirect by ID';
240 }
241
242 public function get_input_schema() {
243 return [
244 'type' => 'object',
245 'properties' => [
246 'redirect_id' => [
247 'type' => 'integer',
248 'description' => 'Redirect ID to delete',
249 'minimum' => 1
250 ]
251 ],
252 'required' => ['redirect_id']
253 ];
254 }
255
256 public function execute($params) {
257 $this->validate_params($params);
258 $this->require_capability('manage_options');
259
260 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection-database.php';
261 $db = new Metasync_Redirection_Database();
262
263 $redirect_id = $this->sanitize_integer($params['redirect_id']);
264
265 // Verify redirect exists
266 $redirect = $db->find($redirect_id);
267 if (!$redirect) {
268 throw new Exception(sprintf("Redirect not found: %d", absint($redirect_id)));
269 }
270
271 // Delete redirect
272 $result = $db->delete([$redirect_id]);
273
274 if ($result === false) {
275 throw new Exception('Failed to delete redirect');
276 }
277
278 return $this->success([
279 'redirect_id' => $redirect_id,
280 'deleted' => true
281 ], 'Redirect deleted successfully');
282 }
283 }
284
285 /**
286 * Update Redirect Tool
287 */
288 class MCP_Tool_Update_Redirect extends MCP_Tool_Base {
289
290 public function get_name() {
291 return 'wordpress_update_redirect';
292 }
293
294 public function get_description() {
295 return 'Update an existing redirect';
296 }
297
298 public function get_input_schema() {
299 return [
300 'type' => 'object',
301 'properties' => [
302 'redirect_id' => [
303 'type' => 'integer',
304 'description' => 'Redirect ID to update',
305 'minimum' => 1
306 ],
307 'destination' => [
308 'type' => 'string',
309 'description' => 'New destination URL (optional)'
310 ],
311 'type' => [
312 'type' => 'integer',
313 'description' => 'New redirect HTTP code (optional)',
314 'enum' => [301, 302]
315 ],
316 'status' => [
317 'type' => 'string',
318 'description' => 'New status (optional)',
319 'enum' => ['active', 'inactive']
320 ]
321 ],
322 'required' => ['redirect_id']
323 ];
324 }
325
326 public function execute($params) {
327 $this->validate_params($params);
328 $this->require_capability('manage_options');
329
330 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection-database.php';
331 $db = new Metasync_Redirection_Database();
332
333 $redirect_id = $this->sanitize_integer($params['redirect_id']);
334
335 // Verify redirect exists
336 $redirect = $db->find($redirect_id);
337 if (!$redirect) {
338 throw new Exception(sprintf("Redirect not found: %d", absint($redirect_id)));
339 }
340
341 // Build update args
342 if (!class_exists('Metasync_Redirection_Validator')) {
343 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection-validator.php';
344 }
345 $update_args = ['updated_at' => current_time('mysql')];
346 // Declared up front so the later loop-guard and reachability blocks —
347 // which run under the same isset() condition — have a defined value.
348 $destination = '';
349
350 if (isset($params['destination'])) {
351 $destination = $this->sanitize_url($params['destination']);
352 if (!empty($destination) && !Metasync_Redirection_Validator::is_safe_destination_syntax($destination)) {
353 return [
354 'error' => 'invalid_destination_syntax',
355 'message' => 'Destination URL contains invalid characters (backslashes or protocol-relative hosts are not accepted).',
356 ];
357 }
358 if (!get_option('metasync_allow_external_redirects', 0) && !empty($destination) && wp_validate_redirect($destination, '') !== $destination) {
359 return [
360 'error' => 'external_destination',
361 'message' => 'Destination URL must be on this site. Enable "Allow External Redirects" in plugin settings to permit off-site redirects.',
362 ];
363 }
364 $update_args['url_redirect_to'] = $destination;
365 }
366
367 if (isset($params['type'])) {
368 $update_args['http_code'] = $this->sanitize_integer($params['type']);
369 }
370
371 if (isset($params['status'])) {
372 $update_args['status'] = $this->sanitize_string($params['status']);
373 }
374
375 // Loop guard, mirroring the create tool: updating A→B while B→A
376 // already exists wires up an instant ping-pong. 410/451 send no
377 // Location header, so they are exempt.
378 if (isset($params['destination'])) {
379 $effective_code = isset($update_args['http_code']) ? (int) $update_args['http_code'] : (int) $redirect->http_code;
380 if (!in_array($effective_code, [410, 451], true)) {
381 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection.php';
382 $db_ref = $db;
383 $redirection_helper = new Metasync_Redirection($db_ref);
384 $stored_sources = json_decode((string) $redirect->sources_from, true);
385 if ($stored_sources === null && (string) $redirect->sources_from !== 'null') {
386 $stored_sources = unserialize($redirect->sources_from, ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
387 }
388 if (is_array($stored_sources)) {
389 foreach ($stored_sources as $stored_key => $stored_value) {
390 $source = is_int($stored_key) ? (string) $stored_value : (string) $stored_key;
391 $loop_chain = [];
392 if ($redirection_helper->would_create_loop($source, $destination, $loop_chain)) {
393 return [
394 'error' => 'loop_detected',
395 'chain' => $loop_chain,
396 ];
397 }
398 }
399 }
400 }
401 }
402
403 // Update redirect
404 $result = $db->update($update_args, $redirect_id);
405
406 if ($result === false) {
407 throw new Exception('Failed to update redirect');
408 }
409
410 // Non-blocking destination reachability check — surface a warning, never block the update.
411 $dest_warning = null;
412 if (isset($params['destination'])) {
413 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'redirections/class-metasync-redirection.php';
414 $db_ref = $db;
415 $effective_code = isset($update_args['http_code']) ? (int) $update_args['http_code'] : (int) $redirect->http_code;
416 $effective_pattern = isset($redirect->pattern_type) ? $redirect->pattern_type : 'exact';
417 $dest_warning = (new Metasync_Redirection($db_ref))->destination_resolves_warning($destination, $effective_code, $effective_pattern);
418 }
419
420 // Get updated redirect (sources_from may be JSON or legacy serialized in DB)
421 $updated = $db->find($redirect_id);
422 $source = null;
423 if ( ! empty( $updated->sources_from ) ) {
424 // Try JSON first; fall back to unserialize for legacy DB records
425 $sources = json_decode( $updated->sources_from, true );
426 if ( $sources === null && $updated->sources_from !== 'null' ) {
427 $sources = unserialize( $updated->sources_from, ['allowed_classes' => false] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
428 }
429 if ( is_array( $sources ) && ! empty( $sources ) ) {
430 $keys = array_keys( $sources );
431 $source = is_int( $keys[0] ) ? $sources[ $keys[0] ] : $keys[0];
432 }
433 }
434
435 $success_data = [
436 'redirect_id' => $redirect_id,
437 'source' => $source,
438 'destination' => $updated->url_redirect_to,
439 'type' => $updated->http_code,
440 'status' => $updated->status
441 ];
442 if ($dest_warning !== null) {
443 $success_data['warning'] = $dest_warning;
444 }
445
446 return $this->success($success_data, 'Redirect updated successfully');
447 }
448 }
449