PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
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
← All changes | includes/abilities/widgets/wdesignkit-create-widget.php +264 -31 2.5.3 → 2.7.0 View file →
@@ -9,12 +9,12 @@
9 9 exit();
10 10 }
11 11
12 12 wp_register_ability('wdesignkit/create-widget', [
13 - 'label' => __('Create WDesignKit Widget', 'sprout-mcp'),
13 + 'label' => __('Create WDesignKit Widget', 'wdesignkit'),
14 14 'description' => __(
15 15 'Creates a new widget for Elementor, Gutenberg, Gutenberg Core, or Bricks builder. Generates all required files (PHP, JSON config, CSS, JS) with proper boilerplate code. You can provide custom code for each file or use defaults.',
16 - 'sprout-mcp',
16 + 'wdesignkit',
17 17 ),
18 18 'category' => 'wdesignkit',
19 19 'input_schema' => [
20 20 'type' => 'object',
@@ -26,9 +26,9 @@
26 26 'maxLength' => 64,
27 27 ],
28 28 'builder' => [
29 29 'type' => 'string',
30 - 'description' => 'Target page builder.',
30 + 'description' => 'Target page builder. Use "gutenberg" for Nexter Blocks / The Plus Addons for Block Editor (TPGB) — "Nexter", "Nexter Blocks", "TPGB", and "Plus Block Editor" all map to builder=gutenberg; it builds a block on the Pmgc component framework. Use "gutenberg_core" for a plain native WordPress block with no TPGB dependency. Note: "Nexter Extension" is a separate plugin, not a builder.',
31 31 'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'],
32 32 ],
33 33 'description' => [
34 34 'type' => 'string',
@@ -117,10 +117,11 @@
117 117 ],
118 118 ],
119 119 ],
120 120 'execute_callback' => 'wdesignkit_mcp_create_widget',
121 - 'permission_callback' => 'sprout_mcp_permission_callback',
121 + 'permission_callback' => 'wdesignkit_mcp_permission_callback',
122 122 'meta' => [
123 + 'public' => true,
123 124 'show_in_rest' => true,
124 125 'mcp' => ['public' => true],
125 126 'annotations' => [
126 127 'instructions' => implode("\n", [
@@ -146,8 +147,12 @@
146 147 '### Required inputs',
147 148 '- name: 1–64 chars. Letters, numbers, spaces, hyphens, underscores ONLY.',
148 149 ' Special characters are REJECTED with an error — do not pass them. Names must be unique per builder.',
149 150 '- builder: one of elementor | gutenberg | gutenberg_core | bricks',
151 + ' Builder aliases — "Nexter" / "Nexter Blocks" / "TPGB" / "The Plus Addons for Block Editor" / "Plus Block Editor" all mean builder=gutenberg.',
152 + ' gutenberg = Nexter Blocks (TPGB): block built on the Pmgc component framework',
153 + ' (Pmgc_PanelTabs/Pmgc_Tab inspector, Pmgc_CssGenerator styling), loaded from the gutenberg/ folder.',
154 + ' "gutenberg_core" = plain native WordPress blocks (no TPGB framework). "Nexter Extension" is a SEPARATE plugin, NOT a builder — do not treat it as the target builder.',
150 155 '',
151 156 '### Code inputs (php_code, css_code, js_code, cdn_js, cdn_css, section_data, editor_html)',
152 157 '- php_code / css_code / js_code: COMPLETE file replacements, not partial snippets.',
153 158 '- STRONGLY RECOMMENDED: omit php_code and let the ability generate correct boilerplate.',
@@ -237,8 +242,18 @@
237 242 '### After creation',
238 243 '- Use wdesignkit/get-widget to read back the generated files.',
239 244 '- Use wdesignkit/update-widget to modify code.',
240 245 '- Does NOT require WDesignKit cloud login.',
246 + '',
247 + '### ⚠ Trust boundary — this ability executes PHP',
248 + 'The php_code written here is saved to disk and require_once\'d by the builder',
249 + 'loaders on subsequent requests, so calling this ability runs arbitrary PHP as the',
250 + 'web server user. That is what a widget builder is, and it is gated on',
251 + 'manage_options — the same capability WordPress requires for the plugin editor.',
252 + 'It does mean this ability, create-widget and update-widget, must never be exposed',
253 + 'to any automation context trusted below a full site administrator. Treat php_code',
254 + 'from an untrusted source exactly as you would treat pasting it into wp-admin.',
255 + '(ClickUp 86d41zavc)',
241 256 ]),
242 257 'readonly' => false,
243 258 'destructive' => false,
244 259 'idempotent' => false,
@@ -253,8 +268,20 @@
253 268 'message' => 'WDesignKit plugin is not active.',
254 269 ];
255 270 }
256 271
272 + // Site-level opt-out for generated widget PHP (ClickUp 86d41zavc). Defaults to allowed, so
273 + // this changes nothing unless an operator explicitly filters it off:
274 + // add_filter( 'wdesignkit_allow_widget_php_write', '__return_false' );
275 + // See wdesignkit_widget_php_write_allowed() for why this is a dedicated filter rather than a
276 + // DISALLOW_FILE_MODS / DISALLOW_FILE_EDIT check.
277 + if (function_exists('wdesignkit_widget_php_write_allowed') && !wdesignkit_widget_php_write_allowed()) {
278 + return [
279 + 'success' => false,
280 + 'message' => 'Widget creation is disabled on this site: generated widget PHP writes have been turned off via the wdesignkit_allow_widget_php_write filter.',
281 + ];
282 + }
283 +
257 284 // --- Validate name ---
258 285 $raw_name = $input['name'] ?? '';
259 286 if (!is_string($raw_name) || trim($raw_name) === '') {
260 287 return ['success' => false, 'message' => 'Widget name is required.'];
@@ -337,19 +364,16 @@
337 364 return ['success' => false, 'message' => 'Invalid version format. Use semver format like "1.0.0".'];
338 365 }
339 366
340 367 // --- Generate folder and file names matching WDesignKit convention ---
341 - // folder: kebab-case (e.g. "my-custom-card")
342 - // file: snake_case (e.g. "my_custom_card")
343 - // Normalize ALL separators: folder is pure kebab-case, file is pure snake_case.
344 - // Without this, a name like "My-Custom Widget" produces "my-custom_widget" (mixed).
345 - $folder_base = sanitize_file_name(strtolower(str_replace([' ', '_'], '-', $name)));
346 - $file_base = sanitize_file_name(strtolower(str_replace([' ', '-'], '_', $name)));
347 -
368 + // folder: "My-Custom-Card_<hash>" file: "My_Custom_Card_<hash>"
369 + // Case is PRESERVED. This used to lowercase both, which put widgets created here in a
370 + // different folder than the one the download/import/save paths write — the same widget
371 + // ended up in two directories differing only by case (ClickUp 86d3yk4yx).
348 372 // 8-character unique hex hash — matches the real WDesignKit widget format (e.g. "kmrd6l24")
349 373 $widget_hash = substr(md5(uniqid('wdkit', true)), 0, 8);
350 - $folder_name_full = $folder_base . '_' . $widget_hash;
351 - $file_name_full = $file_base . '_' . $widget_hash;
374 + $folder_name_full = wdesignkit_widget_folder_name($name, $widget_hash);
375 + $file_name_full = wdesignkit_widget_file_name($name, $widget_hash);
352 376
353 377 // widget_id = JUST the hash (matches the real widget JSON format: "widget_id":"20hzyv26")
354 378 $widget_id = $widget_hash;
355 379
@@ -422,14 +446,23 @@
422 446 $php_code = wdesignkit_mcp_generate_php(
423 447 $builder, $class_name, $name, $folder_name_full, $file_name_full,
424 448 $description, $category, $widget_hash, $version
425 449 );
426 - } elseif ($builder === 'elementor') {
427 - // Auto-correct common mistakes in AI-provided Elementor PHP:
428 - // wrong class name, wrong get_name() return value, missing asset-enqueue methods.
429 - $php_code = wdesignkit_mcp_fix_elementor_php(
430 - $php_code, $file_name_full, $widget_hash, $folder_name_full, $version
431 - );
450 + } else {
451 + // Caller-supplied PHP must live in the global namespace. Every builder's widget
452 + // loader instantiates/registers the class by its bare global name (e.g. the
453 + // Elementor loader calls `new Wdkit_{file}()`), so a `namespace ...;` declaration
454 + // makes the class resolve as Namespace\Wdkit_xxx and the loader fatals with
455 + // "Class not found", crashing every editor/frontend load. Strip it before writing.
456 + $php_code = wdesignkit_mcp_strip_php_namespace($php_code);
457 +
458 + if ($builder === 'elementor') {
459 + // Auto-correct common mistakes in AI-provided Elementor PHP:
460 + // wrong class name, wrong get_name() return value, missing asset-enqueue methods.
461 + $php_code = wdesignkit_mcp_fix_elementor_php(
462 + $php_code, $file_name_full, $widget_hash, $folder_name_full, $version
463 + );
464 + }
432 465 }
433 466
434 467 $js_code = (string) ($input['js_code'] ?? '');
435 468 if (empty($js_code)) {
@@ -519,8 +552,22 @@
519 552 if (file_put_contents($widget_file_base . '.css', $css_code) === false) {
520 553 $write_errors[] = 'CSS';
521 554 }
522 555
556 + // Thumbnail: create-widget has no image input and the JSON's w_image points at
557 + // {file}.png, so without this every new widget shows a broken image in the library
558 + // AND push-widget's pre-flight (which scans the folder for a jpg/png/webp) blocks the
559 + // push. Seed a real default thumbnail from the bundled placeholder so both work; the
560 + // caller can overwrite {file}.png later with a custom image. Best-effort — never fail
561 + // widget creation over the thumbnail. (bug: 86d3rc7n8)
562 + $thumb_written = false;
563 + if (defined('WDKIT_PATH')) {
564 + $default_thumb = WDKIT_PATH . 'assets/images/jpg/placeholder.png';
565 + if (is_readable($default_thumb)) {
566 + $thumb_written = @copy($default_thumb, $widget_file_base . '.png');
567 + }
568 + }
569 +
523 570 if (!empty($write_errors)) {
524 571 return [
525 572 'success' => false,
526 573 'message' => 'Failed to write files: ' . implode(', ', $write_errors) . '. Check directory permissions.',
@@ -526,8 +573,22 @@
526 573 'message' => 'Failed to write files: ' . implode(', ', $write_errors) . '. Check directory permissions.',
527 574 ];
528 575 }
529 576
577 + if (function_exists('wdesignkit_invalidate_widget_registry')) {
578 + wdesignkit_invalidate_widget_registry($builder);
579 + }
580 +
581 + $created_files = [
582 + $file_name_full . '.php',
583 + $file_name_full . '.json',
584 + $file_name_full . '.js',
585 + $file_name_full . '.css',
586 + ];
587 + if ($thumb_written) {
588 + $created_files[] = $file_name_full . '.png';
589 + }
590 +
530 591 return [
531 592 'success' => true,
532 593 'message' => "Widget '{$name}' created successfully for {$builder}.",
533 594 'widget' => [
@@ -534,14 +595,9 @@
534 595 'name' => $name,
535 596 'builder' => $builder,
536 597 'folder' => $folder_name_full,
537 598 'widget_id' => $widget_id, // 8-char hash — use with activate/deactivate-widget
538 - 'files' => [
539 - $file_name_full . '.php',
540 - $file_name_full . '.json',
541 - $file_name_full . '.js',
542 - $file_name_full . '.css',
543 - ],
599 + 'files' => $created_files,
544 600 'version' => $version,
545 601 'css_class' => $widget_css_class,
546 602 ],
547 603 ];
@@ -948,8 +1004,29 @@
948 1004 return $php_code;
949 1005 }
950 1006
951 1007 /**
1008 + * Strip a top-level `namespace ...;` declaration from caller-supplied widget PHP.
1009 + *
1010 + * Every WDesignKit widget loader (Elementor, Bricks, Gutenberg) instantiates or
1011 + * registers the widget class by its bare global name. A namespace declaration makes
1012 + * the class resolve as Namespace\Wdkit_xxx, so the loader's `new Wdkit_xxx()` fatals
1013 + * with "Class not found" and takes down the whole site. Generated boilerplate never
1014 + * declares a namespace — this only guards caller-supplied php_code.
1015 + *
1016 + * Only the common semicolon form (`namespace Foo\Bar;`) is removed; `use` statements
1017 + * are left intact as they remain valid global aliases.
1018 + */
1019 +function wdesignkit_mcp_strip_php_namespace(string $php_code): string {
1020 + return preg_replace(
1021 + '/\bnamespace\s+[A-Za-z0-9_\\\\]+\s*;/',
1022 + '',
1023 + $php_code,
1024 + 1
1025 + ) ?? $php_code;
1026 +}
1027 +
1028 +/**
952 1029 * Generate Gutenberg / Gutenberg Core block PHP boilerplate.
953 1030 *
954 1031 * Uses wp_upload_dir() for SSL-safe asset URLs.
955 1032 * Uses $file_name (includes unique hash) for function name — no collision possible.
@@ -968,8 +1045,48 @@
968 1045
969 1046 // Collision-safe function name: derived from full $file_name (includes unique hash)
970 1047 $func_name = str_replace(['-', '.'], '_', $file_name);
971 1048
1049 + $block_title = 'gutenberg' === $builder ? 'Nexter Blocks (TPGB) Block' : 'Gutenberg Block';
1050 +
1051 + // builder=gutenberg targets Nexter Blocks / TPGB. Its editor JS uses the Pmgc
1052 + // component framework, which lives in wkit_g_pmgc.js — registered under the handle
1053 + // 'wkit-editor-block-pmgc' by the WDesignKit gutenberg loader (editor_assets() in
1054 + // class-wdkit-gutenberg-files-load.php) and by nothing else. Declaring that handle
1055 + // unconditionally would make WordPress silently drop this entire script — and the
1056 + // block with it — on any site where WDesignKit is not active (WP_Dependencies::all_deps()
1057 + // discards a script whose dependency was never registered). Whether WDesignKit is
1058 + // present is a runtime fact of the destination site, so gate it on the constant.
1059 + // Mirrors the widget builder's own output in src/widget-builder/file-creation/gutenberg_file.js.
1060 + $deps_block = 'gutenberg' === $builder
1061 + ? <<<PHP
1062 +
1063 + \$wdkit_block_deps = [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ];
1064 +
1065 + if ( defined( 'WDKIT_VERSION' ) ) {
1066 + \$wdkit_block_deps[] = 'wkit-editor-block-pmgc';
1067 + }
1068 +
1069 +PHP
1070 + : '';
1071 +
1072 + $deps_arg = 'gutenberg' === $builder
1073 + ? '$wdkit_block_deps'
1074 + : "[ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ]";
1075 +
1076 + // The TPGB editor JS stores the per-instance id it styles through Pmgc_CssGenerator.
1077 + $attributes_block = 'gutenberg' === $builder
1078 + ? <<<PHP
1079 +
1080 + 'attributes' => [
1081 + 'block_id' => [
1082 + 'type' => 'string',
1083 + 'default' => '',
1084 + ],
1085 + ],
1086 +PHP
1087 + : '';
1088 +
972 1089 return <<<PHP
973 1090 <?php
974 1091 /*
975 1092 * Widget Name: {$name}
@@ -983,9 +1100,9 @@
983 1100 exit;
984 1101 }
985 1102
986 1103 /**
987 - * {$name} - Gutenberg Block
1104 + * {$name} - {$block_title}
988 1105 * {$description}
989 1106 */
990 1107 if ( ! function_exists( 'wdkit_{$func_name}_register_block' ) ) {
991 1108 function wdkit_{$func_name}_register_block() {
@@ -990,13 +1107,13 @@
990 1107 if ( ! function_exists( 'wdkit_{$func_name}_register_block' ) ) {
991 1108 function wdkit_{$func_name}_register_block() {
992 1109 \$upload_dir = wp_upload_dir();
993 1110 \$baseurl = set_url_scheme( \$upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );
994 -
1111 +{$deps_block}
995 1112 wp_register_script(
996 1113 'wdkit-{$slug}-editor',
997 1114 \$baseurl . '/wdesignkit/{$builder}/{$folder_name}/{$file_name}.js',
998 - [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ],
1115 + {$deps_arg},
999 1116 '{$version}',
1000 1117 true
1001 1118 );
1002 1119
@@ -1009,9 +1126,9 @@
1009 1126
1010 1127 register_block_type( 'wdkit/{$slug}', [
1011 1128 'editor_script' => 'wdkit-{$slug}-editor',
1012 1129 'editor_style' => 'wdkit-{$slug}-style',
1013 - 'style' => 'wdkit-{$slug}-style',
1130 + 'style' => 'wdkit-{$slug}-style',{$attributes_block}
1014 1131 ] );
1015 1132 }
1016 1133 }
1017 1134 add_action( 'init', 'wdkit_{$func_name}_register_block' );
@@ -1172,11 +1289,127 @@
1172 1289 // WDesignKit executes widget JS in the element_ready scope automatically.
1173 1290 return '';
1174 1291 }
1175 1292
1176 - if ($builder === 'gutenberg' || $builder === 'gutenberg_core') {
1293 + if ($builder === 'gutenberg') {
1294 + // Nexter Blocks (TPGB) variant. The difference from gutenberg_core is the
1295 + // component framework: the inspector is built from Pmgc_PanelTabs/Pmgc_Tab and
1296 + // per-instance CSS is emitted through Pmgc_CssGenerator, all served by
1297 + // wkit_g_pmgc.js. Every Pmgc component is used behind a guard with a native
1298 + // fallback, so the block still registers and renders when Nexter Blocks is not
1299 + // installed. Mirrors src/widget-builder/file-creation/gutenberg_file.js.
1300 + $pmgc_id = $widget_hash !== '' ? 'wb-' . $widget_hash : 'wb-' . $slug;
1301 +
1177 1302 return <<<JS
1178 1303 /**
1304 + * {$name} - Nexter Blocks (TPGB) Block
1305 + */
1306 +(function(blocks, element, blockEditor, components, i18n) {
1307 + var el = element.createElement;
1308 + var Fragment = element.Fragment;
1309 + var __ = i18n.__;
1310 + var InspectorControls = blockEditor.InspectorControls;
1311 + var TextControl = components.TextControl;
1312 + var TextareaControl = components.TextareaControl;
1313 + var PanelBody = components.PanelBody;
1314 +
1315 + // Registered on window.wp by wkit_g_pmgc.js (script handle 'wkit-editor-block-pmgc'),
1316 + // which the WDesignKit gutenberg loader enqueues in the block editor.
1317 + var PmgcComponents = window.wp.Pmgc_Components || {};
1318 + var Pmgc_PanelTabs = PmgcComponents.Pmgc_PanelTabs;
1319 + var Pmgc_Tab = PmgcComponents.Pmgc_Tab;
1320 + var Pmgc_CssGenerator = PmgcComponents.Pmgc_CssGenerator;
1321 +
1322 + blocks.registerBlockType('wdkit/{$slug}', {
1323 + title: __('{$safe_name}', 'wdesignkit'),
1324 + description: __('{$safe_desc}', 'wdesignkit'),
1325 + icon: el('i', { className: 'wdkit-icon tpae-wdkit-logo' }),
1326 + category: '{$safe_cat}',
1327 + attributes: {
1328 + block_id: { type: 'string', default: '' },
1329 + title: { type: 'string', default: '{$safe_name}' },
1330 + description: { type: 'string', default: '{$safe_desc}' }
1331 + },
1332 +
1333 + edit: function(props) {
1334 + var attributes = props.attributes;
1335 + var block_id = props.clientId.substr(0, 6);
1336 +
1337 + if (attributes.block_id !== block_id) {
1338 + props.setAttributes({ block_id: block_id });
1339 + }
1340 +
1341 + var settings = [
1342 + el(TextControl, {
1343 + key: 'title',
1344 + label: __('Title', 'wdesignkit'),
1345 + value: attributes.title,
1346 + onChange: function(val) { props.setAttributes({ title: val }); }
1347 + }),
1348 + el(TextareaControl, {
1349 + key: 'description',
1350 + label: __('Description', 'wdesignkit'),
1351 + value: attributes.description,
1352 + onChange: function(val) { props.setAttributes({ description: val }); }
1353 + })
1354 + ];
1355 +
1356 + // Nexter Blocks tabbed inspector when TPGB is available, native panel otherwise.
1357 + var inspector = (Pmgc_PanelTabs && Pmgc_Tab)
1358 + ? el(InspectorControls, null,
1359 + el(Pmgc_PanelTabs, null,
1360 + el(Pmgc_Tab, { tabTitle: __('Layout', 'wdesignkit') }, settings),
1361 + el(Pmgc_Tab, { tabTitle: __('Style', 'wdesignkit') })
1362 + )
1363 + )
1364 + : el(InspectorControls, null,
1365 + el(PanelBody, { title: __('{$safe_name} Settings', 'wdesignkit') }, settings)
1366 + );
1367 +
1368 + if (attributes.block_id && Pmgc_CssGenerator) {
1369 + var node = document.getElementsByClassName('tpgb-block-' + block_id);
1370 + if (null != node && 'undefined' !== typeof node) {
1371 + Pmgc_CssGenerator(attributes, 'wdkit', '{$pmgc_id}', block_id, false, props.clientId);
1372 + }
1373 + }
1374 +
1375 + return el(Fragment, null, inspector,
1376 + el('div', { className: 'wkit-wb-Widget_{$widget_hash} wdkit-block-' + block_id },
1377 + el('h3', {}, attributes.title),
1378 + el('p', {}, attributes.description)
1379 + )
1380 + );
1381 + },
1382 +
1383 + save: function(props) {
1384 + var attributes = props.attributes;
1385 + var block_id = attributes.block_id;
1386 + var styleCss = Pmgc_CssGenerator
1387 + ? Pmgc_CssGenerator(attributes, 'wdkit', '{$pmgc_id}', block_id, true)
1388 + : '';
1389 +
1390 + return el(Fragment, null,
1391 + el('div', { className: 'wkit-wb-Widget_{$widget_hash} wdkit-block-' + block_id },
1392 + el('h3', {}, attributes.title),
1393 + el('p', {}, attributes.description)
1394 + ),
1395 + el('style', null, styleCss)
1396 + );
1397 + }
1398 + });
1399 +})(
1400 + window.wp.blocks,
1401 + window.wp.element,
1402 + window.wp.blockEditor,
1403 + window.wp.components,
1404 + window.wp.i18n
1405 +);
1406 +JS;
1407 + }
1408 +
1409 + if ($builder === 'gutenberg_core') {
1410 + return <<<JS
1411 +/**
1179 1412 * {$name} - Gutenberg Block
1180 1413 */
1181 1414 (function(blocks, element, blockEditor, components, i18n) {
1182 1415 var el = element.createElement;
@@ -2238,9 +2471,9 @@
2238 2471 if ($wdk_type === '') {
2239 2472 return null; // Unsupported type — skip
2240 2473 }
2241 2474
2242 - $json_name = $wdk_type . '_' . $php_name;
2475 + $json_name = $php_name;
2243 2476 $label = wdesignkit_mcp_php_get_string($args, 'label');
2244 2477
2245 2478 $ctrl = [
2246 2479 '_php_name' => $php_name, // temp — removed after map update