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 / widgets / wdesignkit-toggle-widget.php

wdesignkit-toggle-widget.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.0, at includes/abilities/widgets/wdesignkit-toggle-widget.php

379 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abilities: Activate and deactivate WDesignKit widgets.
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/activate-widget', [
13 'label' => __('Activate WDesignKit Widget', 'wdesignkit'),
14 'description' => __(
15 'Activates a deactivated widget so it gets loaded by the page builder. The widget files remain unchanged; this only updates the activation status.',
16 'wdesignkit',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'widget_id' => [
23 'type' => 'string',
24 'description' => 'The widget_id (w_unique) to activate. Get this from list-widgets.',
25 'maxLength' => 128,
26 ],
27 ],
28 'required' => ['widget_id'],
29 'additionalProperties' => false,
30 ],
31 'output_schema' => [
32 'type' => 'object',
33 'properties' => [
34 'success' => ['type' => 'boolean'],
35 'message' => ['type' => 'string'],
36 'status' => ['type' => 'string'],
37 ],
38 ],
39 'execute_callback' => 'wdesignkit_mcp_activate_widget',
40 'permission_callback' => 'wdesignkit_mcp_permission_callback',
41 'meta' => [
42 'public' => true,
43 'show_in_rest' => true,
44 'mcp' => ['public' => true],
45 'annotations' => [
46 'instructions' => implode("\n", [
47 'Activates a deactivated WDesignKit widget.',
48 'Use wdesignkit/list-widgets to find deactivated widgets and their widget_id.',
49 'Returns success:false if the widget_id does not exist on the filesystem.',
50 'Returns status "already_active" if the widget exists but was not deactivated.',
51 'Returns status "activated" if the widget was successfully removed from the deactivated list.',
52 ]),
53 'readonly' => false,
54 'destructive' => false,
55 'idempotent' => true,
56 ],
57 ],
58 ]);
59
60 wp_register_ability('wdesignkit/deactivate-widget', [
61 'label' => __('Deactivate WDesignKit Widget', 'wdesignkit'),
62 'description' => __(
63 'Deactivates a widget so it stops loading on the frontend. The widget files are preserved; this only updates the activation status. Useful for temporarily disabling a widget without deleting it.',
64 'wdesignkit',
65 ),
66 'category' => 'wdesignkit',
67 'input_schema' => [
68 'type' => 'object',
69 'properties' => [
70 'widget_id' => [
71 'type' => 'string',
72 'description' => 'The widget_id (w_unique) to deactivate. Get this from list-widgets.',
73 'maxLength' => 128,
74 ],
75 'builder' => [
76 'type' => 'string',
77 'description' => 'Builder type (for internal tracking).',
78 'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'],
79 ],
80 'title' => [
81 'type' => 'string',
82 'description' => 'Widget title (for internal tracking).',
83 ],
84 ],
85 'required' => ['widget_id'],
86 'additionalProperties' => false,
87 ],
88 'output_schema' => [
89 'type' => 'object',
90 'properties' => [
91 'success' => ['type' => 'boolean'],
92 'message' => ['type' => 'string'],
93 'status' => ['type' => 'string'],
94 ],
95 ],
96 'execute_callback' => 'wdesignkit_mcp_deactivate_widget',
97 'permission_callback' => 'wdesignkit_mcp_permission_callback',
98 'meta' => [
99 'public' => true,
100 'show_in_rest' => true,
101 'mcp' => ['public' => true],
102 'annotations' => [
103 'instructions' => implode("\n", [
104 'Deactivates a WDesignKit widget without deleting its files.',
105 'The widget will stop loading on the frontend but can be reactivated later.',
106 'Returns success:false if the widget_id does not exist on the filesystem.',
107 'Returns status "already_deactivated" if the widget was already deactivated.',
108 'Returns status "builder_mismatch" if the provided builder does not match the widget\'s actual builder — omit the builder parameter to skip this check.',
109 'The builder parameter is optional; when omitted the correct builder is auto-detected from the filesystem.',
110 'Use wdesignkit/activate-widget to re-enable it.',
111 ]),
112 'readonly' => false,
113 'destructive' => false,
114 'idempotent' => true,
115 ],
116 ],
117 ]);
118
119 /**
120 * Validate widget_id format: only alphanumeric, hyphens, underscores, dots.
121 */
122 function wdesignkit_mcp_validate_widget_id(string $widget_id): bool {
123 return (bool) preg_match('/^[a-zA-Z0-9_\-\.]+$/', $widget_id);
124 }
125
126 /**
127 * Check whether a widget with the given widget_id exists in any builder's filesystem.
128 * Returns an array with 'builder' and 'folder' keys if found, or null if not found.
129 *
130 * Fast path: widget folders follow the {slug}_{widget_id} naming convention, so the
131 * hash can be extracted from the folder name with strrpos — no file reads needed.
132 * Slow path: JSON scan fallback for any folder that doesn't match the convention.
133 * Results are cached in a static map for the duration of the PHP request.
134 */
135 function wdesignkit_mcp_find_widget_by_id(string $widget_id): ?array {
136 static $cache = [];
137
138 if (array_key_exists($widget_id, $cache)) {
139 return $cache[$widget_id];
140 }
141
142 if (!defined('WDKIT_BUILDER_PATH')) {
143 return $cache[$widget_id] = null;
144 }
145
146 $builders = ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'];
147
148 // Fast path: compare hash suffix of folder name — O(n folders), zero file reads.
149 foreach ($builders as $builder) {
150 $builder_dir = WDKIT_BUILDER_PATH . '/' . $builder;
151 if (!is_dir($builder_dir)) {
152 continue;
153 }
154 $folders = @scandir($builder_dir);
155 if (!is_array($folders)) {
156 continue;
157 }
158 foreach ($folders as $folder) {
159 if ($folder === '.' || $folder === '..') {
160 continue;
161 }
162 if (!is_dir($builder_dir . '/' . $folder)) {
163 continue;
164 }
165 $sep = strrpos($folder, '_');
166 if ($sep !== false && substr($folder, $sep + 1) === $widget_id) {
167 return $cache[$widget_id] = ['builder' => $builder, 'folder' => $folder];
168 }
169 }
170 }
171
172 // Slow path: read JSON configs for folders that don't follow the naming convention.
173 foreach ($builders as $builder) {
174 $builder_dir = WDKIT_BUILDER_PATH . '/' . $builder;
175 if (!is_dir($builder_dir)) {
176 continue;
177 }
178 $folders = @scandir($builder_dir);
179 if (!is_array($folders)) {
180 continue;
181 }
182 foreach ($folders as $folder) {
183 if ($folder === '.' || $folder === '..') {
184 continue;
185 }
186 $folder_path = $builder_dir . '/' . $folder;
187 if (!is_dir($folder_path)) {
188 continue;
189 }
190 // Skip folders already confirmed not to match in the fast pass above.
191 $sep = strrpos($folder, '_');
192 if ($sep !== false && substr($folder, $sep + 1) !== $widget_id) {
193 continue;
194 }
195 $sub_files = @scandir($folder_path);
196 if (!is_array($sub_files)) {
197 continue;
198 }
199 foreach ($sub_files as $sf) {
200 if (pathinfo($sf, PATHINFO_EXTENSION) !== 'json') {
201 continue;
202 }
203 $raw = @file_get_contents($folder_path . '/' . $sf);
204 if ($raw === false) {
205 continue;
206 }
207 $json = json_decode($raw, true);
208 if (json_last_error() !== JSON_ERROR_NONE) {
209 continue;
210 }
211 if (($json['widget_data']['widgetdata']['widget_id'] ?? '') === $widget_id) {
212 return $cache[$widget_id] = ['builder' => $builder, 'folder' => $folder];
213 }
214 break;
215 }
216 }
217 }
218
219 return $cache[$widget_id] = null;
220 }
221
222 function wdesignkit_mcp_activate_widget(array $input): array {
223 set_time_limit(30);
224
225 $widget_id = sanitize_text_field($input['widget_id'] ?? '');
226
227 if (empty($widget_id)) {
228 return ['success' => false, 'message' => 'widget_id is required.', 'status' => 'error'];
229 }
230
231 // Validate widget_id format to prevent injection
232 if (!wdesignkit_mcp_validate_widget_id($widget_id)) {
233 return [
234 'success' => false,
235 'message' => 'Invalid widget_id format. Only letters, numbers, hyphens, underscores, and dots are allowed.',
236 'status' => 'error',
237 ];
238 }
239
240 if (strlen($widget_id) > 128) {
241 return ['success' => false, 'message' => 'widget_id is too long.', 'status' => 'error'];
242 }
243
244 // Verify the widget actually exists on the filesystem before proceeding
245 $widget_location = wdesignkit_mcp_find_widget_by_id($widget_id);
246 if ($widget_location === null) {
247 return [
248 'success' => false,
249 'message' => "Widget '{$widget_id}' does not exist. Use list-widgets to get valid widget IDs.",
250 'status' => 'not_found',
251 ];
252 }
253
254 $deactivated = get_option('wkit_deactivate_widgets', []);
255
256 if (!is_array($deactivated)) {
257 $deactivated = [];
258 }
259
260 $found = false;
261 foreach ($deactivated as $idx => $dw) {
262 if (($dw['w_unique'] ?? '') === $widget_id) {
263 unset($deactivated[$idx]);
264 $found = true;
265 break;
266 }
267 }
268
269 if (!$found) {
270 return [
271 'success' => true,
272 'message' => "Widget '{$widget_id}' ({$widget_location['builder']}/{$widget_location['folder']}) is already active.",
273 'status' => 'already_active',
274 ];
275 }
276
277 update_option('wkit_deactivate_widgets', array_values($deactivated), false);
278
279 // Activating removes the widget from wkit_deactivate_widgets, which the cached
280 // registry excludes by widget_id — this changes registration eligibility without
281 // touching any widget file, so the cache must be invalidated even though nothing
282 // was written to disk.
283 if (function_exists('wdesignkit_invalidate_widget_registry')) {
284 wdesignkit_invalidate_widget_registry($widget_location['builder']);
285 }
286
287 return [
288 'success' => true,
289 'message' => "Widget '{$widget_id}' ({$widget_location['builder']}/{$widget_location['folder']}) has been activated.",
290 'status' => 'activated',
291 ];
292 }
293
294 function wdesignkit_mcp_deactivate_widget(array $input): array {
295 set_time_limit(30);
296
297 $widget_id = sanitize_text_field($input['widget_id'] ?? '');
298 $builder = sanitize_text_field($input['builder'] ?? '');
299 $title = sanitize_text_field($input['title'] ?? $widget_id);
300
301 if (empty($widget_id)) {
302 return ['success' => false, 'message' => 'widget_id is required.', 'status' => 'error'];
303 }
304
305 // Validate widget_id format
306 if (!wdesignkit_mcp_validate_widget_id($widget_id)) {
307 return [
308 'success' => false,
309 'message' => 'Invalid widget_id format. Only letters, numbers, hyphens, underscores, and dots are allowed.',
310 'status' => 'error',
311 ];
312 }
313
314 if (strlen($widget_id) > 128) {
315 return ['success' => false, 'message' => 'widget_id is too long.', 'status' => 'error'];
316 }
317
318 // Verify the widget actually exists on the filesystem
319 $widget_location = wdesignkit_mcp_find_widget_by_id($widget_id);
320 if ($widget_location === null) {
321 return [
322 'success' => false,
323 'message' => "Widget '{$widget_id}' does not exist. Use list-widgets to get valid widget IDs.",
324 'status' => 'not_found',
325 ];
326 }
327
328 // Validate the provided builder against the filesystem-discovered builder.
329 // If a builder is supplied and it doesn't match, reject the call — deactivating
330 // with the wrong builder would store a corrupt tracking record.
331 // Always use the canonical filesystem value for the stored record regardless.
332 if (!empty($builder) && $builder !== $widget_location['builder']) {
333 return [
334 'success' => false,
335 'message' => "Builder mismatch: widget '{$widget_id}' belongs to '{$widget_location['builder']}', not '{$builder}'. Use the correct builder or omit the parameter.",
336 'status' => 'builder_mismatch',
337 ];
338 }
339 $builder = $widget_location['builder'];
340
341 $deactivated = get_option('wkit_deactivate_widgets', []);
342
343 if (!is_array($deactivated)) {
344 $deactivated = [];
345 }
346
347 // Check if already deactivated
348 foreach ($deactivated as $dw) {
349 if (($dw['w_unique'] ?? '') === $widget_id) {
350 return [
351 'success' => true,
352 'message' => "Widget '{$widget_id}' ({$widget_location['builder']}/{$widget_location['folder']}) is already deactivated.",
353 'status' => 'already_deactivated',
354 ];
355 }
356 }
357
358 $deactivated[] = [
359 'w_unique' => $widget_id,
360 'builder' => $builder,
361 'title' => $title,
362 'is_activated' => 'deactive',
363 ];
364
365 update_option('wkit_deactivate_widgets', $deactivated, false);
366
367 // Same reasoning as activate-widget: deactivation is a wkit_deactivate_widgets change,
368 // not a file write, but the cached registry bakes that list in — must invalidate.
369 if (function_exists('wdesignkit_invalidate_widget_registry')) {
370 wdesignkit_invalidate_widget_registry($builder);
371 }
372
373 return [
374 'success' => true,
375 'message' => "Widget '{$widget_id}' ({$widget_location['builder']}/{$widget_location['folder']}) has been deactivated.",
376 'status' => 'deactivated',
377 ];
378 }
379