← 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, |