PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.1
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.1
2.7.1 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 All 130 releases
wdesignkit / includes / abilities / widgets / wdesignkit-set-widget-thumbnail.php

wdesignkit-set-widget-thumbnail.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.1, at includes/abilities/widgets/wdesignkit-set-widget-thumbnail.php

312 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Set or update a local WDesignKit widget's preview thumbnail image.
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/set-widget-thumbnail', [
13 'label' => __('Set WDesignKit Widget Thumbnail', 'wdesignkit'),
14 'description' => __(
15 'Attaches or replaces the preview thumbnail image for a local WDesignKit widget. Accepts a Media Library attachment ID, an image URL (sideload), or base64 data. Saves the thumbnail image into the widget folder and updates the thumbnail reference in the widget JSON config so wdesignkit/push-widget can validate and upload the thumbnail.',
16 'wdesignkit',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'builder' => [
23 'type' => 'string',
24 'description' => 'Builder type the widget belongs to (elementor, gutenberg, gutenberg_core, bricks).',
25 'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'],
26 ],
27 'folder' => [
28 'type' => 'string',
29 'description' => 'Widget folder name (from wdesignkit/list-widgets). Required if widget_id is omitted.',
30 ],
31 'widget_id' => [
32 'type' => 'string',
33 'description' => 'Widget unique ID. Used to locate the widget folder if folder is omitted.',
34 ],
35 'image_id' => [
36 'type' => 'integer',
37 'description' => 'Media Library attachment ID for the thumbnail image.',
38 ],
39 'attachment_id' => [
40 'type' => 'integer',
41 'description' => 'Alias for image_id (Media Library attachment ID).',
42 ],
43 'image_url' => [
44 'type' => 'string',
45 'description' => 'URL of the image to fetch and use as thumbnail.',
46 ],
47 'image_base64' => [
48 'type' => 'string',
49 'description' => 'Image supplied inline as base64 string or data:image/...;base64,... URI.',
50 ],
51 'image' => [
52 'description' => 'Unified image input — accepts a Media Library attachment ID (int), image URL (string starting with http/https), or base64 string.',
53 ],
54 ],
55 'additionalProperties' => false,
56 ],
57 'output_schema' => [
58 'type' => 'object',
59 'properties' => [
60 'success' => ['type' => 'boolean'],
61 'message' => ['type' => 'string'],
62 'thumbnail_url' => ['type' => 'string'],
63 'builder' => ['type' => 'string'],
64 'folder' => ['type' => 'string'],
65 'widget_id' => ['type' => 'string'],
66 ],
67 ],
68 'execute_callback' => 'wdesignkit_mcp_set_widget_thumbnail',
69 'permission_callback' => 'wdesignkit_mcp_permission_callback',
70 'meta' => [
71 'public' => true,
72 'show_in_rest' => true,
73 'mcp' => ['public' => true],
74 'annotations' => [
75 'instructions' => implode("\n", [
76 'Attaches or replaces the preview thumbnail image for a local WDesignKit widget.',
77 'Accepts image input via image_id (Media Library ID), image_url (sideload URL), image_base64, or image.',
78 'Updates the local widget folder with the image file and sets w_image in JSON config.',
79 'Run wdesignkit/push-widget afterwards to upload the widget and thumbnail to the cloud.',
80 ]),
81 'readonly' => false,
82 'destructive' => false,
83 'idempotent' => false,
84 ],
85 ],
86 ]);
87
88 function wdesignkit_mcp_set_widget_thumbnail(array $input): array {
89 set_time_limit(90);
90
91 if (!defined('WDKIT_BUILDER_PATH') || !defined('WDKIT_SERVER_PATH')) {
92 return ['success' => false, 'message' => 'WDesignKit plugin is not active.'];
93 }
94
95 $builder = sanitize_text_field((string) ($input['builder'] ?? ''));
96 $folder = sanitize_file_name((string) ($input['folder'] ?? ''));
97 $widget_id = sanitize_text_field((string) ($input['widget_id'] ?? ''));
98
99 $allowed_builders = ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'];
100
101 // Locate widget directory
102 $widget_dir = null;
103
104 if ($folder !== '') {
105 if ($builder !== '' && !in_array($builder, $allowed_builders, true)) {
106 return ['success' => false, 'message' => 'Invalid builder type.'];
107 }
108 $builders_to_check = ($builder !== '') ? [$builder] : $allowed_builders;
109 foreach ($builders_to_check as $b) {
110 $candidate_dir = WDKIT_BUILDER_PATH . '/' . $b . '/' . $folder;
111 if (is_dir($candidate_dir)) {
112 $widget_dir = $candidate_dir;
113 $builder = $b;
114 break;
115 }
116 }
117 } elseif ($widget_id !== '') {
118 $builders_to_check = ($builder !== '' && in_array($builder, $allowed_builders, true)) ? [$builder] : $allowed_builders;
119 foreach ($builders_to_check as $b) {
120 $b_dir = WDKIT_BUILDER_PATH . '/' . $b;
121 if (!is_dir($b_dir)) {
122 continue;
123 }
124 $subfolders = array_diff(@scandir($b_dir) ?: [], ['.', '..']);
125 foreach ($subfolders as $sub) {
126 $dir_path = $b_dir . '/' . $sub;
127 if (!is_dir($dir_path)) {
128 continue;
129 }
130 $files = array_diff(@scandir($dir_path) ?: [], ['.', '..']);
131 foreach ($files as $f) {
132 if (pathinfo($f, PATHINFO_EXTENSION) === 'json') {
133 $raw = @file_get_contents($dir_path . '/' . $f);
134 $data = ($raw !== false) ? json_decode($raw, true) : null;
135 $wid = $data['widget_data']['widgetdata']['widget_id'] ?? '';
136 if ($wid === $widget_id) {
137 $widget_dir = $dir_path;
138 $builder = $b;
139 $folder = $sub;
140 break 3;
141 }
142 }
143 }
144 }
145 }
146 }
147
148 if (!$widget_dir || !is_dir($widget_dir)) {
149 return [
150 'success' => false,
151 'message' => 'Widget not found. Specify a valid builder and folder or widget_id.',
152 ];
153 }
154
155 // Path safety check
156 $real_widget = realpath($widget_dir);
157 $real_base = realpath(WDKIT_BUILDER_PATH);
158 if (!$real_widget || !$real_base || strpos($real_widget, $real_base . DIRECTORY_SEPARATOR) !== 0) {
159 return ['success' => false, 'message' => 'Invalid widget path.'];
160 }
161
162 // Read JSON config
163 $files = array_diff(@scandir($widget_dir) ?: [], ['.', '..']);
164 $json_path = null;
165 $json_data = null;
166
167 foreach ($files as $f) {
168 if (pathinfo($f, PATHINFO_EXTENSION) === 'json') {
169 $json_path = $widget_dir . '/' . $f;
170 $raw = @file_get_contents($json_path);
171 $json_data = ($raw !== false) ? json_decode($raw, true) : null;
172 break;
173 }
174 }
175
176 if (!is_array($json_data) || !$json_path) {
177 return ['success' => false, 'message' => "Could not read JSON config in widget folder {$builder}/{$folder}."];
178 }
179
180 $widgetdata = $json_data['widget_data']['widgetdata'] ?? [];
181 $title = sanitize_text_field((string) ($widgetdata['name'] ?? ''));
182 $json_widget_id = sanitize_text_field((string) ($widgetdata['widget_id'] ?? ''));
183 if ($widget_id === '') {
184 $widget_id = $json_widget_id;
185 }
186
187 // Resolve input image (ID, Base64, or URL)
188 $raw_image = $input['image'] ?? null;
189 $img_id = (int) ($input['image_id'] ?? $input['attachment_id'] ?? (is_numeric($raw_image) ? $raw_image : 0));
190 $img_url = sanitize_url((string) ($input['image_url'] ?? (is_string($raw_image) && preg_match('#^https?://#i', $raw_image) ? $raw_image : '')));
191 $img_base64 = (string) ($input['image_base64'] ?? (is_string($raw_image) && !preg_match('#^https?://#i', $raw_image) && !is_numeric($raw_image) ? $raw_image : ''));
192
193 $img_bytes = '';
194 $ext = 'png';
195
196 if ($img_id > 0) {
197 $attached_file = get_attached_file($img_id);
198 if ($attached_file && file_exists($attached_file)) {
199 $read_bytes = @file_get_contents($attached_file);
200 if ($read_bytes !== false) {
201 $img_bytes = $read_bytes;
202 $file_ext = strtolower(pathinfo($attached_file, PATHINFO_EXTENSION));
203 if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'webp'], true)) {
204 $ext = ($file_ext === 'jpeg') ? 'jpg' : $file_ext;
205 }
206 }
207 }
208 if ($img_bytes === '') {
209 $att_url = wp_get_attachment_url($img_id);
210 if ($att_url) {
211 $img_url = $att_url;
212 }
213 }
214 }
215
216 if ($img_bytes === '' && $img_base64 !== '') {
217 $b64 = $img_base64;
218 if (preg_match('#^data:image/([a-zA-Z0-9]+);base64,(.*)$#s', $b64, $m)) {
219 $mime_ext = strtolower($m[1]);
220 if (in_array($mime_ext, ['jpg', 'jpeg', 'png', 'webp'], true)) {
221 $ext = ($mime_ext === 'jpeg') ? 'jpg' : $mime_ext;
222 }
223 $b64 = $m[2];
224 }
225 $b64_clean = preg_replace('/\s+/', '', $b64);
226 $decoded = base64_decode($b64_clean, true);
227 if ($decoded !== false && strlen($decoded) > 0) {
228 $img_bytes = $decoded;
229 }
230 }
231
232 if ($img_bytes === '' && $img_url !== '') {
233 // SSRF guard (CWE-918): image_url is agent/user-supplied. wp_safe_remote_get() only
234 // rejects a literal IP host and doesn't resolve hostnames, so it won't stop a DNS name
235 // pointed at a loopback/private/cloud-metadata address; wdesignkit_safe_remote_get()
236 // resolves the host first and blocks those ranges.
237 $img_resp = wdesignkit_safe_remote_get($img_url, ['timeout' => 30]);
238 if (is_wp_error($img_resp)) {
239 return ['success' => false, 'message' => 'Could not fetch image_url: ' . $img_resp->get_error_message()];
240 }
241 $status_code = (int) wp_remote_retrieve_response_code($img_resp);
242 if (200 !== $status_code) {
243 return ['success' => false, 'message' => "image_url returned HTTP {$status_code}."];
244 }
245 $body = (string) wp_remote_retrieve_body($img_resp);
246 if ($body === '') {
247 return ['success' => false, 'message' => 'image_url returned an empty response.'];
248 }
249 $img_bytes = $body;
250
251 $url_ext = strtolower(pathinfo((string) parse_url($img_url, PHP_URL_PATH), PATHINFO_EXTENSION));
252 if (in_array($url_ext, ['jpg', 'jpeg', 'png', 'webp'], true)) {
253 $ext = ($url_ext === 'jpeg') ? 'jpg' : $url_ext;
254 } else {
255 $content_type = (string) wp_remote_retrieve_header($img_resp, 'content-type');
256 if (strpos($content_type, 'jpeg') !== false || strpos($content_type, 'jpg') !== false) {
257 $ext = 'jpg';
258 } elseif (strpos($content_type, 'webp') !== false) {
259 $ext = 'webp';
260 } elseif (strpos($content_type, 'png') !== false) {
261 $ext = 'png';
262 }
263 }
264 }
265
266 if ($img_bytes === '') {
267 return [
268 'success' => false,
269 'message' => 'Provide a valid image via image_id (Media Library ID), image_url, or image_base64.',
270 ];
271 }
272
273 // Determine target image filename
274 $file_name = ($title !== '' && $widget_id !== '') ? str_replace(' ', '_', $title) . '_' . $widget_id : $folder;
275
276 // Remove old thumbnail images in widget folder to prevent stale files
277 foreach ($files as $f) {
278 $f_ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
279 if (in_array($f_ext, ['jpg', 'jpeg', 'png', 'webp'], true)) {
280 @unlink($widget_dir . '/' . $f);
281 }
282 }
283
284 // Save new image file
285 $target_image_file = $widget_dir . '/' . $file_name . '.' . $ext;
286 if (@file_put_contents($target_image_file, $img_bytes) === false) {
287 return ['success' => false, 'message' => "Failed to write image file to {$builder}/{$folder}."];
288 }
289
290 // Update JSON config w_image reference
291 $stored_thumbnail_url = WDKIT_SERVER_PATH . '/' . $builder . '/' . $folder . '/' . $file_name . '.' . $ext;
292 $json_data['widget_data']['widgetdata']['w_image'] = $stored_thumbnail_url;
293
294 $json_written = @file_put_contents(
295 $json_path,
296 wp_json_encode($json_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
297 );
298
299 if ($json_written === false) {
300 return ['success' => false, 'message' => "Saved image but failed to update JSON config in {$builder}/{$folder}."];
301 }
302
303 return [
304 'success' => true,
305 'message' => "Thumbnail image set successfully for widget '{$title}'.",
306 'thumbnail_url' => $stored_thumbnail_url,
307 'builder' => $builder,
308 'folder' => $folder,
309 'widget_id' => $widget_id,
310 ];
311 }
312