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-delete-widget.php

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

210 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Delete a WDesignKit widget and all its files.
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/delete-widget', [
13 'label' => __('Delete WDesignKit Widget', 'wdesignkit'),
14 'description' => __(
15 'Soft-deletes a local WDesignKit widget by moving all its files to a recoverable trash folder. Requires confirm: true to execute. Use dry_run: true to preview what will be moved without making any changes.',
16 'wdesignkit',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'builder' => [
23 'type' => 'string',
24 'description' => 'Builder type the widget belongs to.',
25 'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'],
26 ],
27 'folder' => [
28 'type' => 'string',
29 'description' => 'Widget folder name to delete (from list-widgets).',
30 ],
31 'confirm' => [
32 'type' => 'boolean',
33 'description' => 'Must be true to execute the deletion. Omitting or passing false returns an error requiring explicit confirmation.',
34 ],
35 'dry_run' => [
36 'type' => 'boolean',
37 'description' => 'When true, returns a preview of what would be moved to trash without making any changes. Overrides confirm.',
38 ],
39 ],
40 'required' => ['builder', 'folder'],
41 'additionalProperties' => false,
42 ],
43 'output_schema' => [
44 'type' => 'object',
45 'properties' => [
46 'success' => ['type' => 'boolean'],
47 'message' => ['type' => 'string'],
48 'dry_run' => ['type' => 'boolean'],
49 'widget_name' => ['type' => 'string'],
50 'trashed_folder' => ['type' => 'string'],
51 'files' => ['type' => 'array'],
52 'recovery_path' => ['type' => 'string'],
53 ],
54 ],
55 'execute_callback' => 'wdesignkit_mcp_delete_widget',
56 'permission_callback' => 'wdesignkit_mcp_permission_callback',
57 'meta' => [
58 'public' => true,
59 'show_in_rest' => true,
60 'mcp' => ['public' => true],
61 'annotations' => [
62 'instructions' => implode("\n", [
63 'Moves a WDesignKit widget to a trash folder (soft-delete). Files are recoverable.',
64 'REQUIRED: Pass confirm: true to execute. Without it the call is rejected.',
65 'SAFE PREVIEW: Pass dry_run: true to see what would be moved without changing anything.',
66 'Trash location: wp-content/uploads/wdesignkit/.trash/{timestamp}_{folder}/',
67 'Use wdesignkit/list-widgets to get the correct folder and builder values.',
68 'Always show the user the dry_run output and ask for confirmation before passing confirm: true.',
69 'This only moves the local copy. If the widget was pushed to cloud, the cloud copy remains.',
70 ]),
71 'readonly' => false,
72 'destructive' => true,
73 'idempotent' => false,
74 ],
75 ],
76 ]);
77
78 function wdesignkit_mcp_delete_widget(array $input): array {
79 if (!defined('WDKIT_BUILDER_PATH')) {
80 return ['success' => false, 'message' => 'WDesignKit plugin is not active.'];
81 }
82
83 $builder = sanitize_text_field($input['builder'] ?? '');
84 $folder = sanitize_file_name($input['folder'] ?? '');
85 $dry_run = !empty($input['dry_run']);
86 $confirm = !empty($input['confirm']);
87
88 if (empty($builder) || empty($folder)) {
89 return ['success' => false, 'message' => 'Both builder and folder are required.'];
90 }
91
92 $allowed_builders = ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'];
93 if (!in_array($builder, $allowed_builders, true)) {
94 return ['success' => false, 'message' => 'Invalid builder type.'];
95 }
96
97 // Require explicit confirmation unless this is a dry run.
98 if (!$dry_run && !$confirm) {
99 return [
100 'success' => false,
101 'message' => 'Deletion requires explicit confirmation. Re-send the request with confirm: true after reviewing the widget details. Use dry_run: true first to preview what will be moved to trash.',
102 ];
103 }
104
105 $widget_dir = WDKIT_BUILDER_PATH . '/' . $builder . '/' . $folder;
106
107 if (!is_dir($widget_dir)) {
108 return ['success' => false, 'message' => "Widget folder not found: {$builder}/{$folder}"];
109 }
110
111 // Path traversal guard.
112 $real_widget = realpath($widget_dir);
113 $real_base = realpath(WDKIT_BUILDER_PATH);
114 if (!$real_widget || !$real_base || strpos($real_widget, $real_base . DIRECTORY_SEPARATOR) !== 0) {
115 return ['success' => false, 'message' => 'Invalid widget path.'];
116 }
117
118 // Collect file list and read widget metadata from JSON config.
119 $widget_name = $folder;
120 $widget_unique_id = '';
121 $files_in_dir = @scandir($widget_dir) ?: [];
122 $file_list = [];
123
124 foreach (array_diff($files_in_dir, ['.', '..']) as $f) {
125 $file_list[] = $f;
126 if (pathinfo($f, PATHINFO_EXTENSION) === 'json') {
127 $raw = @file_get_contents($widget_dir . '/' . $f);
128 $json_data = ($raw !== false) ? json_decode($raw, true) : null;
129 if ($json_data && json_last_error() === JSON_ERROR_NONE) {
130 $widget_name = $json_data['widget_data']['widgetdata']['name'] ?? $folder;
131 $widget_unique_id = $json_data['widget_data']['widgetdata']['widget_id'] ?? '';
132 }
133 }
134 }
135
136 // Trash destination: uploads/wdesignkit/.trash/{timestamp}_{folder}/
137 $upload_dir = wp_upload_dir();
138 $trash_base = $upload_dir['basedir'] . '/wdesignkit/.trash';
139 $trash_stamp = wp_date('Ymd-His') . '_' . $folder;
140 $trash_dest = $trash_base . '/' . $trash_stamp;
141
142 if ($dry_run) {
143 return [
144 'success' => true,
145 'dry_run' => true,
146 'message' => "Dry run — no changes made. Widget '{$widget_name}' would be moved to trash.",
147 'widget_name' => $widget_name,
148 'files' => $file_list,
149 'trashed_folder' => $builder . '/' . $folder,
150 'recovery_path' => str_replace($upload_dir['basedir'], $upload_dir['baseurl'], $trash_dest),
151 ];
152 }
153
154 // Create trash directory if needed.
155 if (!wp_mkdir_p($trash_dest)) {
156 return ['success' => false, 'message' => 'Could not create trash directory. Check filesystem permissions.'];
157 }
158
159 // Move files individually using plain PHP — avoids WP_Filesystem FTP credential prompts.
160 $moved = [];
161 $failed = [];
162 foreach ($file_list as $filename) {
163 $src = $widget_dir . '/' . $filename;
164 $dst = $trash_dest . '/' . $filename;
165 if (@rename($src, $dst)) {
166 $moved[] = $filename;
167 } else {
168 $failed[] = $filename;
169 }
170 }
171
172 if (!empty($failed)) {
173 return [
174 'success' => false,
175 'message' => 'Some files could not be moved to trash: ' . implode(', ', $failed),
176 'files' => $file_list,
177 ];
178 }
179
180 // Remove now-empty widget folder.
181 @rmdir($widget_dir);
182
183 if (function_exists('wdesignkit_invalidate_widget_registry')) {
184 wdesignkit_invalidate_widget_registry($builder);
185 }
186
187 // Remove from deactivated list — match on widget_id only.
188 if (!empty($widget_unique_id)) {
189 $deactivated = get_option('wkit_deactivate_widgets', []);
190 if (is_array($deactivated)) {
191 $filtered = array_values(array_filter($deactivated, static function ($dw) use ($widget_unique_id) {
192 return ($dw['w_unique'] ?? '') !== $widget_unique_id;
193 }));
194 if (count($filtered) !== count($deactivated)) {
195 update_option('wkit_deactivate_widgets', $filtered);
196 }
197 }
198 }
199
200 return [
201 'success' => true,
202 'dry_run' => false,
203 'message' => "Widget '{$widget_name}' moved to trash (" . count($moved) . " files). Recover from trash folder if needed.",
204 'widget_name' => $widget_name,
205 'files' => $moved,
206 'trashed_folder' => $builder . '/' . $folder,
207 'recovery_path' => str_replace($upload_dir['basedir'], $upload_dir['baseurl'], $trash_dest),
208 ];
209 }
210