← All changes
|
includes/abilities/widgets/wdesignkit-pull-widget.php
+77
-20
2.6.3
→
2.7.0
View file →
| @@ -57,8 +57,9 @@ | ||
| 57 | 57 | 'widget_id' => ['type' => 'string'], |
| 58 | 58 | 'widget_name' => ['type' => 'string'], |
| 59 | 59 | 'builder' => ['type' => 'string'], |
| 60 | 60 | 'folder' => ['type' => 'string'], |
| 61 | + 'duplicate_folders' => ['type' => 'array'], | |
| 61 | 62 | 'version' => ['type' => 'string'], |
| 62 | 63 | 'files' => ['type' => 'array'], |
| 63 | 64 | 'response' => ['type' => 'object'], |
| 64 | 65 | ], |
| @@ -65,8 +66,9 @@ | ||
| 65 | 66 | ], |
| 66 | 67 | 'execute_callback' => 'wdesignkit_mcp_pull_widget', |
| 67 | 68 | 'permission_callback' => 'wdesignkit_mcp_permission_callback', |
| 68 | 69 | 'meta' => [ |
| 70 | + 'public' => true, | |
| 69 | 71 | 'show_in_rest' => true, |
| 70 | 72 | 'mcp' => ['public' => true], |
| 71 | 73 | 'annotations' => [ |
| 72 | 74 | 'instructions' => implode("\n", [ |
| @@ -285,13 +287,49 @@ | ||
| 285 | 287 | if ($cloud_title === '' || $cloud_builder === '') { |
| 286 | 288 | return ['success' => false, 'message' => 'Downloaded widget JSON is missing required fields (name, type).']; |
| 287 | 289 | } |
| 288 | 290 | |
| 289 | - // Stable folder and file naming | |
| 290 | - $folder_name = ($folder !== '') ? $folder : (str_replace(' ', '-', $cloud_title) . '_' . ($cloud_wid ?: $r_id)); | |
| 291 | - $file_name = str_replace(' ', '_', $cloud_title) . '_' . ($cloud_wid ?: $r_id); | |
| 292 | - $widget_dir = WDKIT_BUILDER_PATH . '/' . $cloud_builder . '/' . $folder_name; | |
| 291 | + // Stable folder and file naming, via the canonical helpers so every writer agrees on the | |
| 292 | + // name — the loader pairs "<base>.php" with "<base>.json", so a writer that derives the | |
| 293 | + // base differently orphans the widget (ClickUp 86d41cck5). These also apply | |
| 294 | + // sanitize_file_name(), which the hand-rolled derivation here skipped entirely. | |
| 295 | + $widget_uid = $cloud_wid ?: $r_id; | |
| 296 | + $folder_name = ($folder !== '') ? $folder : wdesignkit_widget_folder_name($cloud_title, $widget_uid); | |
| 297 | + $file_name = wdesignkit_widget_file_name($cloud_title, $widget_uid); | |
| 298 | + $builder_dir = WDKIT_BUILDER_PATH . '/' . $cloud_builder; | |
| 293 | 299 | |
| 300 | + // Reuse the folder this widget already occupies instead of minting a new name for it, the | |
| 301 | + // same way download-widget does. Without this, pulling a widget whose folder was written | |
| 302 | + // under an older convention (e.g. a pre-2.6.4 lowercase "my-widget_id") created a second | |
| 303 | + // directory differing only by case — two folders holding one widget on Linux, a silent | |
| 304 | + // write into the wrong one on macOS/Windows (ClickUp 86d41ccka). Match on the widget id, | |
| 305 | + // which is the suffix of every folder name. An explicit folder argument still wins: the | |
| 306 | + // resolver above sets it from an on-disk folder, so the caller has already chosen a target. | |
| 307 | + $duplicate_folders = []; | |
| 308 | + if ($folder === '') { | |
| 309 | + $existing_folder = wdesignkit_find_widget_folder($builder_dir, $widget_uid, $folder_matches); | |
| 310 | + if ($existing_folder !== '') { | |
| 311 | + $folder_name = $existing_folder; | |
| 312 | + | |
| 313 | + // Surface any other folder still holding this widget id. These are inert — a | |
| 314 | + // builder loader only registers a folder containing a .php — but reporting them | |
| 315 | + // lets the caller clear them instead of finding the widget listed twice. | |
| 316 | + $duplicate_folders = array_values(array_diff($folder_matches, [$existing_folder])); | |
| 317 | + } | |
| 318 | + } | |
| 319 | + | |
| 320 | + $widget_dir = $builder_dir . '/' . $folder_name; | |
| 321 | + | |
| 322 | + // Adopt the file base name already used inside the target folder, whichever way that | |
| 323 | + // folder was chosen, so the refreshed files replace the existing ones rather than landing | |
| 324 | + // beside them under a second naming convention. | |
| 325 | + foreach (@scandir($widget_dir) ?: [] as $existing_file) { | |
| 326 | + if (pathinfo($existing_file, PATHINFO_EXTENSION) === 'json') { | |
| 327 | + $file_name = pathinfo($existing_file, PATHINFO_FILENAME); | |
| 328 | + break; | |
| 329 | + } | |
| 330 | + } | |
| 331 | + | |
| 294 | 332 | $proposed_files = [ |
| 295 | 333 | $folder_name . '/' . $file_name . '.json', |
| 296 | 334 | ]; |
| 297 | 335 | if (!empty($json_raw['Editor_data']['css'])) { |
| @@ -347,14 +385,21 @@ | ||
| 347 | 385 | // instead of a plain fetch. |
| 348 | 386 | if ($img_url !== '') { |
| 349 | 387 | $img_resp = wdesignkit_safe_remote_get($img_url, ['timeout' => 30]); |
| 350 | 388 | if (!is_wp_error($img_resp)) { |
| 351 | - $img_ext = strtolower(pathinfo((string) parse_url($img_url, PHP_URL_PATH), PATHINFO_EXTENSION)) ?: 'png'; | |
| 352 | - $img_ext = sanitize_file_name($img_ext); | |
| 353 | - if (defined('WDKIT_SERVER_PATH')) { | |
| 354 | - $json_raw['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/{$cloud_builder}/{$folder_name}/{$file_name}.{$img_ext}"; | |
| 389 | + // sanitize_file_name() passes "php" through unchanged, so the remote extension was | |
| 390 | + // effectively unvalidated and a ".php" thumbnail URL wrote executable PHP into the | |
| 391 | + // builder directory (CWE-434, ClickUp 86d41cczd). Verify against the payload; '' means | |
| 392 | + // the bytes are not an image we accept, so nothing is written. | |
| 393 | + $img_body = (string) wp_remote_retrieve_body($img_resp); | |
| 394 | + $img_ext = wdesignkit_safe_image_extension($img_url, $img_body); | |
| 395 | + | |
| 396 | + if ($img_ext !== '') { | |
| 397 | + if (defined('WDKIT_SERVER_PATH')) { | |
| 398 | + $json_raw['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/{$cloud_builder}/{$folder_name}/{$file_name}.{$img_ext}"; | |
| 399 | + } | |
| 400 | + @file_put_contents($widget_dir . '/' . $file_name . '.' . $img_ext, $img_body); | |
| 355 | 401 | } |
| 356 | - @file_put_contents($widget_dir . '/' . $file_name . '.' . $img_ext, (string) wp_remote_retrieve_body($img_resp)); | |
| 357 | 402 | } |
| 358 | 403 | } |
| 359 | 404 | |
| 360 | 405 | // Save JSON config |
| @@ -374,18 +419,30 @@ | ||
| 374 | 419 | if (!empty($json_raw['Editor_data']['js'])) { |
| 375 | 420 | @file_put_contents($widget_dir . '/' . $file_name . '.js', (string) $json_raw['Editor_data']['js']); |
| 376 | 421 | } |
| 377 | 422 | |
| 423 | + if (function_exists('wdesignkit_invalidate_widget_registry')) { | |
| 424 | + wdesignkit_invalidate_widget_registry($cloud_builder); | |
| 425 | + } | |
| 426 | + | |
| 427 | + $message = "Widget '{$cloud_title}' (r_id: {$r_id}) pulled from cloud and updated locally."; | |
| 428 | + if (!empty($duplicate_folders)) { | |
| 429 | + $message .= ' Note: this widget_id also occupies ' . count($duplicate_folders) | |
| 430 | + . ' other folder(s) from an earlier naming convention (' | |
| 431 | + . implode(', ', $duplicate_folders) . '). They are not loaded and can be deleted.'; | |
| 432 | + } | |
| 433 | + | |
| 378 | 434 | return [ |
| 379 | - 'success' => true, | |
| 380 | - 'message' => "Widget '{$cloud_title}' (r_id: {$r_id}) pulled from cloud and updated locally.", | |
| 381 | - 'dry_run' => false, | |
| 382 | - 'r_id' => $r_id, | |
| 383 | - 'widget_id' => $cloud_wid, | |
| 384 | - 'widget_name' => $cloud_title, | |
| 385 | - 'builder' => $cloud_builder, | |
| 386 | - 'folder' => $folder_name, | |
| 387 | - 'version' => $cloud_version, | |
| 388 | - 'files' => $proposed_files, | |
| 389 | - 'response' => wdesignkit_mcp_ensure_object($data, $body), | |
| 435 | + 'success' => true, | |
| 436 | + 'message' => $message, | |
| 437 | + 'dry_run' => false, | |
| 438 | + 'r_id' => $r_id, | |
| 439 | + 'widget_id' => $cloud_wid, | |
| 440 | + 'widget_name' => $cloud_title, | |
| 441 | + 'builder' => $cloud_builder, | |
| 442 | + 'folder' => $folder_name, | |
| 443 | + 'duplicate_folders' => $duplicate_folders, | |
| 444 | + 'version' => $cloud_version, | |
| 445 | + 'files' => $proposed_files, | |
| 446 | + 'response' => wdesignkit_mcp_ensure_object($data, $body), | |
| 390 | 447 | ]; |
| 391 | 448 | } |