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-push-widget.php +102 -19 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/push-widget', [
13 - 'label' => __('Push WDesignKit Widget to Cloud', 'sprout-mcp'),
13 + 'label' => __('Push WDesignKit Widget to Cloud', 'wdesignkit'),
14 14 'description' => __(
15 15 'Uploads a local widget to the WDesignKit cloud marketplace. Reads the widget files from disk, packages them, and posts to the save_widget endpoint. Requires cloud login. After a successful push the cloud returns a marketplace record ID (r_id) which is written back to the local JSON so future version checks work correctly.',
16 - 'sprout-mcp',
16 + 'wdesignkit',
17 17 ),
18 18 'category' => 'wdesignkit',
19 19 'input_schema' => [
20 20 'type' => 'object',
@@ -41,14 +41,16 @@
41 41 'type' => 'object',
42 42 'properties' => [
43 43 'success' => ['type' => 'boolean'],
44 44 'message' => ['type' => 'string'],
45 + 'r_id' => ['type' => 'integer', 'description' => 'Cloud marketplace record ID on success (0 if the cloud did not return one). Use this — not a value dug out of "response" — with wdesignkit/download-widget or wdesignkit/manage-workspace-widget to reference this widget afterwards.'],
45 46 'response' => ['type' => 'object'],
46 47 ],
47 48 ],
48 49 'execute_callback' => 'wdesignkit_mcp_push_widget',
49 - 'permission_callback' => 'sprout_mcp_permission_callback',
50 + 'permission_callback' => 'wdesignkit_mcp_permission_callback',
50 51 'meta' => [
52 + 'public' => true,
51 53 'show_in_rest' => true,
52 54 'mcp' => ['public' => true],
53 55 'annotations' => [
54 56 'instructions' => implode("\n", [
@@ -54,8 +56,10 @@
54 56 'instructions' => implode("\n", [
55 57 'Pushes a local widget to the WDesignKit cloud marketplace. Requires login.',
56 58 'type defaults to "new". If the widget already has an r_id in its JSON, use "update".',
57 59 'After a successful push the local JSON is updated with the returned r_id and cloud image URL.',
60 + 'The response\'s top-level "r_id" field is the value to use afterwards — with wdesignkit/download-widget,',
61 + 'wdesignkit/manage-workspace-widget, or wdesignkit/get-my-cloud-widgets to find it again later.',
58 62 ]),
59 63 'readonly' => false,
60 64 'destructive' => false,
61 65 'idempotent' => false,
@@ -118,28 +122,72 @@
118 122 if (!is_array($json_data)) {
119 123 return ['success' => false, 'message' => "Could not read JSON config from {$builder}/{$folder}"];
120 124 }
121 125
122 - $widgetdata = $json_data['widget_data']['widgetdata'] ?? [];
123 - $title = sanitize_text_field((string) ($widgetdata['name'] ?? ''));
124 - $widget_id = sanitize_text_field((string) ($widgetdata['widget_id'] ?? ''));
125 - $r_id = (int) ($widgetdata['r_id'] ?? 0);
126 - $w_version = $widgetdata['widget_version'] ?? '1.0.0';
126 + $widgetdata = $json_data['widget_data']['widgetdata'] ?? [];
127 + $title = sanitize_text_field((string) ($widgetdata['name'] ?? ''));
128 + $widget_id = sanitize_text_field((string) ($widgetdata['widget_id'] ?? ''));
129 + $r_id = (int) ($widgetdata['r_id'] ?? 0);
130 + $w_version = (string) ($widgetdata['widget_version'] ?? '1.0.0');
131 + $version_detail = $widgetdata['version_detail'] ?? [];
132 + if (!is_array($version_detail)) {
133 + $version_detail = [];
134 + }
127 135
128 136 if ($title === '' || $widget_id === '') {
129 137 return ['success' => false, 'message' => 'Widget JSON is missing name or widget_id.'];
130 138 }
131 139
140 + // Pre-flight: version must be a non-empty semver string (x.y.z).
141 + // The cloud SetSaveWidget() endpoint rejects empty or non-semver versions with
142 + // "Not allowed version number". Catch this locally before wasting a round-trip.
143 + if ($w_version === '' || !preg_match('/^\d+\.\d+\.\d+$/', $w_version)) {
144 + return [
145 + 'success' => false,
146 + 'message' => "Widget '{$title}' has an invalid widget_version '{$w_version}'. Set a valid semver version (e.g. '1.0.0') using wdesignkit/update-widget before pushing.",
147 + ];
148 + }
149 +
150 + // Pre-flight: re-push version check.
151 + // When r_id > 0 the widget is already in the marketplace. The cloud rejects
152 + // pushes where the incoming w_version matches the already-published version.
153 + // The UI enforces a "new version must be higher" rule — replicate it here so
154 + // the caller gets a clear message instead of the opaque "Not allowed version number".
155 + // We cannot know the exact cloud version without a network call, but we can
156 + // detect the case where version_detail has only one entry (never bumped) and
157 + // r_id > 0, which is the most common trigger of this error.
158 + if ($r_id > 0 && count($version_detail) <= 1 && $w_version === '1.0.0') {
159 + return [
160 + 'success' => false,
161 + 'message' => "Widget '{$title}' (r_id: {$r_id}) appears to already be published at version '1.0.0'. Bump widget_version (e.g. to '1.0.1') and update version_detail using wdesignkit/update-widget before re-pushing.",
162 + 'hint' => 'The cloud rejects re-pushes with the same version number. Increment widget_version in the JSON config first.',
163 + ];
164 + }
165 +
166 + // Thumbnail is OPTIONAL, matching the manual "Push to Cloud" UI. SetSaveWidget
167 + // (wdk-server WdkitPluginController) reads w_image with a '' default and only stores
168 + // a file inside if( !empty($w_image) ) — it never validates or rejects an imageless
169 + // push. Blocking here made this ability stricter than the product it wraps. (bug: 86d3rc7q4)
132 170 $w_image_body = '';
133 171 $w_imgext = '';
172 +
134 173 if ($img_path && file_exists($img_path)) {
135 174 $w_image_body = @file_get_contents($img_path);
136 175 $w_imgext = $img_ext;
176 +
177 + if (false === $w_image_body) {
178 + $w_image_body = '';
179 + $w_imgext = '';
180 + }
137 181 }
138 182
139 183 $array_data = [
140 184 'token' => $auth['token'],
141 - 'type' => $type,
185 + // The cloud save_widget endpoint only understands 'add' — it determines
186 + // new vs update by checking whether the widget already exists in the DB
187 + // (matched by w_unique). The MCP 'new'/'update' distinction is irrelevant
188 + // to the server; always send 'add'.
189 + 'type' => 'add',
142 190 'title' => $title,
143 191 'content' => '',
144 192 'builder' => $builder,
145 193 'w_data' => wp_json_encode($json_data),
@@ -146,9 +194,13 @@
146 194 'w_unique' => $widget_id,
147 195 'w_image' => $w_image_body,
148 196 'w_imgext' => $w_imgext,
149 197 'w_version' => $w_version,
150 - 'w_updates' => serialize([]),
198 + // Use the widget's actual version_detail array (mirrors the JS frontend which sends
199 + // all_files.WcardData.widgetdata.version_detail). Sending serialize([]) was causing
200 + // "Not allowed version number" from the cloud because an empty w_updates signals
201 + // no version history, which the server treats as an invalid push.
202 + 'w_updates' => serialize($version_detail),
151 203 'r_id' => $r_id,
152 204 'unique_id' => get_option('wdkit_unique_id', ''),
153 205 ];
154 206
@@ -169,15 +221,15 @@
169 221 $body = wp_remote_retrieve_body($response);
170 222 $data = json_decode($body, true);
171 223
172 224 // HTTP 200 with an empty/null body ( [], '', null ) means the cloud silently rejected
173 - // the request — common when the widget is missing a thumbnail image, the account
174 - // lacks marketplace publish rights, or a required metadata field is absent.
175 - // Guard covers both json_decode('[]') → [] and json_decode('') → null.
225 + // the request — the account lacks marketplace publish rights, or a required metadata
226 + // field is absent. (A missing thumbnail is NOT a cause: save_widget stores the image
227 + // only when one is sent.) Guard covers both json_decode('[]') → [] and json_decode('') → null.
176 228 if (200 === (int) $status && ($data === null || (is_array($data) && empty($data)))) {
177 229 return [
178 230 'success' => false,
179 - 'message' => 'Invalid API response format: empty array. The cloud returned HTTP 200 but an empty response body. Check that the widget has a thumbnail image, that the WDesignKit account has marketplace publish rights, and that all required metadata fields (name, widget_id, widget_version) are present.',
231 + 'message' => 'Invalid API response format: empty array. The cloud returned HTTP 200 but an empty response body. Check that the WDesignKit account has marketplace publish rights and that all required metadata fields (name, widget_id, widget_version) are present.',
180 232 'response' => ['raw' => $body],
181 233 ];
182 234 }
183 235
@@ -182,25 +234,52 @@
182 234 }
183 235
184 236 if (200 !== (int) $status || empty($data['success'])) {
185 237 $raw_msg = is_array($data) ? ($data['massage'] ?? $data['message'] ?? null) : null;
238 + $msg = is_string($raw_msg) ? $raw_msg : (is_null($raw_msg) ? "Cloud returned status {$status}." : json_encode($raw_msg));
239 +
240 + // Enrich version-rejection errors with actionable guidance.
241 + // The cloud returns "Not allowed version number" when the incoming w_version
242 + // matches or is lower than the already-published version for this widget.
243 + if (stripos($msg, 'version') !== false && stripos($msg, 'not allowed') !== false) {
244 + $msg .= " To fix: use wdesignkit/update-widget to bump widget_version (e.g. '1.0.0' → '1.0.1') and add a new entry to version_detail, then push again.";
245 + }
246 +
186 247 return [
187 248 'success' => false,
188 - 'message' => is_string($raw_msg) ? $raw_msg : (is_null($raw_msg) ? "Cloud returned status {$status}." : json_encode($raw_msg)),
249 + 'message' => $msg,
189 250 'response' => wdesignkit_mcp_ensure_object($data, $body),
190 251 ];
191 252 }
192 253
193 - // Write back r_id and updated image URL to local JSON
254 + // Write back r_id and updated image URL to local JSON.
255 + // $res_data IS the cloud's "data" object already ({imgurl, id}) — reading a further
256 + // ['data'] level off of it (the previous code) always missed, so img_url and the id were
257 + // silently '' / 0 on every push regardless of what the cloud sent. 'id' is kit_widgets.id
258 + // (added server-side in SetSaveWidget) — the identifier download-widget / get-my-cloud-widgets
259 + // / manage-workspace-widget all key on; it is NOT the same thing as the widgetdata.r_id this
260 + // ability's own re-publish check reads (that column tracks copy lineage, not "my widget's id"),
261 + // but this ability's response has always called it r_id, so keep that external name.
194 262 $res_data = is_array($data['data'] ?? null) ? $data['data'] : [];
195 - $img_url = $res_data['data']['imgurl'] ?? '';
196 - $new_r_id = $res_data['data']['r_id'] ?? ($res_data['r_id'] ?? 0);
263 + $img_url = $res_data['imgurl'] ?? '';
264 + $new_r_id = $res_data['id'] ?? ($res_data['r_id'] ?? 0);
197 265
266 + $json_dirty = false;
267 +
198 268 if ($new_r_id) {
199 269 $json_data['widget_data']['widgetdata']['r_id'] = $new_r_id;
270 + $json_dirty = true;
200 271 }
201 - if ($img_url && $json_path) {
272 + if ($img_url) {
202 273 $json_data['widget_data']['widgetdata']['w_image'] = $img_url;
274 + $json_dirty = true;
275 + }
276 +
277 + // The write must run whenever EITHER field changed — previously it only ran inside the
278 + // $img_url check, so an imageless push (now valid: the thumbnail pre-flight was removed
279 + // in 86d3rc7q4) got a real r_id back from the cloud but never persisted it, leaving the
280 + // widget permanently unrecognized as already-published on every later push/download call.
281 + if ($json_dirty && $json_path) {
203 282 @file_put_contents($json_path, wp_json_encode($json_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
204 283 }
205 284
206 285 return [
@@ -205,7 +284,11 @@
205 284
206 285 return [
207 286 'success' => true,
208 287 'message' => "Widget '{$title}' pushed to cloud." . ($new_r_id ? " Marketplace record ID: {$new_r_id}." : ''),
288 + // Surfaced as a first-class field — callers previously had to dig it out of the raw
289 + // cloud response shape (response.data.data.r_id), which QA correctly flagged as
290 + // effectively undiscoverable.
291 + 'r_id' => (int) $new_r_id,
209 292 'response' => wdesignkit_mcp_ensure_object($data, $body),
210 293 ];
211 294 }