| 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 |
|