← All changes
|
includes/abilities/widgets/wdesignkit-duplicate-widget.php
+116
-23
2.5.1
→
2.7.0
View file →
| @@ -9,12 +9,12 @@ | ||
| 9 | 9 | exit(); |
| 10 | 10 | } |
| 11 | 11 | |
| 12 | 12 | wp_register_ability('wdesignkit/duplicate-widget', [ |
| 13 | - 'label' => __('Duplicate WDesignKit Widget', 'sprout-mcp'), | |
| 13 | + 'label' => __('Duplicate WDesignKit Widget', 'wdesignkit'), | |
| 14 | 14 | 'description' => __( |
| 15 | 15 | 'Creates a copy of a local widget under a new name. All files (JSON, PHP, CSS, JS, image) are duplicated into a new folder. The JSON config is updated with a freshly generated widget_id and the new name. The duplicate is immediately visible in wdesignkit/list-widgets. Cloud records are NOT duplicated — the copy starts as a local-only widget.', |
| 16 | - 'sprout-mcp', | |
| 16 | + 'wdesignkit', | |
| 17 | 17 | ), |
| 18 | 18 | 'category' => 'wdesignkit', |
| 19 | 19 | 'input_schema' => [ |
| 20 | 20 | 'type' => 'object', |
| @@ -47,10 +47,11 @@ | ||
| 47 | 47 | 'new_widget_name'=> ['type' => 'string'], |
| 48 | 48 | ], |
| 49 | 49 | ], |
| 50 | 50 | 'execute_callback' => 'wdesignkit_mcp_duplicate_widget', |
| 51 | - 'permission_callback' => 'sprout_mcp_permission_callback', | |
| 51 | + 'permission_callback' => 'wdesignkit_mcp_permission_callback', | |
| 52 | 52 | 'meta' => [ |
| 53 | + 'public' => true, | |
| 53 | 54 | 'show_in_rest' => true, |
| 54 | 55 | 'mcp' => ['public' => true], |
| 55 | 56 | 'annotations' => [ |
| 56 | 57 | 'instructions' => implode("\n", [ |
| @@ -70,8 +71,20 @@ | ||
| 70 | 71 | if (!defined('WDKIT_BUILDER_PATH')) { |
| 71 | 72 | return ['success' => false, 'message' => 'WDesignKit plugin is not active.']; |
| 72 | 73 | } |
| 73 | 74 | |
| 75 | + // Site-level opt-out for generated widget PHP (ClickUp 86d41zavc). This ability introduces no | |
| 76 | + // new code — it clones a widget already on disk and running — but it does write a new | |
| 77 | + // executable .php file, so a site that has turned widget PHP writes off should not get one. | |
| 78 | + // Refusing the whole call rather than copying everything except the .php: a widget folder with | |
| 79 | + // no PHP is not a working widget, so a partial duplicate would be worse than none. | |
| 80 | + if (function_exists('wdesignkit_widget_php_write_allowed') && !wdesignkit_widget_php_write_allowed()) { | |
| 81 | + return [ | |
| 82 | + 'success' => false, | |
| 83 | + 'message' => 'Widget duplication is disabled on this site: generated widget PHP writes have been turned off via the wdesignkit_allow_widget_php_write filter. Duplicating would have to write a new .php file.', | |
| 84 | + ]; | |
| 85 | + } | |
| 86 | + | |
| 74 | 87 | $builder = sanitize_text_field((string) ($input['builder'] ?? '')); |
| 75 | 88 | $folder = sanitize_file_name((string) ($input['folder'] ?? '')); |
| 76 | 89 | |
| 77 | 90 | $allowed_builders = ['elementor', 'gutenberg', 'gutenberg_core', 'bricks']; |
| @@ -96,16 +109,21 @@ | ||
| 96 | 109 | $json_path = null; |
| 97 | 110 | $orig_json = null; |
| 98 | 111 | $orig_name = $folder; |
| 99 | 112 | $orig_widget_id = ''; |
| 113 | + // Real original file base = the actual JSON filename (every file in the folder — | |
| 114 | + // php/js/css/image — shares it). Read it from disk rather than rebuilding it from the | |
| 115 | + // name, so the identifier rewrites match exactly what is inside the copied files. | |
| 116 | + $orig_file_base = ''; | |
| 100 | 117 | |
| 101 | 118 | foreach ($src_files as $f) { |
| 102 | 119 | if (pathinfo($f, PATHINFO_EXTENSION) !== 'json') { |
| 103 | 120 | continue; |
| 104 | 121 | } |
| 105 | - $json_path = $src_dir . '/' . $f; | |
| 106 | - $raw = @file_get_contents($json_path); | |
| 107 | - $orig_json = ($raw !== false) ? json_decode($raw, true) : null; | |
| 122 | + $json_path = $src_dir . '/' . $f; | |
| 123 | + $orig_file_base = pathinfo($f, PATHINFO_FILENAME); | |
| 124 | + $raw = @file_get_contents($json_path); | |
| 125 | + $orig_json = ($raw !== false) ? json_decode($raw, true) : null; | |
| 108 | 126 | if (is_array($orig_json)) { |
| 109 | 127 | $orig_name = $orig_json['widget_data']['widgetdata']['name'] ?? $folder; |
| 110 | 128 | $orig_widget_id = $orig_json['widget_data']['widgetdata']['widget_id'] ?? ''; |
| 111 | 129 | } |
| @@ -122,13 +140,21 @@ | ||
| 122 | 140 | } |
| 123 | 141 | |
| 124 | 142 | // Generate a 6-char hex widget_id — consistent with the standard WDesignKit format (e.g. "a1b2c3"). |
| 125 | 143 | // wp_generate_uuid4() produces a 36-char string that makes folder names excessively long. |
| 126 | - $new_widget_id = substr(bin2hex(random_bytes(3)), 0, 6); | |
| 127 | - $new_folder = str_replace(' ', '-', $new_name) . '_' . $new_widget_id; | |
| 128 | - $new_file_base = str_replace(' ', '_', $new_name) . '_' . $new_widget_id; | |
| 129 | - $dst_dir = WDKIT_BUILDER_PATH . '/' . $builder . '/' . $new_folder; | |
| 144 | + $new_widget_id = substr(bin2hex(random_bytes(3)), 0, 6); | |
| 130 | 145 | |
| 146 | + // Derive the new folder / file base with the SAME helpers every other writer uses, so a | |
| 147 | + // duplicate never lands in a differently-cased twin of an existing folder (ClickUp | |
| 148 | + // 86d3yk4yx). The file base still uses underscores, which keeps the Elementor loader — | |
| 149 | + // it instantiates 'Wdkit_' . str_replace('-','_', filename) — resolving the class. | |
| 150 | + if (sanitize_file_name($new_name) === '') { | |
| 151 | + $new_name = 'Widget'; | |
| 152 | + } | |
| 153 | + $new_folder = wdesignkit_widget_folder_name($new_name, $new_widget_id); | |
| 154 | + $new_file_base = wdesignkit_widget_file_name($new_name, $new_widget_id); | |
| 155 | + $dst_dir = WDKIT_BUILDER_PATH . '/' . $builder . '/' . $new_folder; | |
| 156 | + | |
| 131 | 157 | if (is_dir($dst_dir)) { |
| 132 | 158 | return ['success' => false, 'message' => "Target folder already exists: {$builder}/{$new_folder}"]; |
| 133 | 159 | } |
| 134 | 160 | |
| @@ -135,35 +161,102 @@ | ||
| 135 | 161 | if (!wp_mkdir_p($dst_dir)) { |
| 136 | 162 | return ['success' => false, 'message' => 'Could not create destination folder.']; |
| 137 | 163 | } |
| 138 | 164 | |
| 139 | - // Copy each file, renaming to match new folder/name | |
| 140 | - $orig_file_base = str_replace(' ', '_', $orig_name) . '_' . $orig_widget_id; | |
| 165 | + if ($orig_file_base === '') { | |
| 166 | + $orig_file_base = $folder; // defensive; a missing JSON already returned above. | |
| 167 | + } | |
| 141 | 168 | |
| 169 | + // --- Independence rewrites ---------------------------------------------------------- | |
| 170 | + // Copying files byte-for-byte kept the ORIGINAL class name (Elementor: Wdkit_{file}; | |
| 171 | + // Bricks: {Pascal}_Bricks), block/element names, asset paths and hashes — so the instant | |
| 172 | + // the duplicate co-existed with the original, PHP fataled with "Cannot declare class ... | |
| 173 | + // already in use" (or a block/element registered twice) and the whole site 500'd. Rewrite | |
| 174 | + // every self-referential identifier so the duplicate is a fully independent widget. | |
| 175 | + $new_class = 'Wdkit_' . str_replace('-', '_', $new_file_base); | |
| 176 | + $new_class_bricks = $new_class . '_Bricks'; | |
| 177 | + $orig_slug = sanitize_title($orig_name); | |
| 178 | + $new_slug = sanitize_title($new_name); | |
| 179 | + | |
| 180 | + // Builder-aware PHP class rename (first declaration only). Elementor must match the loader's | |
| 181 | + // derived name exactly; Bricks just needs a unique valid class (its loader resolves the | |
| 182 | + // declared class dynamically). Gutenberg/gutenberg_core use functions, not a class, so the | |
| 183 | + // regex simply finds nothing there. | |
| 184 | + $rewrite_php_class = static function (string $php) use ($builder, $new_class, $new_class_bricks): string { | |
| 185 | + if ($builder === 'elementor') { | |
| 186 | + $out = preg_replace('/\bclass\s+\w+\s+extends\b/', 'class ' . $new_class . ' extends', $php, 1); | |
| 187 | + return is_string($out) ? $out : $php; | |
| 188 | + } | |
| 189 | + if ($builder === 'bricks') { | |
| 190 | + $out = preg_replace('/\bclass\s+\w+\s+extends\b/', 'class ' . $new_class_bricks . ' extends', $php, 1); | |
| 191 | + return is_string($out) ? $out : $php; | |
| 192 | + } | |
| 193 | + return $php; | |
| 194 | + }; | |
| 195 | + | |
| 196 | + // Ordered string rewrites for paths / block names / element $name / asset handles / hashes. | |
| 197 | + // Composite tokens (folder + file base both END with the OLD widget_id) run before the bare | |
| 198 | + // widget_id so its suffix isn't corrupted first. | |
| 199 | + $rewrites = []; | |
| 200 | + if ($folder !== $new_folder) { $rewrites[] = [$folder, $new_folder]; } | |
| 201 | + if ($orig_file_base !== $new_file_base) { $rewrites[] = [$orig_file_base, $new_file_base]; } | |
| 202 | + if ($orig_slug !== '' && $orig_slug !== $new_slug) { | |
| 203 | + $rewrites[] = ['wdkit/' . $orig_slug, 'wdkit/' . $new_slug]; // Gutenberg block name | |
| 204 | + $rewrites[] = ['wdkit-' . $orig_slug, 'wdkit-' . $new_slug]; // Bricks $name + CSS class | |
| 205 | + } | |
| 206 | + if ($orig_widget_id !== '' && $orig_widget_id !== $new_widget_id) { | |
| 207 | + $rewrites[] = [$orig_widget_id, $new_widget_id]; // get_name 'wb-', handles, data-wdkitunique | |
| 208 | + } | |
| 209 | + | |
| 210 | + $apply_rewrites = static function (string $s) use ($rewrites): string { | |
| 211 | + foreach ($rewrites as $p) { | |
| 212 | + if ($p[0] !== '') { | |
| 213 | + $s = str_replace($p[0], $p[1], $s); | |
| 214 | + } | |
| 215 | + } | |
| 216 | + return $s; | |
| 217 | + }; | |
| 218 | + | |
| 142 | 219 | foreach ($src_files as $f) { |
| 143 | 220 | $src_file = $src_dir . '/' . $f; |
| 144 | 221 | $ext = pathinfo($f, PATHINFO_EXTENSION); |
| 145 | 222 | |
| 146 | - // Rename file using new_file_base | |
| 147 | - $new_filename = str_replace($orig_file_base, $new_file_base, $f); | |
| 148 | - // Fallback: if rename didn't match, just copy with original name | |
| 149 | - if ($new_filename === $f && $orig_file_base !== '') { | |
| 223 | + // Rename the file to the new base. | |
| 224 | + $new_filename = ($orig_file_base !== '') ? str_replace($orig_file_base, $new_file_base, $f) : $f; | |
| 225 | + if ($new_filename === $f) { | |
| 150 | 226 | $new_filename = $new_file_base . '.' . $ext; |
| 151 | 227 | } |
| 152 | 228 | $dst_file = $dst_dir . '/' . $new_filename; |
| 153 | 229 | |
| 154 | 230 | if ($ext === 'json') { |
| 155 | - // Patch JSON: update name + widget_id | |
| 231 | + // Patch metadata, then rewrite identifiers so section_data / Editor_data / asset | |
| 232 | + // paths stay in sync with the rewritten code files. | |
| 156 | 233 | $new_json = $orig_json; |
| 157 | - $new_json['widget_data']['widgetdata']['name'] = $new_name; | |
| 158 | - $new_json['widget_data']['widgetdata']['widget_id'] = $new_widget_id; | |
| 159 | - // Clear cloud-specific fields so it starts as local-only | |
| 234 | + $new_json['widget_data']['widgetdata']['name'] = $new_name; | |
| 235 | + $new_json['widget_data']['widgetdata']['widget_id'] = $new_widget_id; | |
| 160 | 236 | $new_json['widget_data']['widgetdata']['r_id'] = 0; |
| 161 | - $new_json['widget_data']['widgetdata']['allow_push'] = false; | |
| 162 | - file_put_contents($dst_file, wp_json_encode($new_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); | |
| 237 | + $new_json['widget_data']['widgetdata']['allow_push'] = false; | |
| 238 | + $json_out = (string) wp_json_encode($new_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); | |
| 239 | + file_put_contents($dst_file, $apply_rewrites($json_out)); | |
| 240 | + } elseif (in_array($ext, ['php', 'js', 'css'], true)) { | |
| 241 | + $content = @file_get_contents($src_file); | |
| 242 | + if ($content === false) { | |
| 243 | + @copy($src_file, $dst_file); | |
| 244 | + } else { | |
| 245 | + if ($ext === 'php') { | |
| 246 | + $content = $rewrite_php_class($content); | |
| 247 | + } | |
| 248 | + if (file_put_contents($dst_file, $apply_rewrites($content)) === false) { | |
| 249 | + @copy($src_file, $dst_file); | |
| 250 | + } | |
| 251 | + } | |
| 163 | 252 | } else { |
| 164 | - @copy($src_file, $dst_file); | |
| 253 | + @copy($src_file, $dst_file); // binary assets (thumbnail image, etc.) | |
| 165 | 254 | } |
| 255 | + } | |
| 256 | + | |
| 257 | + if (function_exists('wdesignkit_invalidate_widget_registry')) { | |
| 258 | + wdesignkit_invalidate_widget_registry($builder); | |
| 166 | 259 | } |
| 167 | 260 | |
| 168 | 261 | return [ |
| 169 | 262 | 'success' => true, |