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 / code-snippets / wdesignkit-import-snippet.php

wdesignkit-import-snippet.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.1, at includes/abilities/code-snippets/wdesignkit-import-snippet.php

341 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Import a code snippet payload/file into the local WordPress site.
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/import-snippet', [
13 'label' => __('Import WDesignKit Code Snippet', 'wdesignkit'),
14 'description' => __(
15 'Imports a code snippet from a payload or file directly into the local site (supports both Nexter Pro file-based storage and legacy post-based storage). No cloud login required. Duplicate snippet names are declined by default unless overwrite: true is passed. Imported PHP snippets start deactivated for safety.',
16 'wdesignkit',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'name' => [
23 'type' => 'string',
24 'description' => 'Snippet display name (e.g. "Custom Header CSS"). Required if snippet_payload is omitted.',
25 ],
26 'type' => [
27 'type' => 'string',
28 'description' => 'Snippet type: php, css, js, or html.',
29 'enum' => ['php', 'css', 'js', 'html'],
30 ],
31 'code' => [
32 'type' => 'string',
33 'description' => 'Snippet code content. Required if snippet_payload is omitted.',
34 ],
35 'description' => [
36 'type' => 'string',
37 'description' => 'Short description or note about the snippet.',
38 ],
39 'location' => [
40 'type' => 'string',
41 'description' => 'Snippet execution/insertion location (e.g. "wp_head", "wp_footer", "admin_head", "shortcode").',
42 ],
43 'code_execute' => [
44 'type' => 'string',
45 'description' => 'Execution scope or method.',
46 ],
47 'tags' => [
48 'type' => 'array',
49 'description' => 'Tags for organizing snippets.',
50 'items' => ['type' => 'string'],
51 ],
52 'priority' => [
53 'type' => 'integer',
54 'description' => 'Hook execution priority (default 10).',
55 ],
56 'status' => [
57 'type' => 'string',
58 'description' => 'Status: "publish" (active) or "draft" (inactive). PHP snippets default to "draft" for safety.',
59 'enum' => ['publish', 'draft'],
60 ],
61 'snippet_payload' => [
62 'type' => 'object',
63 'description' => 'Unified JSON snippet payload object containing name, type, langCode/code, etc.',
64 ],
65 'overwrite' => [
66 'type' => 'boolean',
67 'description' => 'When true, overwrites an existing local snippet with the same name instead of declining.',
68 ],
69 ],
70 'additionalProperties' => false,
71 ],
72 'output_schema' => [
73 'type' => 'object',
74 'properties' => [
75 'success' => ['type' => 'boolean'],
76 'message' => ['type' => 'string'],
77 'storage' => ['type' => 'string'],
78 'file_id' => ['type' => ['string', 'null']],
79 'post_id' => ['type' => ['integer', 'null']],
80 'name' => ['type' => 'string'],
81 'type' => ['type' => 'string'],
82 'status' => ['type' => 'string'],
83 ],
84 ],
85 'execute_callback' => 'wdesignkit_mcp_import_snippet',
86 'permission_callback' => 'wdesignkit_mcp_permission_callback',
87 'meta' => [
88 'public' => true,
89 'show_in_rest' => true,
90 'mcp' => ['public' => true],
91 'annotations' => [
92 'instructions' => implode("\n", [
93 'Imports a code snippet payload/file directly into the local site.',
94 'No cloud login required.',
95 'Valid types: php, css, js, html.',
96 'Imported PHP snippets default to deactivated (status: draft) for site safety.',
97 'Declines duplicate names unless overwrite: true is passed.',
98 ]),
99 'readonly' => false,
100 'destructive' => false,
101 'idempotent' => false,
102 ],
103 ],
104 ]);
105
106 function wdesignkit_mcp_import_snippet(array $input): array {
107 set_time_limit(60);
108
109 $payload = is_array($input['snippet_payload'] ?? null) ? $input['snippet_payload'] : [];
110
111 $name = sanitize_text_field((string) ($input['name'] ?? $payload['name'] ?? $payload['title'] ?? ''));
112 $type = strtolower(sanitize_text_field((string) ($input['type'] ?? $payload['type'] ?? $payload['post_type'] ?? '')));
113
114 if ($type === 'nxt-code-snippet') {
115 $type = strtolower(sanitize_text_field((string) ($payload['nxt-code-type'] ?? 'php')));
116 }
117
118 $code = (string) ($input['code']
119 ?? $payload['langCode']
120 ?? $payload['code']
121 ?? $payload['nxt-php-code']
122 ?? $payload['nxt-css-code']
123 ?? $payload['nxt-js-code']
124 ?? $payload['nxt-html-code']
125 ?? '');
126
127 $description = sanitize_text_field((string) ($input['description'] ?? $payload['description'] ?? $payload['note'] ?? ''));
128 $location = sanitize_text_field((string) ($input['location'] ?? $payload['location'] ?? ''));
129 $code_execute = sanitize_text_field((string) ($input['code_execute'] ?? $payload['codeExecute'] ?? $payload['code-execute'] ?? ''));
130 $priority = (int) ($input['priority'] ?? $payload['priority'] ?? $payload['hooksPriority'] ?? 10);
131 $overwrite = !empty($input['overwrite']);
132
133 $raw_tags = $input['tags'] ?? $payload['tags'] ?? [];
134 $tags = is_array($raw_tags)
135 ? array_map('sanitize_text_field', $raw_tags)
136 : array_map('sanitize_text_field', explode(',', (string) $raw_tags));
137
138 // Safety rule: PHP snippets start deactivated (status 0 / draft) unless explicitly allowed
139 $status_in = sanitize_text_field((string) ($input['status'] ?? $payload['status'] ?? ''));
140 if ($type === 'php') {
141 $status_int = 0; // Draft for PHP safety
142 } else {
143 $status_int = ($status_in === 'publish' || $status_in === '1' || $status_in === 1) ? 1 : 0;
144 }
145 $status_str = ($status_int === 1) ? 'publish' : 'draft';
146
147 $allowed_types = ['php', 'css', 'js', 'html'];
148 if ($name === '' || $type === '' || !in_array($type, $allowed_types, true) || $code === '') {
149 return [
150 'success' => false,
151 'message' => 'Snippet name, valid type (php, css, js, html), and non-empty code content are required.',
152 'storage' => '',
153 'file_id' => null,
154 'post_id' => null,
155 'name' => $name,
156 'type' => $type,
157 'status' => $status_str,
158 ];
159 }
160
161 // ── Duplicate check ───────────────────────────────────────────────────────
162 $existing_file_id = null;
163 $existing_post_id = null;
164
165 if (class_exists('Nexter_Code_Snippets_File_Based')) {
166 $file_based = new \Nexter_Code_Snippets_File_Based();
167
168 // getListCode() only surfaces the 'publish' bucket — draft snippets (where every
169 // imported PHP snippet starts, for safety) would silently bypass this duplicate
170 // check. Scan the full index (both buckets) directly when available.
171 if (method_exists($file_based, 'getIndexedConfig')) {
172 $config = $file_based->getIndexedConfig();
173 foreach (['publish', 'draft'] as $bucket) {
174 if ($existing_file_id !== null) {
175 break;
176 }
177 foreach ((is_array($config[$bucket] ?? null) ? $config[$bucket] : []) as $file_key => $item) {
178 $item_name = (string) ($item['name'] ?? '');
179 if (is_array($item) && strtolower($item_name) === strtolower($name)) {
180 $existing_file_id = preg_replace('/\.php$/', '', sanitize_file_name((string) $file_key));
181 break;
182 }
183 }
184 }
185 } else {
186 $raw_list = $file_based->getListCode();
187 foreach ((is_array($raw_list) ? $raw_list : []) as $item) {
188 $item_name = (string) ($item['name'] ?? '');
189 if (strtolower($item_name) === strtolower($name)) {
190 $existing_file_id = (string) ($item['id'] ?? '');
191 break;
192 }
193 }
194 }
195 } else {
196 $found_posts = get_posts([
197 'post_type' => 'nxt-code-snippet',
198 'title' => $name,
199 'posts_per_page' => 1,
200 'post_status' => 'any',
201 'fields' => 'ids',
202 ]);
203 if (!empty($found_posts)) {
204 $existing_post_id = (int) $found_posts[0];
205 }
206 }
207
208 if (($existing_file_id !== null || $existing_post_id !== null) && !$overwrite) {
209 $id_label = ($existing_file_id !== null) ? "file_id: {$existing_file_id}" : "post_id: {$existing_post_id}";
210 return [
211 'success' => false,
212 'message' => "A snippet named '{$name}' already exists ({$id_label}). Pass overwrite: true to update it.",
213 'storage' => ($existing_file_id !== null) ? 'file' : 'post',
214 'file_id' => $existing_file_id,
215 'post_id' => $existing_post_id,
216 'name' => $name,
217 'type' => $type,
218 'status' => $status_str,
219 ];
220 }
221
222 // ── Build snippet payload structure ───────────────────────────────────────
223 $snippet_array = [
224 'name' => $name,
225 'post_type' => 'nxt-code-snippet',
226 'type' => $type,
227 'description' => $description,
228 'langCode' => $code,
229 'codeExecute' => $code_execute,
230 'status' => $status_int,
231 'tags' => $tags,
232 'location' => $location,
233 'hooksPriority' => (string) $priority,
234 ];
235
236 // ── File-based path (Nexter Pro) ─────────────────────────────────────────
237 if (class_exists('Nexter_Code_Snippets_File_Based')) {
238 $file_based = new \Nexter_Code_Snippets_File_Based();
239 $import_result = apply_filters('nexter_before_import_snippet_file_based', null, $snippet_array, $file_based);
240
241 if ($import_result === null && class_exists('Nexter_Builder_Code_Snippets_Render')) {
242 $render = \Nexter_Builder_Code_Snippets_Render::get_instance();
243 if (method_exists($render, 'import_single_snippet_file_based')) {
244 $import_result = $render->import_single_snippet_file_based($snippet_array, $file_based);
245 }
246 }
247
248 if (is_wp_error($import_result)) {
249 return [
250 'success' => false,
251 'message' => $import_result->get_error_message(),
252 'storage' => 'file',
253 'file_id' => null,
254 'post_id' => null,
255 'name' => $name,
256 'type' => $type,
257 'status' => $status_str,
258 ];
259 }
260
261 if (isset($import_result['success']) && $import_result['success']) {
262 if (method_exists($file_based, 'snippetIndexData')) {
263 $file_based->snippetIndexData();
264 }
265 // The index is a generated PHP file read back via `include`; drop any stale
266 // OPcache copy so this import is visible to list-local-snippets on the very
267 // next request instead of lagging by one import ("stale-by-one").
268 if (function_exists('wdesignkit_flush_snippet_index_cache')) {
269 wdesignkit_flush_snippet_index_cache();
270 }
271 if (method_exists($file_based, 'getIndexedConfig')) {
272 $file_based->getIndexedConfig(false);
273 }
274 $raw_id = $import_result['id'] ?? null;
275 $file_id = ($raw_id !== null && $raw_id !== '' && $raw_id !== false) ? (string) $raw_id : null;
276
277 return [
278 'success' => true,
279 'message' => "Snippet '{$name}' imported successfully (file-based)." . ($type === 'php' ? ' Deactivated by default for safety.' : ''),
280 'storage' => 'file',
281 'file_id' => $file_id,
282 'post_id' => null,
283 'name' => $name,
284 'type' => $type,
285 'status' => $status_str,
286 ];
287 }
288 }
289
290 // ── Post-based fallback ──────────────────────────────────────────────────
291 if ($existing_post_id && $overwrite) {
292 $post_id = $existing_post_id;
293 wp_update_post([
294 'ID' => $post_id,
295 'post_title' => $name,
296 'post_status'=> $status_str,
297 ]);
298 } else {
299 $post_id = wp_insert_post([
300 'post_title' => $name,
301 'post_type' => 'nxt-code-snippet',
302 'post_status' => $status_str,
303 ]);
304 }
305
306 if (is_wp_error($post_id)) {
307 return [
308 'success' => false,
309 'message' => $post_id->get_error_message(),
310 'storage' => 'post',
311 'file_id' => null,
312 'post_id' => null,
313 'name' => $name,
314 'type' => $type,
315 'status' => $status_str,
316 ];
317 }
318
319 update_post_meta($post_id, 'nxt-code-type', $type);
320 update_post_meta($post_id, 'nxt-code-note', $description);
321 update_post_meta($post_id, 'nxt-code-tags', $tags);
322 update_post_meta($post_id, 'nxt-code-execute', $code_execute);
323 update_post_meta($post_id, 'nxt-code-status', $status_int);
324 update_post_meta($post_id, 'nxt-' . $type . '-code', wp_unslash($code));
325 update_post_meta($post_id, 'nxt-code-hooks-priority', (string) $priority);
326 if ($location !== '') {
327 update_post_meta($post_id, 'nxt-code-location', $location);
328 }
329
330 return [
331 'success' => true,
332 'message' => "Snippet '{$name}' imported successfully (post-based)." . ($type === 'php' ? ' Deactivated by default for safety.' : ''),
333 'storage' => 'post',
334 'file_id' => null,
335 'post_id' => (int) $post_id,
336 'name' => $name,
337 'type' => $type,
338 'status' => $status_str,
339 ];
340 }
341