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 / templates / wdesignkit-set-template-link.php

wdesignkit-set-template-link.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.1, at includes/abilities/templates/wdesignkit-set-template-link.php

547 lines 27.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Set ONE link field on ONE template, directly.
4 *
5 * Deliberately explicit: no patterns, no title guessing, no dry run by default. You name the
6 * template, and either give the URL outright or name the template to point at — the URL is then
7 * built from that target and its builder verified.
8 *
9 * Same whole-row hazard applies as everywhere else on this endpoint: updatetemplatesid rewrites
10 * every column on each call, defaulting anything the request omits. So the current row is read
11 * first and re-sent verbatim, with only the chosen link overlaid. post_image / post_otherimages
12 * are deliberately not re-sent — the controller restores both from the database itself.
13 *
14 * Reads through /templates/view (the same endpoint the admin edit screen uses) rather than the
15 * account-wide list, because get_user_info has been observed returning a truncated set.
16 */
17
18 declare(strict_types=1);
19
20 if (!defined('ABSPATH')) {
21 exit();
22 }
23
24 if (!function_exists('wdesignkit_mcp_link_slug')) {
25 /**
26 * Slugify exactly as the site does (helpers.jsx RemoveExtraSpace).
27 */
28 function wdesignkit_mcp_link_slug(string $title): string {
29 $slug = preg_replace('/\s+/', '-', $title);
30 $slug = preg_replace('/[^\w-]+/', '', (string) $slug);
31
32 return strtolower((string) $slug);
33 }
34 }
35
36 if (!function_exists('wdesignkit_mcp_ids_to_csv')) {
37 /**
38 * Normalise an array / PHP-serialized string / CSV string into a CSV of positive int IDs.
39 */
40 function wdesignkit_mcp_ids_to_csv($value): string {
41 if (is_string($value)) {
42 // allowed_classes => false: these values are terms_id / tags_id / plugins_id /
43 // plugin_licence straight out of the cloud /templates/view response. Without it a
44 // hostile or MITM'd response could hand us a serialized object and have its magic
45 // methods run on instantiation (PHP object injection, CWE-502, ClickUp 86d41cczq).
46 // Only ID lists are ever expected here, so no class needs to survive.
47 $unserialized = @unserialize($value, ['allowed_classes' => false]);
48 $value = (false !== $unserialized || 'b:0;' === $value) ? $unserialized : explode(',', $value);
49 }
50 if (!is_array($value)) {
51 return '';
52 }
53
54 $ids = [];
55 foreach ($value as $id) {
56 $id = (int) $id;
57 if ($id > 0) {
58 $ids[] = $id;
59 }
60 }
61
62 return implode(',', array_values(array_unique($ids)));
63 }
64 }
65
66 if (!function_exists('wdesignkit_mcp_template_id_from_url')) {
67 /**
68 * Pull a template ID out of a WDesignKit URL — either the admin edit link
69 * (…/admin/packs/view/22870) or the public template link
70 * (…/templates/page/some-slug/22870).
71 *
72 * @return int The ID, or 0 when the URL is not a recognisable WDesignKit template URL.
73 */
74 function wdesignkit_mcp_template_id_from_url(string $url): int {
75 if (preg_match('#/admin/packs/view/(\d+)#', $url, $m)) {
76 return (int) $m[1];
77 }
78 if (preg_match('#/templates/(?:page|section|kit)/[^/]*/(\d+)#', $url, $m)) {
79 return (int) $m[1];
80 }
81
82 return 0;
83 }
84 }
85
86 if (!function_exists('wdesignkit_mcp_fetch_template_row')) {
87 /**
88 * Fetch a single template via /templates/view.
89 *
90 * @return array|null The template row, or null when it cannot be read.
91 */
92 function wdesignkit_mcp_fetch_template_row(string $token, int $template_id): ?array {
93 if (!defined('WDKIT_SERVER_API_URL')) {
94 return null;
95 }
96
97 $response = wp_remote_post(
98 WDKIT_SERVER_API_URL . 'api/templates/view',
99 ['method' => 'POST', 'body' => ['token' => $token, 'id' => $template_id], 'timeout' => 60]
100 );
101
102 if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) {
103 return null;
104 }
105
106 $data = json_decode(wp_remote_retrieve_body($response), true);
107 if (!is_array($data) || empty($data['success']) || !is_array($data['data'] ?? null)) {
108 return null;
109 }
110
111 return $data['data'];
112 }
113 }
114
115 if (!function_exists('wdesignkit_mcp_template_preserve_args')) {
116 /**
117 * Rebuild the FULL updatetemplatesid payload from a template's current values.
118 *
119 * updatetemplatesid writes every column in its $NewData array on every call, falling back to a
120 * hardcoded default for anything absent — so omitting a field here silently blanks it. Only
121 * post_image and post_otherimages are safe to leave out (the controller re-reads both).
122 *
123 * @param array $row Current template row.
124 * @param string $token Cloud auth token.
125 * @return array Request args ready to POST, before any overlay.
126 */
127 function wdesignkit_mcp_template_preserve_args(array $row, string $token): array {
128 $title = (string) ($row['title'] ?? '');
129
130 return [
131 'token' => $token,
132 'id' => (int) ($row['id'] ?? 0),
133 'title' => $title,
134 'description' => (string) ($row['post_content'] ?? ''),
135 'subcontent' => (string) ($row['post_subcontent'] ?? ''),
136 'url' => (string) ($row['post_url'] ?? ''),
137 'feature' => (string) ($row['post_feature'] ?? ''),
138 'status' => (string) ($row['post_status'] ?? 'private'),
139 'post_display' => (string) ($row['post_display'] ?? 'show'),
140 'collection' => (string) ($row['post_collection'] ?? ''),
141 'ModifiedDate' => (string) ($row['post_modified'] ?? ''),
142 'type' => (string) ($row['type'] ?? ''),
143 'Builder' => (string) ($row['post_builder'] ?? ''),
144 'freepro' => (string) ($row['free_pro'] ?? 'free'),
145 'views' => (string) ($row['post_views'] ?? 0),
146 'Download' => (string) ($row['post_download'] ?? 0),
147 'average' => (string) ($row['avg_rating'] ?? 0),
148 'elementor_url' => (string) ($row['elementor_url'] ?? ''),
149 'gutenberg_url' => (string) ($row['gutenberg_url'] ?? ''),
150 'figma_url' => (string) ($row['figma_url'] ?? ''),
151 'keywords' => is_array($row['key_words'] ?? null) ? implode(',', $row['key_words']) : (string) ($row['key_words'] ?? ''),
152 'ai_compatible' => (string) ($row['ai_compatible'] ?? 'no'),
153 'color_palette' => (string) ($row['color_palette'] ?? 'no'),
154 'color_palette_id' => (string) ($row['color_palette_id'] ?? ''),
155 'categorylist' => wdesignkit_mcp_ids_to_csv($row['terms_id'] ?? []),
156 'tagslist' => wdesignkit_mcp_ids_to_csv($row['tags_id'] ?? []),
157 'pluginlist' => wdesignkit_mcp_ids_to_csv($row['plugins_id'] ?? []),
158 // The view endpoint spells this plugin_licence; the list spells it pluginlicence.
159 'PluginLicence' => wdesignkit_mcp_ids_to_csv($row['plugin_licence'] ?? $row['pluginlicence'] ?? []),
160 'Filename' => $title,
161 'OldFileName' => $title,
162 ];
163 }
164 }
165
166 wp_register_ability('wdesignkit/set-template-link', [
167 'label' => __('Set WDesignKit Template Link', 'wdesignkit'),
168 'description' => __(
169 'Sets one link field (Live Demo URL, Elementor/Gutenberg cross-link, or Figma URL) on one template, directly. Give the template id, the field, and either an explicit url OR a target_template_id to link to — when you give a target, the URL is built for you and the target is verified to exist and to be the expected builder. Every other field on the template is read and re-sent unchanged so the cloud\'s whole-row overwrite cannot blank its title, categories, tags, plugins or copy.',
170 'wdesignkit',
171 ),
172 'category' => 'wdesignkit',
173 'input_schema' => [
174 'type' => 'object',
175 'properties' => [
176 'template_id' => [
177 'type' => 'integer',
178 'description' => 'The cloud template ID to update — the template the link is being ADDED TO. Optional if you pass template_url instead.',
179 ],
180 'template_url' => [
181 'type' => 'string',
182 'description' => 'Alternative to template_id: the URL of the template being updated, with its ID read out of it. Accepts either the admin edit link (https://wdesignkit.com/admin/packs/view/22870) or the public template link (https://wdesignkit.com/templates/page/some-slug/22870) — so you can paste straight from the browser instead of hunting for IDs. Ignored when template_id is given.',
183 ],
184 'link_field' => [
185 'type' => 'string',
186 'description' => 'Which link to set. OPTIONAL for cross-links: when omitted it is inferred from the template\'s OWN builder — an Elementor template stores its counterpart under gutenberg_url, a Gutenberg template under elementor_url — so passing just template_id plus the other builder\'s URL is enough. Give it explicitly for demo_url (Live Demo URL) or figma_url, which cannot be inferred.',
187 'enum' => ['demo_url', 'elementor_url', 'gutenberg_url', 'figma_url'],
188 ],
189 'target_template_id' => [
190 'type' => 'integer',
191 'description' => 'The template to LINK TO — normally the other builder\'s version. Its URL is built automatically (see url_format) and it is checked to exist and to be the builder link_field implies, so you cannot silently point a gutenberg_url at another Elementor template. Use this for cross-links instead of typing a URL by hand. Ignored when url is given.',
192 ],
193 'url' => [
194 'type' => 'string',
195 'description' => 'An explicit URL to store. Takes precedence over target_template_id, and is the natural way to pass a link you already have in the address bar. When it is a WDesignKit ADMIN link (…/admin/packs/view/{id}) for a cross-link field, it is rewritten to the equivalent PUBLIC template URL before saving — these links are shown to visitors, who would otherwise land on a login screen; pass url_format="admin" to keep the admin link as-is. External URLs (a demo on another domain, a Figma file) are stored untouched.',
196 ],
197 'url_format' => [
198 'type' => 'string',
199 'description' => 'Which URL shape to build from target_template_id. "public" (DEFAULT) -> https://wdesignkit.com/templates/{type_path}/{slug}/{id}, matching the site\'s own getFrontendUrl(). This is the correct choice for elementor_url/gutenberg_url/figma_url, because those render on the PUBLIC template page as the "Other Available Versions" builder icons (templates/page/[tempName]/[id]/single-page.jsx) — visitor-facing links. "admin" -> https://wdesignkit.com/admin/packs/view/{id}, the admin edit screen, which requires the owner to be logged in; only useful for internal cross-referencing, never for links shown to visitors. NOTE: a public URL only resolves once the TARGET template is published — templates save as private, and the admin UI only exposes the preview link when post_status is publish. Ignored when url is given.',
200 'enum' => ['public', 'admin'],
201 ],
202 'dry_run' => [
203 'type' => 'boolean',
204 'description' => 'When true, resolve and validate everything and report what would change, without writing. Defaults to FALSE — this touches a single named template, so it acts directly.',
205 ],
206 ],
207 // Neither key is listed as required: identify the template with EITHER template_id or
208 // template_url, and give the destination as EITHER target_template_id or url. The callback
209 // enforces that one of each pair is present.
210 'required' => [],
211 'additionalProperties' => false,
212 ],
213 'output_schema' => [
214 'type' => 'object',
215 'properties' => [
216 'success' => ['type' => 'boolean'],
217 'message' => ['type' => 'string'],
218 'template_id' => ['type' => 'integer'],
219 'link_field' => ['type' => 'string'],
220 'from' => ['type' => 'string'],
221 'to' => ['type' => 'string'],
222 'linked_to' => ['type' => ['object', 'null']],
223 'dry_run' => ['type' => 'boolean'],
224 ],
225 ],
226 'execute_callback' => 'wdesignkit_mcp_set_template_link',
227 'permission_callback' => 'wdesignkit_mcp_permission_callback',
228 'meta' => [
229 'public' => true,
230 'show_in_rest' => true,
231 'mcp' => ['public' => true],
232 'annotations' => [
233 'instructions' => implode("\n", [
234 'Sets ONE link on ONE template. Requires WDesignKit login.',
235 'Identify the template with template_id OR template_url; give the destination as',
236 'target_template_id OR url. The simplest call is two browser URLs — the first names the',
237 'template to update, the second is the link to store:',
238 ' template_url=https://wdesignkit.com/admin/packs/view/22870',
239 ' url=https://wdesignkit.com/admin/packs/view/22871',
240 ' -> template 22870 now links to 22871 as its Gutenberg version (field inferred, builder checked).',
241 'Pass url instead when the destination is not another template (external demo, Figma file).',
242 'The target is rejected if it does not exist, if it is the same template (a self-link), or if',
243 'its builder does not match what link_field implies — those are the mistakes worth catching.',
244 'Call it once per template when several need updating.',
245 ]),
246 'readonly' => false,
247 'destructive' => false,
248 'idempotent' => true,
249 ],
250 ],
251 ]);
252
253 function wdesignkit_mcp_set_template_link(array $input): array {
254 set_time_limit(120);
255
256 $auth = wdesignkit_mcp_template_get_auth();
257 if (empty($auth['logged_in'])) {
258 return ['success' => false, 'message' => $auth['message'] ?? 'Not logged in.'];
259 }
260 $token = $auth['token'];
261
262 if (!defined('WDKIT_SERVER_API_URL')) {
263 return ['success' => false, 'message' => 'WDesignKit plugin core not loaded.'];
264 }
265
266 $template_id = (int) ($input['template_id'] ?? 0);
267
268 // Accept the template's own URL in place of its ID, so both ends of a cross-link can be
269 // pasted straight from the browser.
270 if ($template_id <= 0 && !empty($input['template_url'])) {
271 $template_url = sanitize_url((string) $input['template_url']);
272 $template_id = wdesignkit_mcp_template_id_from_url($template_url);
273
274 if ($template_id <= 0) {
275 return [
276 'success' => false,
277 'message' => 'Could not read a template ID out of template_url. Expected a link like https://wdesignkit.com/admin/packs/view/22870 or https://wdesignkit.com/templates/page/some-slug/22870.',
278 ];
279 }
280 }
281 $link_field = sanitize_text_field((string) ($input['link_field'] ?? ''));
282 $target_id = (int) ($input['target_template_id'] ?? 0);
283 $url_literal = isset($input['url']) ? sanitize_url((string) $input['url']) : '';
284 // Defaults to the PUBLIC form: these link fields render as visitor-facing "Other Available
285 // Versions" icons on the public template page, where an admin URL would hit a login wall.
286 $url_format = sanitize_text_field((string) ($input['url_format'] ?? 'public'));
287 $dry_run = (bool) ($input['dry_run'] ?? false);
288
289 $field_map = [
290 'demo_url' => ['column' => 'post_url', 'param' => 'url'],
291 'elementor_url' => ['column' => 'elementor_url', 'param' => 'elementor_url'],
292 'gutenberg_url' => ['column' => 'gutenberg_url', 'param' => 'gutenberg_url'],
293 'figma_url' => ['column' => 'figma_url', 'param' => 'figma_url'],
294 ];
295 if ($template_id <= 0) {
296 return ['success' => false, 'message' => 'Identify the template to update: pass template_id, or template_url (its admin or public link).'];
297 }
298 if ($target_id <= 0 && $url_literal === '') {
299 return ['success' => false, 'message' => 'Provide either target_template_id (the template to link to) or url (an explicit link).'];
300 }
301 if ($target_id > 0 && $target_id === $template_id) {
302 return ['success' => false, 'message' => 'target_template_id is the same as template_id — that would make the template link to itself.'];
303 }
304
305 $row = wdesignkit_mcp_fetch_template_row($token, $template_id);
306 if (null === $row) {
307 return [
308 'success' => false,
309 'message' => 'Could not read template ' . $template_id . '. It may not exist, or may belong to a different account than the one this site is logged into.',
310 ];
311 }
312
313 // link_field is optional for cross-links: a template's OWN builder determines which column the
314 // cross-link belongs in — an Elementor template stores its counterpart under gutenberg_url and
315 // vice versa. So "here is template X and the URL of its other-builder version" is unambiguous.
316 $auto_field = false;
317 if ($link_field === '') {
318 $own_builder = (string) ($row['post_builder'] ?? '');
319 $derived_field = ['1001' => 'gutenberg_url', '1002' => 'elementor_url'][$own_builder] ?? '';
320
321 if ($derived_field === '') {
322 return [
323 'success' => false,
324 'message' => sprintf(
325 'Cannot infer which link field to use: template %d has builder "%s", which is neither Elementor (1001) nor Gutenberg (1002). Pass link_field explicitly.',
326 $template_id,
327 $own_builder === '' ? 'unset' : $own_builder
328 ),
329 ];
330 }
331
332 $link_field = $derived_field;
333 $auto_field = true;
334 }
335
336 if (!isset($field_map[$link_field])) {
337 return ['success' => false, 'message' => 'link_field must be one of: demo_url, elementor_url, gutenberg_url, figma_url.'];
338 }
339
340 $link_column = $field_map[$link_field]['column'];
341 $link_param = $field_map[$link_field]['param'];
342
343 $linked_to = null;
344 $new_url = $url_literal;
345 $normalised_from_admin = false;
346
347 // A raw url is an opaque string — nothing about it says which builder it belongs to, so a URL
348 // for the WRONG builder would be written without complaint. When the URL is recognisably a
349 // WDesignKit template link, pull the id out of it and verify the target's builder anyway.
350 if ($url_literal !== '') {
351 $is_cross = in_array($link_field, ['elementor_url', 'gutenberg_url'], true);
352 $embedded_id = wdesignkit_mcp_template_id_from_url($url_literal);
353
354 if ($embedded_id > 0) {
355 // Pointing at itself is wrong for a cross-link, but perfectly correct for demo_url —
356 // a template's live demo IS its own page.
357 if ($is_cross && $embedded_id === $template_id) {
358 return [
359 'success' => false,
360 'message' => sprintf(
361 'That URL points at template %d — the same template being updated — so it would link to itself. A cross-link should point at the OTHER builder\'s version.',
362 $template_id
363 ),
364 ];
365 }
366
367 $embedded = wdesignkit_mcp_fetch_template_row($token, $embedded_id);
368 if (null !== $embedded) {
369 $actual = (string) ($embedded['post_builder'] ?? '');
370 $names = ['1001' => 'Elementor', '1002' => 'Gutenberg'];
371
372 // Builder only matters for cross-links; demo_url/figma_url have no such expectation.
373 if ($is_cross) {
374 $expected = ['elementor_url' => '1001', 'gutenberg_url' => '1002'][$link_field];
375
376 if ($actual !== $expected) {
377 return [
378 'success' => false,
379 'message' => sprintf(
380 'That URL points at template %d ("%s"), which is a %s template — but %s must point at a %s template. Check the URL: a cross-link goes to the OTHER builder\'s version.',
381 $embedded_id,
382 (string) ($embedded['title'] ?? ''),
383 $names[$actual] ?? ('builder ' . ($actual === '' ? 'unset' : $actual)),
384 $link_field,
385 $names[$expected] ?? $expected
386 ),
387 ];
388 }
389 }
390
391 $linked_to = [
392 'id' => $embedded_id,
393 'title' => (string) ($embedded['title'] ?? ''),
394 'builder' => $actual,
395 ];
396
397 // These fields are rendered to VISITORS (the "Other Available Versions" icons on
398 // the public template page), so an admin/packs/view link would send them to a
399 // login screen. When a WDesignKit admin URL is supplied, rewrite it to the public
400 // template URL — pasting straight from the address bar is the obvious thing to do,
401 // and silently storing a dead-for-visitors link is the wrong outcome.
402 if ('admin' !== $url_format && str_contains($url_literal, '/admin/packs/view/')) {
403 $embedded_type = (string) ($embedded['type'] ?? '');
404 $type_path_map = ['pagetemplate' => 'page', 'section' => 'section', 'websitekit' => 'kit'];
405
406 $new_url = sprintf(
407 'https://wdesignkit.com/templates/%s/%s/%d',
408 $type_path_map[$embedded_type] ?? $embedded_type,
409 wdesignkit_mcp_link_slug((string) ($embedded['title'] ?? '')),
410 $embedded_id
411 );
412 $normalised_from_admin = true;
413 }
414 }
415 }
416 }
417
418 if ($url_literal === '') {
419 $target = wdesignkit_mcp_fetch_template_row($token, $target_id);
420 if (null === $target) {
421 return [
422 'success' => false,
423 'message' => 'Could not read target template ' . $target_id . '. It may not exist, or may belong to a different account.',
424 ];
425 }
426
427 // Catch the cross-link pointed at the wrong builder — the mistake that produces a
428 // "Gutenberg Version URL" leading to another Elementor template.
429 $expected_builder = ['elementor_url' => '1001', 'gutenberg_url' => '1002'][$link_field] ?? '';
430 $target_builder = (string) ($target['post_builder'] ?? '');
431 if ($expected_builder !== '' && $target_builder !== $expected_builder) {
432 $names = ['1001' => 'Elementor', '1002' => 'Gutenberg'];
433
434 return [
435 'success' => false,
436 'message' => sprintf(
437 'link_field "%s" expects the target to be a %s template, but template %d is %s. Check the ids — a cross-link should point at the OTHER builder\'s version.',
438 $link_field,
439 $names[$expected_builder] ?? $expected_builder,
440 $target_id,
441 $names[$target_builder] ?? ('builder ' . ($target_builder === '' ? 'unset' : $target_builder))
442 ),
443 ];
444 }
445
446 $target_type = (string) ($target['type'] ?? '');
447 $type_path_map = ['pagetemplate' => 'page', 'section' => 'section', 'websitekit' => 'kit'];
448
449 $new_url = 'public' === $url_format
450 ? sprintf(
451 'https://wdesignkit.com/templates/%s/%s/%d',
452 $type_path_map[$target_type] ?? $target_type,
453 wdesignkit_mcp_link_slug((string) ($target['title'] ?? '')),
454 $target_id
455 )
456 : sprintf('https://wdesignkit.com/admin/packs/view/%d', $target_id);
457
458 $linked_to = [
459 'id' => $target_id,
460 'title' => (string) ($target['title'] ?? ''),
461 'builder' => $target_builder,
462 ];
463 }
464
465 $current = (string) ($row[$link_column] ?? '');
466
467 if ($current === $new_url) {
468 return [
469 'success' => true,
470 'message' => sprintf('No change — %s on template %d is already set to %s.', $link_field, $template_id, $new_url),
471 'template_id' => $template_id,
472 'link_field' => $link_field,
473 'from' => $current,
474 'to' => $new_url,
475 'linked_to' => $linked_to,
476 'dry_run' => $dry_run,
477 ];
478 }
479
480 if ($dry_run) {
481 return [
482 'success' => true,
483 'message' => sprintf('DRY RUN — would set %s on template %d to %s (currently %s).', $link_field, $template_id, $new_url, $current === '' ? 'empty' : $current),
484 'template_id' => $template_id,
485 'link_field' => $link_field,
486 'from' => $current,
487 'to' => $new_url,
488 'linked_to' => $linked_to,
489 'dry_run' => true,
490 ];
491 }
492
493 // Re-send the whole row, then overlay just this link.
494 $args = wdesignkit_mcp_template_preserve_args($row, $token);
495 $args['id'] = $template_id;
496 $args[$link_param] = $new_url;
497
498 $response = wp_remote_post(
499 WDKIT_SERVER_API_URL . 'api/templates/update',
500 ['method' => 'POST', 'body' => $args, 'timeout' => 60]
501 );
502
503 if (is_wp_error($response)) {
504 return ['success' => false, 'message' => 'Update request failed: ' . $response->get_error_message()];
505 }
506
507 $code = (int) wp_remote_retrieve_response_code($response);
508 $body = wp_remote_retrieve_body($response);
509 $data = json_decode($body, true);
510
511 if (200 !== $code) {
512 return ['success' => false, 'message' => 'Cloud returned status ' . $code . ' — the link was not saved.'];
513 }
514
515 // Demand an explicit confirmation. Failing only on an explicitly-falsy success key meant an
516 // unparseable 200 — a WAF or proxy HTML page, which the plugin's own comments record as a real
517 // occurrence — fell through to the success return below and reported a link that was never
518 // written (ClickUp 86d41cczy). The guard above only caught a completely empty body.
519 if (!is_array($data)) {
520 return [
521 'success' => false,
522 'message' => 'Cloud returned a 200 that could not be parsed as JSON, so the link is unconfirmed and may not have been saved. Re-read the template to check before retrying.',
523 ];
524 }
525 if (empty($data['success'])) {
526 return ['success' => false, 'message' => $data['message'] ?? $data['massage'] ?? 'Cloud reported failure — the link was not saved.'];
527 }
528
529 return [
530 'success' => true,
531 'message' => sprintf(
532 'Set %s on template %d to %s.%s',
533 $link_field,
534 $template_id,
535 $new_url,
536 ($auto_field ? sprintf(' (link_field was inferred from the template\'s own builder: %s.)', ['1001' => 'Elementor', '1002' => 'Gutenberg'][(string) ($row['post_builder'] ?? '')] ?? 'unknown') : '')
537 . ($normalised_from_admin ? ' The admin URL you supplied was rewritten to the public template URL, because this link is shown to visitors — pass url_format="admin" to keep the admin link instead.' : '')
538 ),
539 'template_id' => $template_id,
540 'link_field' => $link_field,
541 'from' => $current,
542 'to' => $new_url,
543 'linked_to' => $linked_to,
544 'dry_run' => false,
545 ];
546 }
547