PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
2.7.0 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 All 129 releases
← All changes | includes/abilities/widgets/wdesignkit-import-widget.php +91 -15 2.5.0 → 2.7.0 View file →
@@ -9,12 +9,12 @@
9 9 exit();
10 10 }
11 11
12 12 wp_register_ability('wdesignkit/import-widget', [
13 - 'label' => __('Import WDesignKit Widget', 'sprout-mcp'),
13 + 'label' => __('Import WDesignKit Widget', 'wdesignkit'),
14 14 'description' => __(
15 15 'Imports a widget into the local WDesignKit library from its JSON config object. Provide the full widget_data JSON (same structure as the .json file inside a .wdk ZIP export). The ability creates the correct builder folder, writes the JSON, and optionally downloads the widget thumbnail. Rejects imports when a widget with the same widget_id already exists.',
16 - 'sprout-mcp',
16 + 'wdesignkit',
17 17 ),
18 18 'category' => 'wdesignkit',
19 19 'input_schema' => [
20 20 'type' => 'object',
@@ -24,10 +24,18 @@
24 24 'description' => 'The parsed widget config object — the top-level structure is {"widget_data":{"widgetdata":{…}}}. This is the same JSON that lives inside the .wdk ZIP export.',
25 25 ],
26 26 'image_url' => [
27 27 'type' => 'string',
28 - 'description' => 'Optional URL of the widget thumbnail image. Downloaded and stored alongside the JSON.',
28 + 'description' => 'Optional URL of the widget thumbnail image. Downloaded and stored alongside the JSON. Used only when image_base64 is not provided.',
29 29 ],
30 + 'image_base64' => [
31 + 'type' => 'string',
32 + 'description' => 'Optional widget thumbnail supplied inline as base64 (raw base64, or a data:image/...;base64,... URI). Use this to import the thumbnail bundled INSIDE a .wdk ZIP export — mirrors the manual Import UI, no separately-hosted URL needed. Takes precedence over image_url.',
33 + ],
34 + 'image_ext' => [
35 + 'type' => 'string',
36 + 'description' => 'Optional image extension (png, jpg, jpeg, webp) for image_base64 when it is raw base64 without a data: URI. Defaults to the widget JSON img_ext, or "png".',
37 + ],
30 38 'overwrite' => [
31 39 'type' => 'boolean',
32 40 'description' => 'When true, overwrites an existing widget that shares the same widget_id instead of rejecting the import.',
33 41 ],
@@ -46,10 +54,11 @@
46 54 'widget_name' => ['type' => 'string'],
47 55 ],
48 56 ],
49 57 'execute_callback' => 'wdesignkit_mcp_import_widget',
50 - 'permission_callback' => 'sprout_mcp_permission_callback',
58 + 'permission_callback' => 'wdesignkit_mcp_permission_callback',
51 59 'meta' => [
60 + 'public' => true,
52 61 'show_in_rest' => true,
53 62 'mcp' => ['public' => true],
54 63 'annotations' => [
55 64 'instructions' => implode("\n", [
@@ -55,8 +64,9 @@
55 64 'instructions' => implode("\n", [
56 65 'Imports a widget from its JSON config into the local library.',
57 66 'Does NOT require cloud login — this is a local filesystem operation.',
58 67 'widget_json must be the full widget_data structure ({"widget_data":{"widgetdata":{…}}}) — get it from wdesignkit/get-widget or from a .wdk ZIP export.',
68 + 'To import a .wdk ZIP\'s bundled thumbnail, pass its bytes as image_base64 (no external URL needed); use image_url only for a remotely-hosted image.',
59 69 'Fails if a widget with the same widget_id already exists unless overwrite: true.',
60 70 'After a successful import the widget appears in wdesignkit/list-widgets.',
61 71 ]),
62 72 'readonly' => false,
@@ -97,10 +107,15 @@
97 107 return ['success' => false, 'message' => "Unsupported builder type: {$builder}."];
98 108 }
99 109
100 110 $overwrite = !empty($input['overwrite']);
101 - $folder_name = str_replace(' ', '-', $widget_name) . '_' . $widget_id;
102 - $file_name = str_replace(' ', '_', $widget_name) . '_' . $widget_id;
111 + // Canonical helpers: they replace spaces BEFORE sanitize_file_name(). Sanitising first
112 + // (as this did) already collapsed spaces to hyphens, so the underscore pass was a no-op
113 + // and a multi-word title produced "My-Widget_id.json" while the builder's own save path
114 + // writes "My_Widget_id.php". The registry resolves the JSON by swapping .php for .json,
115 + // so that pair never matched and the widget was skipped for good (ClickUp 86d41cck5).
116 + $folder_name = wdesignkit_widget_folder_name($widget_name, $widget_id);
117 + $file_name = wdesignkit_widget_file_name($widget_name, $widget_id);
103 118 $builder_dir = WDKIT_BUILDER_PATH . '/' . $builder;
104 119 $widget_dir = $builder_dir . '/' . $folder_name;
105 120
106 121 // Duplicate check / overwrite: scan for any existing folder sharing the same widget_id.
@@ -152,8 +167,15 @@
152 167 if (!wp_mkdir_p($widget_dir)) {
153 168 return ['success' => false, 'message' => "Could not create widget folder: {$builder}/{$folder_name}"];
154 169 }
155 170
171 + // Realpath validation — ensure we're still inside WDKIT_BUILDER_PATH.
172 + $real_widget = realpath($widget_dir);
173 + $real_base = realpath(WDKIT_BUILDER_PATH);
174 + if (!$real_widget || !$real_base || strpos($real_widget, $real_base . DIRECTORY_SEPARATOR) !== 0) {
175 + return ['success' => false, 'message' => 'Invalid widget path.'];
176 + }
177 +
156 178 $json_path = $widget_dir . '/' . $file_name . '.json';
157 179 $written = @file_put_contents($json_path, wp_json_encode($widget_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
158 180
159 181 if ($written === false) {
@@ -159,24 +181,78 @@
159 181 if ($written === false) {
160 182 return ['success' => false, 'message' => 'Could not write widget JSON file.'];
161 183 }
162 184
163 - $image_saved = false;
164 - $image_url = sanitize_url((string) ($input['image_url'] ?? ''));
165 - if ($image_url !== '') {
166 - $img_resp = wp_remote_get($image_url, ['timeout' => 30]);
185 + // Thumbnail. Prefer an inline base64 image — this is how a .wdk ZIP's OWN bundled
186 + // thumbnail is imported (mirrors the manual Import UI's ZipArchive extraction). Fall
187 + // back to downloading a remote image_url. Whichever is used, the JSON's w_image / img_ext
188 + // are pointed at the saved local file so the library shows it and push-widget finds it.
189 + $image_saved = false;
190 + $image_source = '';
191 + $image_base64 = (string) ($input['image_base64'] ?? '');
192 + $image_url = sanitize_url((string) ($input['image_url'] ?? ''));
193 +
194 + $img_bytes = '';
195 + $img_ext = '';
196 +
197 + if ($image_base64 !== '') {
198 + $b64 = $image_base64;
199 + if (preg_match('#^data:image/([a-zA-Z0-9.+-]+);base64,(.*)$#s', $b64, $m)) {
200 + $img_ext = $m[1];
201 + $b64 = $m[2];
202 + }
203 + $b64 = preg_replace('/\s+/', '', (string) $b64);
204 + $decoded = ($b64 !== '') ? base64_decode($b64, true) : false;
205 + if ($decoded !== false && $decoded !== '') {
206 + $img_bytes = $decoded;
207 + if ($img_ext === '') {
208 + $img_ext = (string) ($input['image_ext'] ?? ($widgetdata['img_ext'] ?? 'png'));
209 + }
210 + $image_source = 'bundled';
211 + }
212 + } elseif ($image_url !== '') {
213 + // SSRF guard (CWE-918): validate the resolved host before fetching a caller-supplied URL.
214 + $img_resp = wdesignkit_safe_remote_get($image_url, ['timeout' => 30]);
167 215 if (!is_wp_error($img_resp)) {
168 - $img_ext = pathinfo(parse_url($image_url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION) ?: 'png';
169 - $img_ext = sanitize_file_name($img_ext);
170 - $img_path = $widget_dir . '/' . $file_name . '.' . $img_ext;
171 - @file_put_contents($img_path, wp_remote_retrieve_body($img_resp));
216 + $img_bytes = (string) wp_remote_retrieve_body($img_resp);
217 + $img_ext = pathinfo(parse_url($image_url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION) ?: 'png';
218 + $image_source = 'downloaded';
219 + }
220 + }
221 +
222 + if ($img_bytes !== '') {
223 + // Both branches above take the extension from caller-supplied data — a data-URI subtype or
224 + // the remote URL. The old preg_replace() only stripped punctuation, so "php" survived and
225 + // this write could drop executable PHP into the builder directory (CWE-434,
226 + // ClickUp 86d41cczd). Verify against the decoded bytes instead; '' means not an image.
227 + // The synthetic "image.<ext>" filename lets the helper read either branch's extension.
228 + $img_ext = wdesignkit_safe_image_extension('image.' . strtolower((string) $img_ext), $img_bytes);
229 + }
230 +
231 + if ($img_bytes !== '' && $img_ext !== '') {
232 + $img_path = $widget_dir . '/' . $file_name . '.' . $img_ext;
233 + if (@file_put_contents($img_path, $img_bytes) !== false) {
172 234 $image_saved = true;
235 + // Point the JSON at the local thumbnail so it renders in the library and push finds it.
236 + if (defined('WDKIT_SERVER_PATH')) {
237 + $widget_json['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/{$builder}/{$folder_name}/{$file_name}.{$img_ext}";
238 + }
239 + $widget_json['widget_data']['widgetdata']['img_ext'] = $img_ext;
240 + @file_put_contents($json_path, wp_json_encode($widget_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
173 241 }
174 242 }
175 243
244 + $thumb_note = $image_saved
245 + ? ($image_source === 'bundled' ? ' Bundled thumbnail imported.' : ' Thumbnail downloaded.')
246 + : '';
247 +
248 + if (function_exists('wdesignkit_invalidate_widget_registry')) {
249 + wdesignkit_invalidate_widget_registry($builder);
250 + }
251 +
176 252 return [
177 253 'success' => true,
178 - 'message' => "Widget '{$widget_name}' imported successfully." . ($image_saved ? ' Thumbnail downloaded.' : ''),
254 + 'message' => "Widget '{$widget_name}' imported successfully." . $thumb_note,
179 255 'folder' => $folder_name,
180 256 'builder' => $builder,
181 257 'widget_id' => $widget_id,
182 258 'widget_name' => $widget_name,