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
wdesignkit / includes / abilities / templates / wdesignkit-save-template.php

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

242 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Save the contents of a builder page as a new WDesignKit cloud template.
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/save-template', [
13 'label' => __('Save WDesignKit Template', 'wdesignkit'),
14 'description' => __(
15 'Saves a page builder layout to the user\'s WDesignKit cloud library as a new template. Provide the full builder data payload (JSON string for Gutenberg, base64-decoded Elementor export for Elementor) plus the source post_id so any nxt-* custom meta is captured alongside it.',
16 'wdesignkit',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'builder' => [
23 'type' => 'string',
24 'description' => 'Builder the template was authored in.',
25 'enum' => ['elementor', 'gutenberg'],
26 ],
27 'data' => [
28 'type' => 'string',
29 'description' => 'Serialized builder data. For Elementor pass the JSON Elementor exporter output. For Gutenberg pass the serialized block markup or block JSON.',
30 ],
31 'post_id' => [
32 'type' => 'string',
33 'description' => 'Source post/page ID. Used to capture nxt-* custom meta alongside the saved template.',
34 ],
35 'name' => [
36 'type' => 'string',
37 'description' => 'Optional template display name. Defaults to the source post title when omitted.',
38 ],
39 'type' => [
40 'type' => 'string',
41 'description' => 'Template type (e.g. "page", "section", "block").',
42 ],
43 'plugins' => [
44 'type' => 'array',
45 'description' => 'Optional list of cloud plugin IDs (integers) this template requires — populates the template\'s plugins_id so the library shows its plugin dependencies. Get the IDs from wdesignkit/get-template-plugins.',
46 'items' => ['type' => 'integer'],
47 ],
48 ],
49 'required' => ['builder', 'data'],
50 'additionalProperties' => false,
51 ],
52 'output_schema' => [
53 'type' => 'object',
54 'properties' => [
55 'success' => ['type' => 'boolean'],
56 'message' => ['type' => 'string'],
57 'response' => ['type' => ['object', 'array']],
58 ],
59 ],
60 'execute_callback' => 'wdesignkit_mcp_save_template',
61 'permission_callback' => 'wdesignkit_mcp_permission_callback',
62 'meta' => [
63 'public' => true,
64 'show_in_rest' => true,
65 'mcp' => ['public' => true],
66 'annotations' => [
67 'instructions' => implode("\n", [
68 'Saves a new cloud template. Requires WDesignKit login.',
69 'The "data" field MUST already be plain JSON / serialized markup — do NOT base64 encode it.',
70 'If post_id is provided, every post_meta key starting with "nxt-" is merged into the saved payload as custom_meta.',
71 'Use wdesignkit/update-template to modify an existing saved template instead of duplicating it here.',
72 ]),
73 'readonly' => false,
74 'destructive' => false,
75 'idempotent' => false,
76 ],
77 ],
78 ]);
79
80 function wdesignkit_mcp_save_template(array $input): array {
81 set_time_limit(90);
82
83 $auth = wdesignkit_mcp_template_get_auth();
84 if (empty($auth['logged_in'])) {
85 return ['success' => false, 'message' => $auth['message'] ?? 'Not logged in.'];
86 }
87
88 $builder = sanitize_text_field((string) ($input['builder'] ?? ''));
89 $data = (string) ($input['data'] ?? '');
90 $post_id = sanitize_text_field((string) ($input['post_id'] ?? ''));
91 $name = sanitize_text_field((string) ($input['name'] ?? ''));
92 $type = sanitize_text_field((string) ($input['type'] ?? ''));
93
94 // Map the caller's friendly type to the literal value the cloud stores and filters on
95 // ('section' / 'pagetemplate'). "block" has no confirmed cloud equivalent, so it passes
96 // through unmapped rather than being silently rewritten to something wrong.
97 $cloud_type_map = ['page' => 'pagetemplate', 'section' => 'section'];
98 $cloud_type = $cloud_type_map[$type] ?? $type;
99
100 if (!in_array($builder, ['elementor', 'gutenberg'], true)) {
101 return ['success' => false, 'message' => 'builder must be "elementor" or "gutenberg".'];
102 }
103 if ($data === '') {
104 return ['success' => false, 'message' => 'data is required — pass the full builder export payload.'];
105 }
106
107 // Wrap the payload in the envelope the cloud, the importer and the editor JS all expect —
108 // the same one the Save Template UI builds in main_save_template.js:
109 // { file_type, title, page_id, content, el_type, settings } with the layout in `content`.
110 //
111 // Sending the raw layout as `data` instead broke both builders, in different ways:
112 // Elementor — _elementor_data decodes to a LIST, and adding the string key 'custom_meta' to
113 // a list makes wp_json_encode() emit an object, so the indices became "0","1",…
114 // Gutenberg — block markup is a STRING, so json_decode() returned null, the is_array()
115 // branch was skipped, and the markup was stored bare with no envelope at all.
116 // The editor then runs JSON.parse() on it and throws.
117 // Building the envelope first fixes both: custom_meta now goes onto an OBJECT, where an extra
118 // key cannot reshape `content`.
119 //
120 // el_type is left empty — the UI derives it from the Elementor preview DOM, which has no
121 // server-side equivalent.
122 if ('elementor' === $builder) {
123 $content = json_decode($data, true);
124 if (!is_array($content)) {
125 return [
126 'success' => false,
127 'message' => 'data is not valid Elementor JSON — pass the _elementor_data export (a JSON array), not markup.',
128 ];
129 }
130 } else {
131 // Gutenberg content is serialized block markup; keep the string unless block JSON was sent.
132 $decoded = json_decode($data, true);
133 $content = is_array($decoded) ? $decoded : $data;
134
135 }
136
137 $settings = [];
138 if ($post_id !== '') {
139 $page_settings = get_post_meta((int) $post_id, '_elementor_page_settings', true);
140 if (is_array($page_settings) && !empty($page_settings)) {
141 $settings = $page_settings;
142 }
143 }
144
145 $envelope = [
146 'file_type' => 'elementor' === $builder ? 'elementor' : 'wp_block',
147 'title' => $name,
148 'page_id' => $post_id !== '' ? (int) $post_id : 0,
149 'content' => $content,
150 'el_type' => '',
151 'settings' => $settings,
152 ];
153
154 // The Plus Addons resolves its global button styles, radii and shadows from the kit by
155 // `_id`. Without those definitions travelling with the template, an imported section keeps
156 // the reference but loses the styling. Scoped to what this layout actually references, so a
157 // section carries its own globals rather than a copy of the whole kit.
158 $tp_globals = ('elementor' === $builder && is_array($content)) ? wdesignkit_mcp_tp_globals($content) : [];
159 if (!empty($tp_globals)) {
160 $envelope['tp_globals'] = $tp_globals;
161
162 // A Plus global can reference an Elementor global colour or font of its own. Those refs
163 // live in the lists rather than the layout, so they need capturing separately or the
164 // button style arrives pointing at an id the destination kit does not have.
165 $tp_refs = wdesignkit_mcp_tp_global_refs($tp_globals);
166 if (!empty($tp_refs['color']) || !empty($tp_refs['typography'])) {
167 $envelope['tp_global_refs'] = $tp_refs;
168 }
169 }
170
171 if ($post_id !== '') {
172 $custom_fields = [];
173 foreach ((array) get_post_custom($post_id) as $key => $value) {
174 if (is_string($key) && str_contains($key, 'nxt-')) {
175 $custom_fields[$key] = $value;
176 }
177 }
178
179 if (!empty($custom_fields)) {
180 $envelope['custom_meta'] = $custom_fields;
181 }
182 }
183
184 $data = wp_json_encode($envelope);
185
186 $args = [
187 'token' => $auth['token'],
188 'data' => $data,
189 'post_id' => $post_id,
190 'builder' => $builder,
191 // The cloud SetSaveTemplate endpoint reads the display name from 'title'
192 // (request->get('title')) and ignores 'name' — sending only 'name' is why
193 // templates saved via this ability came back with a blank title. Send 'title'
194 // (keep 'name' too, harmlessly, for any other consumer).
195 'title' => $name,
196 'name' => $name,
197 // The cloud stores the template's type from 'template_type', NOT 'type':
198 // $TemplateType = $request->get('template_type');
199 // 'type' => !empty($TemplateType) ? $TemplateType : 'pagetemplate',
200 // (WdkitPluginController::SetSaveTemplate lines 171 / 252). Sending only 'type' meant
201 // $TemplateType was always empty, so EVERY template saved through this ability was
202 // stored as 'pagetemplate' regardless of the caller's input — a section saved here
203 // could never be found by list-templates(type: 'section'). Send 'template_type' too.
204 //
205 // The cloud's own queries match the literal values 'section' and 'pagetemplate'
206 // (see the whereIn at SetSaveTemplate line 574), not 'page'/'block', so map first.
207 'type' => $type,
208 'template_type' => $cloud_type,
209 ];
210
211 // Optional: record which cloud plugin IDs this template requires. The cloud
212 // SetSaveTemplate endpoint reads 'plugins' as a JSON string of integer plugin IDs
213 // and stores them as the template's plugins_id (previously always empty because the
214 // ability had no way to supply them).
215 $plugins_in = $input['plugins'] ?? [];
216 if (is_string($plugins_in)) {
217 $decoded = json_decode($plugins_in, true);
218 $plugins_in = is_array($decoded) ? $decoded : [];
219 }
220 if (is_array($plugins_in)) {
221 $plugin_ids = [];
222 foreach ($plugins_in as $pid) {
223 $pid = (int) $pid;
224 if ($pid > 0) {
225 $plugin_ids[] = $pid;
226 }
227 }
228 if (!empty($plugin_ids)) {
229 // Sent as a JSON string: the cloud json_decodes 'plugins' server-side.
230 $args['plugins'] = wp_json_encode(array_values(array_unique($plugin_ids)));
231 }
232 }
233
234 $response = wdesignkit_mcp_template_cloud_call('save_template', $args, 'json');
235
236 return [
237 'success' => (bool) ($response['success'] ?? false),
238 'message' => $response['message'] ?? '',
239 'response' => $response,
240 ];
241 }
242