PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.17
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.17
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-custom-pages.php

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

732 lines 29.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tools for Custom HTML Pages Operations
4 *
5 * Provides MCP tools for managing custom HTML pages.
6 *
7 * @package MetaSync
8 * @subpackage MCP_Server/Tools
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 require_once plugin_dir_path(dirname(__FILE__)) . 'class-mcp-tool-base.php';
16
17 /**
18 * Create Custom Page Tool
19 *
20 * Creates a new custom HTML page
21 */
22 class MCP_Tool_Create_Custom_Page extends MCP_Tool_Base {
23
24 public function get_name() {
25 return 'wordpress_create_custom_page';
26 }
27
28 public function get_description() {
29 return 'Create a new custom HTML page with raw HTML content';
30 }
31
32 public function get_input_schema() {
33 return [
34 'type' => 'object',
35 'properties' => [
36 'title' => [
37 'type' => 'string',
38 'description' => 'Page title',
39 ],
40 'slug' => [
41 'type' => 'string',
42 'description' => 'Page slug (URL-friendly name, optional)',
43 ],
44 'html_content' => [
45 'type' => 'string',
46 'description' => 'Raw HTML content for the page',
47 ],
48 'status' => [
49 'type' => 'string',
50 'enum' => ['publish', 'draft', 'pending'],
51 'description' => 'Page status (default: publish)',
52 ],
53 'enable_raw_html' => [
54 'type' => 'boolean',
55 'description' => 'Enable raw HTML mode to bypass theme (default: true)',
56 ],
57 'filename' => [
58 'type' => 'string',
59 'description' => 'Original HTML filename for reference (optional)',
60 ],
61 ],
62 'required' => ['title', 'html_content'],
63 ];
64 }
65
66 public function execute($params) {
67 $this->validate_params($params);
68 $this->require_capability('edit_pages');
69
70 // Load custom pages class constants
71 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages.php';
72
73 // Prepare page data
74 $page_data = [
75 'post_title' => sanitize_text_field($params['title']),
76 'post_type' => 'page',
77 'post_status' => isset($params['status']) ? sanitize_text_field($params['status']) : 'publish',
78 'post_content' => '', // HTML stored in meta
79 ];
80
81 // Set slug if provided
82 if (!empty($params['slug'])) {
83 $page_data['post_name'] = sanitize_title($params['slug']);
84
85 // Check if page with same slug already exists
86 $existing_page = get_page_by_path($page_data['post_name'], OBJECT, 'page');
87 if ($existing_page) {
88 throw new Exception('A page with this slug already exists');
89 }
90 }
91
92 // Insert the page
93 $page_id = wp_insert_post($page_data, true);
94
95 if (is_wp_error($page_id)) {
96 throw new Exception('Failed to create page: ' . $page_id->get_error_message());
97 }
98
99 // Mark as custom HTML page
100 update_post_meta($page_id, Metasync_Custom_Pages::META_IS_CUSTOM_HTML_PAGE, '1');
101
102 // Mark as created via API
103 update_post_meta($page_id, Metasync_Custom_Pages::META_CREATED_VIA_API, '1');
104
105 // Enable raw HTML mode
106 $enable_raw_html = isset($params['enable_raw_html']) ? (bool)$params['enable_raw_html'] : true;
107 update_post_meta($page_id, Metasync_Custom_Pages::META_HTML_ENABLED, $enable_raw_html ? '1' : '0');
108
109 // Store HTML content (no sanitization - admin users are trusted)
110 update_post_meta($page_id, Metasync_Custom_Pages::META_HTML_CONTENT, wp_unslash($params['html_content']));
111
112 // Store filename if provided
113 if (!empty($params['filename'])) {
114 update_post_meta($page_id, Metasync_Custom_Pages::META_HTML_FILENAME, sanitize_file_name($params['filename']));
115 }
116
117 // Clear cache
118 wp_cache_delete($page_id, 'posts');
119 wp_cache_delete($page_id, 'post_meta');
120
121 return $this->success([
122 'page_id' => $page_id,
123 'title' => get_the_title($page_id),
124 'slug' => get_post_field('post_name', $page_id),
125 'url' => get_permalink($page_id),
126 'edit_url' => get_edit_post_link($page_id, 'raw'),
127 'status' => get_post_status($page_id),
128 'raw_html_enabled' => $enable_raw_html,
129 'html_length' => strlen($params['html_content']),
130 'created_at' => get_post_field('post_date', $page_id),
131 'message' => 'Custom HTML page created successfully',
132 ]);
133 }
134 }
135
136 /**
137 * Get Custom Page Tool
138 *
139 * Retrieves a custom HTML page
140 */
141 class MCP_Tool_Get_Custom_Page extends MCP_Tool_Base {
142
143 public function get_name() {
144 return 'wordpress_get_custom_page';
145 }
146
147 public function get_description() {
148 return 'Get a custom HTML page by ID or slug';
149 }
150
151 public function get_input_schema() {
152 return [
153 'type' => 'object',
154 'properties' => [
155 'page_id' => [
156 'type' => 'integer',
157 'description' => 'Page ID',
158 ],
159 'slug' => [
160 'type' => 'string',
161 'description' => 'Page slug',
162 ],
163 ],
164 ];
165 }
166
167 public function execute($params) {
168 $this->validate_params($params);
169 $this->require_capability('edit_pages');
170
171 // Load custom pages class constants
172 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages.php';
173
174 $page_id = isset($params['page_id']) ? intval($params['page_id']) : null;
175 $slug = isset($params['slug']) ? sanitize_text_field($params['slug']) : null;
176
177 if (empty($page_id) && empty($slug)) {
178 throw new Exception('Either page_id or slug must be provided');
179 }
180
181 // Get page by ID or slug
182 if (!empty($page_id)) {
183 $page = get_post($page_id);
184 } else {
185 $page = get_page_by_path($slug, OBJECT, 'page');
186 }
187
188 if (!$page || $page->post_type !== 'page') {
189 throw new Exception('Page not found');
190 }
191
192 // Verify it's a custom HTML page
193 $is_custom_html_page = get_post_meta($page->ID, Metasync_Custom_Pages::META_IS_CUSTOM_HTML_PAGE, true);
194 if ($is_custom_html_page !== '1') {
195 throw new Exception('This page is not a custom HTML page');
196 }
197
198 // Get HTML content and metadata
199 $html_content = get_post_meta($page->ID, Metasync_Custom_Pages::META_HTML_CONTENT, true);
200 $html_enabled = get_post_meta($page->ID, Metasync_Custom_Pages::META_HTML_ENABLED, true);
201 $html_filename = get_post_meta($page->ID, Metasync_Custom_Pages::META_HTML_FILENAME, true);
202
203 return $this->success([
204 'page_id' => $page->ID,
205 'title' => $page->post_title,
206 'slug' => $page->post_name,
207 'url' => get_permalink($page->ID),
208 'edit_url' => get_edit_post_link($page->ID, 'raw'),
209 'status' => $page->post_status,
210 'raw_html_enabled' => $html_enabled === '1',
211 'html_content' => $html_content,
212 'html_filename' => $html_filename,
213 'html_length' => strlen($html_content),
214 'created_at' => $page->post_date,
215 'updated_at' => $page->post_modified,
216 ]);
217 }
218 }
219
220 /**
221 * List Custom Pages Tool
222 *
223 * Lists all custom HTML pages
224 */
225 class MCP_Tool_List_Custom_Pages extends MCP_Tool_Base {
226
227 public function get_name() {
228 return 'wordpress_list_custom_pages';
229 }
230
231 public function get_description() {
232 return 'List all custom HTML pages';
233 }
234
235 public function get_input_schema() {
236 return [
237 'type' => 'object',
238 'properties' => [
239 'status' => [
240 'type' => 'string',
241 'enum' => ['publish', 'draft', 'pending', 'all'],
242 'description' => 'Filter by page status (default: all)',
243 ],
244 'per_page' => [
245 'type' => 'integer',
246 'description' => 'Number of pages per page (default: 20, max: 100)',
247 ],
248 'page' => [
249 'type' => 'integer',
250 'description' => 'Page number, for pagination (default: 1)',
251 ],
252 ],
253 ];
254 }
255
256 public function execute($params) {
257 $this->validate_params($params);
258 $this->require_capability('edit_pages');
259
260 // Load custom pages class
261 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages.php';
262
263 // Bound the result set. The previous implementation loaded EVERY custom
264 // page along with each page's full HTML body into one array, which could
265 // exhaust PHP memory on sites with many/large pages.
266 $per_page = isset($params['per_page']) ? min(max(intval($params['per_page']), 1), 100) : 20;
267 $page_num = isset($params['page']) ? max(intval($params['page']), 1) : 1;
268
269 // Build query args
270 $args = [
271 'posts_per_page' => $per_page,
272 'paged' => $page_num,
273 ];
274 if (isset($params['status']) && $params['status'] !== 'all') {
275 $args['post_status'] = sanitize_text_field($params['status']);
276 }
277
278 $pages = Metasync_Custom_Pages::get_custom_pages($args);
279
280 $pages_data = [];
281 foreach ($pages as $page) {
282 $html_enabled = get_post_meta($page->ID, Metasync_Custom_Pages::META_HTML_ENABLED, true);
283 $html_content = get_post_meta($page->ID, Metasync_Custom_Pages::META_HTML_CONTENT, true);
284 $html_filename = get_post_meta($page->ID, Metasync_Custom_Pages::META_HTML_FILENAME, true);
285
286 $pages_data[] = [
287 'page_id' => $page->ID,
288 'title' => $page->post_title,
289 'slug' => $page->post_name,
290 'url' => get_permalink($page->ID),
291 'edit_url' => get_edit_post_link($page->ID, 'raw'),
292 'status' => $page->post_status,
293 'raw_html_enabled' => $html_enabled === '1',
294 'html_filename' => $html_filename,
295 'html_length' => strlen($html_content),
296 'created_at' => $page->post_date,
297 'updated_at' => $page->post_modified,
298 ];
299
300 // Free the full HTML body each iteration so a page of large pages can't
301 // accumulate their bodies in memory.
302 unset($html_content);
303 }
304 unset($pages);
305
306 // Count total matching pages cheaply (single hydrated row + found_posts),
307 // mirroring get_custom_pages()'s default statuses and meta filter.
308 $total = (int) (new WP_Query([
309 'post_type' => 'page',
310 'post_status' => isset($args['post_status']) ? $args['post_status'] : ['publish', 'draft', 'pending'],
311 'posts_per_page' => 1,
312 'fields' => 'ids',
313 'no_found_rows' => false,
314 'update_post_meta_cache' => false,
315 'update_post_term_cache' => false,
316 'meta_query' => [
317 [
318 'key' => Metasync_Custom_Pages::META_IS_CUSTOM_HTML_PAGE,
319 'value' => '1',
320 'compare' => '=',
321 ],
322 ],
323 ]))->found_posts;
324
325 return $this->success([
326 'count' => count($pages_data),
327 'total' => $total,
328 'page' => $page_num,
329 'per_page' => $per_page,
330 'total_pages' => $per_page > 0 ? (int) ceil($total / $per_page) : 1,
331 'pages' => $pages_data,
332 ]);
333 }
334 }
335
336 /**
337 * Update Custom Page Tool
338 *
339 * Updates an existing custom HTML page
340 */
341 class MCP_Tool_Update_Custom_Page extends MCP_Tool_Base {
342
343 public function get_name() {
344 return 'wordpress_update_custom_page';
345 }
346
347 public function get_description() {
348 return 'Update an existing custom HTML page';
349 }
350
351 public function get_input_schema() {
352 return [
353 'type' => 'object',
354 'properties' => [
355 'page_id' => [
356 'type' => 'integer',
357 'description' => 'Page ID to update',
358 ],
359 'title' => [
360 'type' => 'string',
361 'description' => 'Page title (optional)',
362 ],
363 'slug' => [
364 'type' => 'string',
365 'description' => 'Page slug (optional)',
366 ],
367 'html_content' => [
368 'type' => 'string',
369 'description' => 'Raw HTML content (optional)',
370 ],
371 'status' => [
372 'type' => 'string',
373 'enum' => ['publish', 'draft', 'pending'],
374 'description' => 'Page status (optional)',
375 ],
376 'enable_raw_html' => [
377 'type' => 'boolean',
378 'description' => 'Enable raw HTML mode (optional)',
379 ],
380 'filename' => [
381 'type' => 'string',
382 'description' => 'HTML filename (optional)',
383 ],
384 ],
385 'required' => ['page_id'],
386 ];
387 }
388
389 public function execute($params) {
390 $this->validate_params($params);
391 $this->require_capability('edit_pages');
392
393 // Load custom pages class constants
394 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages.php';
395
396 $page_id = intval($params['page_id']);
397
398 // Verify page exists
399 $page = get_post($page_id);
400 if (!$page || $page->post_type !== 'page') {
401 throw new Exception(sprintf("Page not found with ID: %d", absint($page_id)));
402 }
403
404 // Verify it's a custom HTML page
405 $is_custom_html_page = get_post_meta($page_id, Metasync_Custom_Pages::META_IS_CUSTOM_HTML_PAGE, true);
406 if ($is_custom_html_page !== '1') {
407 throw new Exception('This page is not a custom HTML page');
408 }
409
410 // Update page data if provided
411 $update_data = ['ID' => $page_id];
412
413 if (isset($params['title'])) {
414 $update_data['post_title'] = sanitize_text_field($params['title']);
415 }
416
417 if (isset($params['slug'])) {
418 $update_data['post_name'] = sanitize_title($params['slug']);
419 }
420
421 if (isset($params['status'])) {
422 $update_data['post_status'] = sanitize_text_field($params['status']);
423 }
424
425 // Update page if there are changes
426 if (count($update_data) > 1) {
427 $result = wp_update_post($update_data, true);
428 if (is_wp_error($result)) {
429 throw new Exception('Failed to update page: ' . $result->get_error_message());
430 }
431 }
432
433 // Update HTML content if provided
434 if (isset($params['html_content'])) {
435 update_post_meta($page_id, Metasync_Custom_Pages::META_HTML_CONTENT, wp_unslash($params['html_content']));
436 }
437
438 // Update raw HTML mode if provided
439 if (isset($params['enable_raw_html'])) {
440 $enable_raw_html = (bool)$params['enable_raw_html'];
441 update_post_meta($page_id, Metasync_Custom_Pages::META_HTML_ENABLED, $enable_raw_html ? '1' : '0');
442 }
443
444 // Update filename if provided
445 if (isset($params['filename'])) {
446 update_post_meta($page_id, Metasync_Custom_Pages::META_HTML_FILENAME, sanitize_file_name($params['filename']));
447 }
448
449 // Clear cache
450 wp_cache_delete($page_id, 'posts');
451 wp_cache_delete($page_id, 'post_meta');
452
453 return $this->success([
454 'page_id' => $page_id,
455 'title' => get_the_title($page_id),
456 'slug' => get_post_field('post_name', $page_id),
457 'url' => get_permalink($page_id),
458 'edit_url' => get_edit_post_link($page_id, 'raw'),
459 'status' => get_post_status($page_id),
460 'raw_html_enabled' => get_post_meta($page_id, Metasync_Custom_Pages::META_HTML_ENABLED, true) === '1',
461 'html_length' => strlen(get_post_meta($page_id, Metasync_Custom_Pages::META_HTML_CONTENT, true)),
462 'updated_at' => get_post_field('post_modified', $page_id),
463 'message' => 'Custom HTML page updated successfully',
464 ]);
465 }
466 }
467
468 /**
469 * Delete Custom Page Tool
470 *
471 * Deletes a custom HTML page
472 */
473 class MCP_Tool_Delete_Custom_Page extends MCP_Tool_Base {
474
475 public function get_name() {
476 return 'wordpress_delete_custom_page';
477 }
478
479 public function get_description() {
480 return 'Delete a custom HTML page permanently';
481 }
482
483 public function get_input_schema() {
484 return [
485 'type' => 'object',
486 'properties' => [
487 'page_id' => [
488 'type' => 'integer',
489 'description' => 'Page ID to delete',
490 ],
491 ],
492 'required' => ['page_id'],
493 ];
494 }
495
496 public function execute($params) {
497 $this->validate_params($params);
498 $this->require_capability('delete_pages');
499
500 // Delegate to the shared REST/MCP delete implementation so both paths
501 // perform identical asset cleanup and front-page reset.
502 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages.php';
503 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages-api.php';
504
505 $page_id = intval($params['page_id']);
506
507 $api = new Metasync_Custom_Pages_API();
508 $result = $api->delete_custom_page_with_cleanup($page_id);
509
510 if (is_wp_error($result)) {
511 throw new Exception($result->get_error_message());
512 }
513
514 return $this->success(array_merge($result, [
515 'message' => 'Custom HTML page deleted successfully',
516 ]));
517 }
518 }
519
520 /**
521 * Import LPS Page Tool
522 *
523 * Imports an LPS (Landing Page Studio) ZIP export into WordPress as a custom
524 * HTML page, extracting bundled assets to wp-content/uploads/metasync-pages/{slug}/.
525 * Re-importing with the same slug overwrites the previous page and assets.
526 */
527 class MCP_Tool_Import_LPS_Page extends MCP_Tool_Base {
528
529 public function get_name() {
530 return 'wordpress_import_lps_page';
531 }
532
533 public function get_description() {
534 return 'Import an LPS (Landing Page Studio) ZIP export into WordPress as a custom HTML page, extracting assets to uploads/metasync-pages/{slug}/';
535 }
536
537 public function get_input_schema() {
538 return [
539 'type' => 'object',
540 'properties' => [
541 'download_url' => [
542 'type' => 'string',
543 'description' => 'URL to download the LPS ZIP from.',
544 ],
545 'title' => [
546 'type' => 'string',
547 'description' => 'Page title (single-page imports only). Ignored for multi-page bundles, which take titles from pages.manifest.json.',
548 ],
549 'slug' => [
550 'type' => 'string',
551 'description' => 'URL slug (single-page imports only). Required when the ZIP has no pages.manifest.json; ignored for multi-page bundles. Re-importing with the same slug overwrites the previous LPS page.',
552 ],
553 'assets_folder' => [
554 'type' => 'string',
555 'description' => 'On-disk folder name the bundle is extracted to (under uploads/metasync-pages/). This is the path LPS bakes into the asset URLs at build time and is intentionally separate from the slug. Defaults to the slug when omitted.',
556 ],
557 'external_ref' => [
558 'type' => 'string',
559 'description' => 'LPS project UUID (stable per-project identifier). When provided, used as the primary home-page dedup key instead of assets_folder.',
560 ],
561 'overwrite' => [
562 'type' => 'boolean',
563 'description' => 'When true (default), an existing page with the same slug is overwritten.',
564 ],
565 'status' => [
566 'type' => 'string',
567 'enum' => ['publish', 'draft', 'pending'],
568 'description' => 'Page status (default: publish)',
569 ],
570 ],
571 'required' => ['download_url'],
572 ];
573 }
574
575 public function execute($params) {
576 $this->validate_params($params);
577 $this->require_capability('edit_pages');
578
579 if (empty($params['download_url'])) {
580 throw new Exception('download_url is required.');
581 }
582
583 $raw_slug = isset($params['slug']) ? $params['slug'] : '';
584 if (is_string($raw_slug) && (strpos($raw_slug, '..') !== false || strpos($raw_slug, '/') !== false || strpos($raw_slug, '\\') !== false)) {
585 throw new Exception('Slug must not contain path separators or parent references.');
586 }
587
588 $tmp_file = null;
589 $max_zip_bytes = 50 * 1024 * 1024;
590
591 // --- Audit tracking (one persistent record on success and failure paths) ---
592 $_lps_start_ms = (int) round(microtime(true) * 1000);
593 $_lps_success = false;
594 $_lps_result_data = null;
595 $_lps_exc = null;
596 $_lps_err_status = 0;
597 $_lps_downloaded = 0;
598 $_lps_af = isset($params['assets_folder']) ? $params['assets_folder'] : (isset($params['slug']) ? $params['slug'] : '');
599 $_lps_external_ref = isset($params['external_ref']) ? sanitize_text_field($params['external_ref']) : '';
600
601 try {
602 // wp_tempnam() lives in wp-admin/includes/file.php, which is NOT loaded
603 // during a normal REST/MCP request — load it so this works in any context.
604 if (!function_exists('wp_tempnam')) {
605 require_once ABSPATH . 'wp-admin/includes/file.php';
606 }
607 $tmp_file = wp_tempnam('lps_import_');
608 if (!$tmp_file) {
609 throw new Exception('Failed to allocate a temporary file for the ZIP.');
610 }
611
612 $response = wp_safe_remote_get($params['download_url'], [
613 'timeout' => 60,
614 'stream' => true,
615 'filename' => $tmp_file,
616 ]);
617 if (is_wp_error($response)) {
618 throw new Exception('Failed to download ZIP: ' . $response->get_error_message());
619 }
620 $code = wp_remote_retrieve_response_code($response);
621 if ((int) $code !== 200) {
622 throw new Exception('ZIP download returned HTTP ' . intval($code));
623 }
624 $content_type = strtolower((string) wp_remote_retrieve_header($response, 'content-type'));
625 if ($content_type !== '' && strpos($content_type, 'zip') === false && strpos($content_type, 'octet-stream') === false) {
626 throw new Exception('Downloaded ZIP has unexpected Content-Type: ' . $content_type);
627 }
628 $downloaded_size = file_exists($tmp_file) ? filesize($tmp_file) : 0;
629 $_lps_downloaded = $downloaded_size;
630 if ($downloaded_size === 0) {
631 throw new Exception('Downloaded ZIP file is empty.');
632 }
633 if ($downloaded_size > $max_zip_bytes) {
634 throw new Exception('Downloaded ZIP exceeds the maximum allowed size (' . intval($max_zip_bytes / 1024 / 1024) . ' MB).');
635 }
636
637 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages.php';
638 require_once plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages-api.php';
639
640 $slug = isset($params['slug']) ? sanitize_title($params['slug']) : '';
641 $title = isset($params['title']) ? sanitize_text_field($params['title']) : '';
642 $status = isset($params['status']) ? sanitize_text_field($params['status']) : 'publish';
643 $overwrite = isset($params['overwrite']) ? (bool) $params['overwrite'] : true;
644 $assets_folder = isset($params['assets_folder']) ? $params['assets_folder'] : '';
645 $external_ref = isset($params['external_ref']) ? sanitize_text_field($params['external_ref']) : '';
646
647 $api = new Metasync_Custom_Pages_API();
648 $result = $api->extract_and_create_lps_page($tmp_file, $slug, $title, $status, $overwrite, $assets_folder, $external_ref);
649
650 if (is_wp_error($result)) {
651 $_lps_err_data = $result->get_error_data();
652 $_lps_err_status = (is_array($_lps_err_data) && isset($_lps_err_data['status'])) ? (int) $_lps_err_data['status'] : 0;
653 throw new Exception($result->get_error_message());
654 }
655
656 // Surface the helper's full data payload — the shape differs for single
657 // (page_id/url/status) vs multi (created/updated/failed) imports.
658 $data = isset($result['data']) && is_array($result['data']) ? $result['data'] : [];
659 $data['message'] = isset($result['message']) ? $result['message'] : 'LPS ZIP imported successfully';
660
661 // Capture for the audit record now so the per-page breakdown survives
662 // even when the all-failed multi-page case throws below.
663 $_lps_result_data = $data;
664
665 // For a multi-page import where EVERY page failed, the helper still
666 // returns a non-WP_Error array. Surface that as a failure to the agent
667 // (mirrors the REST endpoint's 422) instead of reporting false success.
668 if (isset($data['mode']) && $data['mode'] === 'multi') {
669 $succeeded = count(isset($data['created']) ? $data['created'] : [])
670 + count(isset($data['updated']) ? $data['updated'] : []);
671 $failed = isset($data['failed']) ? $data['failed'] : [];
672 if ($succeeded === 0 && count($failed) > 0) {
673 $reasons = array();
674 foreach ($failed as $f) {
675 $reasons[] = (isset($f['slug']) ? $f['slug'] : '?') . ': ' . (isset($f['code']) ? $f['code'] : 'failed');
676 }
677 throw new Exception('LPS import failed for all pages — ' . implode('; ', $reasons));
678 }
679 }
680
681 $_lps_success = true;
682 return $this->success($data);
683 } catch (Exception $e) {
684 $_lps_exc = $e;
685 throw $e;
686 } finally {
687 if (!empty($tmp_file) && file_exists($tmp_file)) {
688 @unlink($tmp_file);
689 }
690
691 // Write exactly one persistent audit record (success or failure),
692 // regardless of WP_DEBUG. Load the API class if an early exception
693 // fired before the require_once above ran.
694 if (!class_exists('Metasync_Custom_Pages_API')) {
695 $api_class_path = plugin_dir_path(dirname(dirname(__FILE__))) . 'custom-pages/class-metasync-custom-pages-api.php';
696 if (file_exists($api_class_path)) {
697 require_once $api_class_path;
698 }
699 }
700 if (class_exists('Metasync_Custom_Pages_API')) {
701 // Derive HTTP status from the captured data when available (covers the
702 // success path AND the all-failed multi-page case that throws → 422).
703 // A null payload means an early exception (download/extract) → 500.
704 if (is_array($_lps_result_data)) {
705 $_s = count(isset($_lps_result_data['created']) ? $_lps_result_data['created'] : array())
706 + count(isset($_lps_result_data['updated']) ? $_lps_result_data['updated'] : array());
707 $_f = count(isset($_lps_result_data['failed']) ? $_lps_result_data['failed'] : array());
708 $_lps_http = ($_f > 0 && $_s > 0) ? 207 : (($_f > 0 && $_s === 0) ? 422 : 200);
709 } else {
710 $_lps_http = $_lps_err_status > 0 ? $_lps_err_status : 500;
711 }
712
713 Metasync_Custom_Pages_API::write_lps_import_audit(array(
714 'result' => is_array($_lps_result_data) ? array('success' => $_lps_success, 'data' => $_lps_result_data) : null,
715 'http_status' => $_lps_http,
716 'input' => array(
717 'source_type' => 'download_url',
718 'zip_size_bytes' => $_lps_downloaded,
719 'assets_folder' => $_lps_af,
720 'external_ref' => $_lps_external_ref,
721 'pages_in_manifest' => ($_lps_success && isset($_lps_result_data['pages_total'])) ? (int) $_lps_result_data['pages_total'] : null,
722 ),
723 'start_ms' => $_lps_start_ms,
724 'auth_method' => 'mcp-capability',
725 'api_key_prefix' => '',
726 'error_message' => $_lps_exc ? $_lps_exc->getMessage() : '',
727 ));
728 }
729 }
730 }
731 }
732