| 1 |
<?php |
| 2 |
/** |
| 3 |
* Ability: Get or create a live preview URL for a local WDesignKit widget. |
| 4 |
*/ |
| 5 |
|
| 6 |
declare(strict_types=1); |
| 7 |
|
| 8 |
if (!defined('ABSPATH')) { |
| 9 |
exit(); |
| 10 |
} |
| 11 |
|
| 12 |
wp_register_ability('wdesignkit/widget-preview', [ |
| 13 |
'label' => __('WDesignKit Widget Preview', 'wdesignkit'), |
| 14 |
'description' => __( |
| 15 |
'Creates (or reuses) a temporary WordPress page to preview a widget and returns its editor URL. For Elementor widgets the page opens in the Elementor canvas editor. For Gutenberg/gutenberg_core widgets the page opens in the block editor. No page is created for builder types other than those two.', |
| 16 |
'wdesignkit', |
| 17 |
), |
| 18 |
'category' => 'wdesignkit', |
| 19 |
'input_schema' => [ |
| 20 |
'type' => 'object', |
| 21 |
'properties' => [ |
| 22 |
'builder' => [ |
| 23 |
'type' => 'string', |
| 24 |
'description' => 'Builder type.', |
| 25 |
'enum' => ['elementor', 'gutenberg', 'gutenberg_core'], |
| 26 |
], |
| 27 |
'folder' => [ |
| 28 |
'type' => 'string', |
| 29 |
'description' => 'Widget folder name (from wdesignkit/list-widgets).', |
| 30 |
], |
| 31 |
'page_data' => [ |
| 32 |
'type' => 'string', |
| 33 |
'description' => 'Builder-specific page data token: for Elementor this is the widgetType slug; for Gutenberg this is the block name suffix (the part after "wdkit/").', |
| 34 |
], |
| 35 |
], |
| 36 |
'required' => ['builder', 'folder'], |
| 37 |
'additionalProperties' => false, |
| 38 |
], |
| 39 |
'output_schema' => [ |
| 40 |
'type' => 'object', |
| 41 |
'properties' => [ |
| 42 |
'success' => ['type' => 'boolean'], |
| 43 |
'message' => ['type' => 'string'], |
| 44 |
'preview_url' => ['type' => 'string'], |
| 45 |
'page_id' => ['type' => 'integer'], |
| 46 |
], |
| 47 |
], |
| 48 |
'execute_callback' => 'wdesignkit_mcp_widget_preview', |
| 49 |
'permission_callback' => 'wdesignkit_mcp_permission_callback', |
| 50 |
'meta' => [ |
| 51 |
'public' => true, |
| 52 |
'show_in_rest' => true, |
| 53 |
'mcp' => ['public' => true], |
| 54 |
'annotations' => [ |
| 55 |
'instructions' => implode("\n", [ |
| 56 |
'Returns an admin URL where the user can preview the widget live.', |
| 57 |
'No cloud login required — this is a local WP operation.', |
| 58 |
'A temporary preview page is created once per widget_id (or reused if one already exists).', |
| 59 |
'page_data is the widgetType string for Elementor and the block suffix for Gutenberg.', |
| 60 |
'Get widget_id and widget_name from wdesignkit/get-widget (check widget.config.widget_id and widget.config.name).', |
| 61 |
]), |
| 62 |
'readonly' => false, |
| 63 |
'destructive' => false, |
| 64 |
'idempotent' => true, |
| 65 |
], |
| 66 |
], |
| 67 |
]); |
| 68 |
|
| 69 |
function wdesignkit_mcp_widget_preview(array $input): array { |
| 70 |
if (!defined('WDKIT_BUILDER_PATH')) { |
| 71 |
return ['success' => false, 'message' => 'WDesignKit plugin is not active.']; |
| 72 |
} |
| 73 |
|
| 74 |
$builder = sanitize_key((string) ($input['builder'] ?? '')); |
| 75 |
$folder = sanitize_file_name((string) ($input['folder'] ?? '')); |
| 76 |
$page_data = sanitize_key((string) ($input['page_data'] ?? '')); |
| 77 |
|
| 78 |
if (!in_array($builder, ['elementor', 'gutenberg', 'gutenberg_core'], true)) { |
| 79 |
return ['success' => false, 'message' => 'builder must be "elementor", "gutenberg", or "gutenberg_core".']; |
| 80 |
} |
| 81 |
|
| 82 |
if ($folder === '') { |
| 83 |
return ['success' => false, 'message' => 'folder is required.']; |
| 84 |
} |
| 85 |
|
| 86 |
// gutenberg_core widgets live under their own gutenberg_core/ directory — create-widget, |
| 87 |
// list-widgets, and the gutenberg_core loader all use the raw builder name. Resolve the |
| 88 |
// folder under the actual builder dir; collapsing gutenberg_core → gutenberg made the |
| 89 |
// lookup fail with "Widget folder not found" for every Core Gutenberg widget. (bug: 86d3rc7nf) |
| 90 |
$fs_builder = $builder; |
| 91 |
|
| 92 |
$widget_dir = WDKIT_BUILDER_PATH . '/' . $fs_builder . '/' . $folder; |
| 93 |
|
| 94 |
if (!is_dir($widget_dir)) { |
| 95 |
return ['success' => false, 'message' => "Widget folder not found: {$fs_builder}/{$folder}"]; |
| 96 |
} |
| 97 |
|
| 98 |
// Realpath validation — ensure we're still inside WDKIT_BUILDER_PATH. |
| 99 |
$real_widget = realpath($widget_dir); |
| 100 |
$real_base = realpath(WDKIT_BUILDER_PATH); |
| 101 |
if (!$real_widget || !$real_base || strpos($real_widget, $real_base . DIRECTORY_SEPARATOR) !== 0) { |
| 102 |
return ['success' => false, 'message' => 'Invalid widget path.']; |
| 103 |
} |
| 104 |
|
| 105 |
// Read widget metadata from JSON |
| 106 |
$widget_name = $folder; |
| 107 |
$widget_id = ''; |
| 108 |
$files = array_diff(@scandir($widget_dir) ?: [], ['.', '..']); |
| 109 |
foreach ($files as $f) { |
| 110 |
if (pathinfo($f, PATHINFO_EXTENSION) !== 'json') { |
| 111 |
continue; |
| 112 |
} |
| 113 |
$raw = @file_get_contents($widget_dir . '/' . $f); |
| 114 |
$jd = ($raw !== false) ? json_decode($raw, true) : null; |
| 115 |
if (is_array($jd)) { |
| 116 |
$widget_name = $jd['widget_data']['widgetdata']['name'] ?? $folder; |
| 117 |
$widget_id = $jd['widget_data']['widgetdata']['widget_id'] ?? ''; |
| 118 |
} |
| 119 |
break; |
| 120 |
} |
| 121 |
|
| 122 |
if ($widget_id === '') { |
| 123 |
return ['success' => false, 'message' => "Could not read widget_id from {$fs_builder}/{$folder}"]; |
| 124 |
} |
| 125 |
|
| 126 |
if ($page_data === '') { |
| 127 |
// Derive from widget_id as best-effort fallback |
| 128 |
$page_data = $widget_id; |
| 129 |
} |
| 130 |
|
| 131 |
$preview_url = ''; |
| 132 |
$page_id = 0; |
| 133 |
|
| 134 |
if ($builder === 'elementor') { |
| 135 |
if (!class_exists('\Elementor\Plugin')) { |
| 136 |
return ['success' => false, 'message' => 'Elementor is not active. Install and activate Elementor to preview Elementor widgets.']; |
| 137 |
} |
| 138 |
|
| 139 |
$existing = get_posts([ |
| 140 |
'post_type' => 'page', |
| 141 |
'meta_key' => '_wkit_preview_widget_id', |
| 142 |
'meta_value' => $widget_id, |
| 143 |
'posts_per_page' => 1, |
| 144 |
'fields' => 'ids', |
| 145 |
]); |
| 146 |
|
| 147 |
if (!empty($existing)) { |
| 148 |
$page_id = (int) $existing[0]; |
| 149 |
} else { |
| 150 |
$page_id = (int) wp_insert_post([ |
| 151 |
'post_title' => $widget_name . ' - Preview', |
| 152 |
'post_status' => 'publish', |
| 153 |
'post_type' => 'page', |
| 154 |
'post_name' => sanitize_title($widget_id . '-preview'), |
| 155 |
'post_content' => '', |
| 156 |
'meta_input' => [ |
| 157 |
'_is_preview_page' => true, |
| 158 |
'_wkit_preview_widget_id' => $widget_id, |
| 159 |
'_elementor_edit_mode' => 'builder', |
| 160 |
'_wp_page_template' => 'elementor_canvas', |
| 161 |
], |
| 162 |
]); |
| 163 |
} |
| 164 |
|
| 165 |
if (!$page_id) { |
| 166 |
return ['success' => false, 'message' => 'Failed to create preview page.']; |
| 167 |
} |
| 168 |
|
| 169 |
$preview_url = admin_url('post.php?post=' . $page_id . '&action=elementor'); |
| 170 |
|
| 171 |
} elseif (in_array($builder, ['gutenberg', 'gutenberg_core'], true)) { |
| 172 |
$existing = get_posts([ |
| 173 |
'post_type' => 'page', |
| 174 |
'meta_key' => '_wkit_preview_widget_id', |
| 175 |
'meta_value' => $widget_id, |
| 176 |
'posts_per_page' => 1, |
| 177 |
'fields' => 'ids', |
| 178 |
]); |
| 179 |
|
| 180 |
if (!empty($existing)) { |
| 181 |
$page_id = (int) $existing[0]; |
| 182 |
} else { |
| 183 |
$page_id = (int) wp_insert_post([ |
| 184 |
'post_title' => $widget_name . ' - Preview', |
| 185 |
'post_status' => 'publish', |
| 186 |
'post_type' => 'page', |
| 187 |
'post_name' => sanitize_title(wp_unique_post_slug($widget_id . '-preview', 0, 'publish', 'page', 0)), |
| 188 |
'post_content' => '', |
| 189 |
'meta_input' => [ |
| 190 |
'gutenberg_preview' => true, |
| 191 |
'_wkit_preview_widget_id' => $widget_id, |
| 192 |
'_wp_page_template' => 'default', |
| 193 |
], |
| 194 |
]); |
| 195 |
} |
| 196 |
|
| 197 |
if (!$page_id) { |
| 198 |
return ['success' => false, 'message' => 'Failed to create preview page.']; |
| 199 |
} |
| 200 |
|
| 201 |
if (function_exists('serialize_blocks')) { |
| 202 |
// Generated Gutenberg widgets register their block as wdkit/wb-<widget_id> |
| 203 |
// (see gutenberg_file.js / gutenberg_core_file.js). Ensure the wb- prefix so |
| 204 |
// the inserted block matches the registered name and renders instead of |
| 205 |
// showing WordPress's "invalid content" recovery error. |
| 206 |
$block_suffix = (strpos($page_data, 'wb-') === 0) ? $page_data : 'wb-' . $page_data; |
| 207 |
$blocks = [['blockName' => 'wdkit/' . $block_suffix, 'attrs' => [], 'innerHTML' => '', 'innerContent' => []]]; |
| 208 |
$updated_content = serialize_blocks($blocks); |
| 209 |
wp_update_post(['ID' => $page_id, 'post_content' => $updated_content]); |
| 210 |
} |
| 211 |
|
| 212 |
$preview_url = admin_url('post.php?post=' . $page_id . '&action=edit'); |
| 213 |
} |
| 214 |
|
| 215 |
return [ |
| 216 |
'success' => true, |
| 217 |
'message' => 'Preview page ready.', |
| 218 |
'preview_url' => $preview_url, |
| 219 |
'page_id' => $page_id, |
| 220 |
]; |
| 221 |
} |
| 222 |
|