PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
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 1.0.16 All 129 releases
wdesignkit / includes / abilities / templates / wdesignkit-replace-template.php

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

198 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Replace one saved WDesignKit cloud template's content with another payload (guarded swap).
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/replace-template', [
13 'label' => __('Replace WDesignKit Template', 'wdesignkit'),
14 'description' => __(
15 'Replaces the contents of an existing user-saved cloud template with a new payload. Requires confirm: true. Behind the scenes this is an in-place update, but the ability is split out so it can carry the "you are about to overwrite saved work" guardrail. Use dry_run: true to preview the swap.',
16 'wdesignkit',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'id' => [
23 'type' => 'string',
24 'description' => 'Existing template ID whose body will be replaced.',
25 ],
26 'data' => [
27 'type' => 'string',
28 'description' => 'New serialized builder data that replaces the current template body.',
29 ],
30 'post_id' => [
31 'type' => 'string',
32 'description' => 'Optional source post ID — used to refresh nxt-* custom meta on the saved template.',
33 ],
34 'type' => [
35 'type' => 'string',
36 'description' => 'Template type (e.g. "page", "section", "block").',
37 ],
38 'confirm' => [
39 'type' => 'boolean',
40 'description' => 'Must be true to execute the replacement. Omitting or passing false returns an error requiring explicit confirmation.',
41 ],
42 'dry_run' => [
43 'type' => 'boolean',
44 'description' => 'When true, returns what would be sent without making the cloud call. Overrides confirm.',
45 ],
46 ],
47 'required' => ['id', 'data', 'type'],
48 'additionalProperties' => false,
49 ],
50 'output_schema' => [
51 'type' => 'object',
52 'properties' => [
53 'success' => ['type' => 'boolean'],
54 'dry_run' => ['type' => 'boolean'],
55 'message' => ['type' => 'string'],
56 'response' => ['type' => ['object', 'array']],
57 ],
58 ],
59 'execute_callback' => 'wdesignkit_mcp_replace_template',
60 'permission_callback' => 'wdesignkit_mcp_permission_callback',
61 'meta' => [
62 'public' => true,
63 'show_in_rest' => true,
64 'mcp' => ['public' => true],
65 'annotations' => [
66 'instructions' => implode("\n", [
67 'Replaces the body of an existing saved template — destructive: the previous body cannot be recovered.',
68 'REQUIRED: confirm: true. Dry-run first with dry_run: true and show the user what will change.',
69 'For non-destructive metadata-only edits use wdesignkit/update-template.',
70 ]),
71 'readonly' => false,
72 'destructive' => true,
73 'idempotent' => false,
74 ],
75 ],
76 ]);
77
78 function wdesignkit_mcp_replace_template(array $input): array {
79 set_time_limit(90);
80
81 $auth = wdesignkit_mcp_template_get_auth();
82 if (empty($auth['logged_in'])) {
83 return ['success' => false, 'message' => $auth['message'] ?? 'Not logged in.'];
84 }
85
86 $template_id = sanitize_text_field((string) ($input['id'] ?? ''));
87 $data = (string) ($input['data'] ?? '');
88 $post_id = sanitize_text_field((string) ($input['post_id'] ?? ''));
89 $type = sanitize_text_field((string) ($input['type'] ?? ''));
90 $dry_run = !empty($input['dry_run']);
91 $confirm = !empty($input['confirm']);
92
93 if ($template_id === '' || $data === '') {
94 return ['success' => false, 'message' => 'id and data are both required.'];
95 }
96
97 if ($type === '') {
98 return ['success' => false, 'message' => 'type is required. Pass one of: page, section, block.'];
99 }
100
101 if (!$dry_run && !$confirm) {
102 return [
103 'success' => false,
104 'message' => 'Replacement requires explicit confirmation. Re-send with confirm: true after previewing with dry_run: true.',
105 ];
106 }
107
108 // Wrap the replacement body in the envelope the cloud, the importer and the editor JS expect —
109 // { file_type, title, page_id, content, el_type, settings } with the layout in `content`, the
110 // same shape the Save Template UI builds.
111 //
112 // Writing the raw layout instead broke both builders:
113 // Elementor — the layout decodes to a LIST, and adding the string key 'custom_meta' to a
114 // list makes wp_json_encode() emit an object, turning the indices into "0","1",…
115 // Gutenberg — block markup is a STRING, so json_decode() returned null, the is_array()
116 // branch never ran, and the markup was stored bare with no envelope. The editor
117 // then runs JSON.parse() on it and throws.
118 //
119 // This ability has no `builder` input, so infer it: Elementor data decodes to an array,
120 // Gutenberg content is serialized block markup.
121 $decoded_content = json_decode($data, true);
122 $is_elementor = is_array($decoded_content);
123
124 $settings = [];
125 if ($post_id !== '') {
126 $page_settings = get_post_meta((int) $post_id, '_elementor_page_settings', true);
127 if (is_array($page_settings) && !empty($page_settings)) {
128 $settings = $page_settings;
129 }
130 }
131
132 $envelope = [
133 'file_type' => $is_elementor ? 'elementor' : 'wp_block',
134 'title' => '',
135 'page_id' => $post_id !== '' ? (int) $post_id : 0,
136 'content' => $is_elementor ? $decoded_content : $data,
137 'el_type' => '',
138 'settings' => $settings,
139 ];
140
141 if ($post_id !== '') {
142 $custom_fields = [];
143 foreach ((array) get_post_custom($post_id) as $key => $value) {
144 if (is_string($key) && str_contains($key, 'nxt-')) {
145 $custom_fields[$key] = $value;
146 }
147 }
148 if (!empty($custom_fields)) {
149 // On the envelope (an object), so it cannot reshape `content`.
150 $envelope['custom_meta'] = $custom_fields;
151 }
152 }
153
154 $data = wp_json_encode($envelope);
155
156 $args = [
157 'data' => $data,
158 'post_id' => $post_id,
159 'token' => $auth['token'],
160 'type' => 'update_template', // server dispatches on this value — template type (page/section) is not used by the update path
161 'id' => $template_id,
162 'remove' => 'yes',
163 ];
164
165 if ($dry_run) {
166 $preview = $args;
167 unset($preview['token']);
168 $preview['data_preview'] = substr($data, 0, 200) . (strlen($data) > 200 ? '…' : '');
169 unset($preview['data']);
170 return [
171 'success' => true,
172 'dry_run' => true,
173 'message' => 'Dry run — no cloud call made. The fields below would be sent to existing_template.',
174 'response' => $preview,
175 ];
176 }
177
178 $response = wdesignkit_mcp_template_cloud_call('existing_template', $args, 'form');
179
180 // The cloud returns HTTP 200 with an empty body when the template ID does not exist
181 // or the account has no permission to update it. An empty body cannot confirm success.
182 if (array_key_exists('raw', $response) && ($response['raw'] === '' || $response['raw'] === null)) {
183 return [
184 'success' => false,
185 'dry_run' => false,
186 'message' => 'Cloud returned no confirmation for replace. The template ID may not exist or the account has no permission to update it. Verify with wdesignkit/find-template.',
187 'response' => $response,
188 ];
189 }
190
191 return [
192 'success' => (bool) ($response['success'] ?? false),
193 'dry_run' => false,
194 'message' => $response['message'] ?? $response['massage'] ?? '',
195 'response' => $response,
196 ];
197 }
198