| 1 |
<?php |
| 2 |
/** |
| 3 |
* Ability: Package a local widget as a standalone, installable WordPress plugin ZIP. |
| 4 |
* |
| 5 |
* Mirrors the WP-admin "Download WP Plugin" popup: it posts the widget's PHP/JS/CSS to the |
| 6 |
* cloud packager (api/v2/plugin/download/get), which wraps them in a plugin skeleton and |
| 7 |
* returns a download URL. |
| 8 |
* |
| 9 |
* The one thing the popup does that a raw file upload must not skip: it re-emits the widget's |
| 10 |
* code with WDKIT_DLPFX_ placeholders in front of every class / top-level function / widget |
| 11 |
* name, which the packager swaps for a per-plugin prefix. Without them the downloaded plugin |
| 12 |
* declares the SAME class names WDesignKit already registers, and installing it next to |
| 13 |
* WDesignKit fatals with "Cannot declare class ...". This ability reproduces that step by |
| 14 |
* parsing the stored files and prefixing the identifiers that are actually in them, which |
| 15 |
* also keeps hand-written php_code (from wdesignkit/create-widget) intact. |
| 16 |
*/ |
| 17 |
|
| 18 |
declare(strict_types=1); |
| 19 |
|
| 20 |
if (!defined('ABSPATH')) { |
| 21 |
exit(); |
| 22 |
} |
| 23 |
|
| 24 |
wp_register_ability('wdesignkit/download-plugin', [ |
| 25 |
'label' => __('Download Widget as WordPress Plugin', 'wdesignkit'), |
| 26 |
'description' => __( |
| 27 |
'Packages a local WDesignKit widget into a standalone, installable WordPress plugin ZIP and returns the download URL. Equivalent to the WP-admin "Download WP Plugin" popup. Requires cloud login.', |
| 28 |
'wdesignkit', |
| 29 |
), |
| 30 |
'category' => 'wdesignkit', |
| 31 |
'input_schema' => [ |
| 32 |
'type' => 'object', |
| 33 |
'properties' => [ |
| 34 |
'builder' => [ |
| 35 |
'type' => 'string', |
| 36 |
'description' => 'Builder the widget belongs to.', |
| 37 |
'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'], |
| 38 |
], |
| 39 |
'folder' => [ |
| 40 |
'type' => 'string', |
| 41 |
'description' => 'Widget folder name inside the builder directory (the "folder" field from wdesignkit/list-widgets).', |
| 42 |
], |
| 43 |
'plugin_name' => [ |
| 44 |
'type' => 'string', |
| 45 |
'description' => 'Plugin display name shown in the WordPress plugins list. Required.', |
| 46 |
'maxLength' => 100, |
| 47 |
], |
| 48 |
'plugin_slug' => [ |
| 49 |
'type' => 'string', |
| 50 |
'description' => 'Plugin folder/file slug. Lowercase letters, numbers, and hyphens; must start with a letter. Derived from plugin_name when omitted.', |
| 51 |
'maxLength' => 100, |
| 52 |
], |
| 53 |
'plugin_prefix' => [ |
| 54 |
'type' => 'string', |
| 55 |
'description' => 'Short identifier prefix for the generated class, function, and constant names (e.g. "acme"). Derived from plugin_slug when omitted.', |
| 56 |
'maxLength' => 50, |
| 57 |
], |
| 58 |
'version' => [ |
| 59 |
'type' => 'string', |
| 60 |
'description' => 'Plugin version for the plugin header. Defaults to 1.0.0.', |
| 61 |
'maxLength' => 20, |
| 62 |
], |
| 63 |
'author_name' => [ |
| 64 |
'type' => 'string', |
| 65 |
'description' => 'Plugin author name. Left to the caller — the cloud falls back to "POSIMYTH" only when this is omitted.', |
| 66 |
'maxLength' => 50, |
| 67 |
], |
| 68 |
'author_url' => [ |
| 69 |
'type' => 'string', |
| 70 |
'description' => 'Plugin author URL.', |
| 71 |
], |
| 72 |
'licence_url' => [ |
| 73 |
'type' => 'string', |
| 74 |
'description' => 'URL of the plugin licence.', |
| 75 |
], |
| 76 |
'required_php_version' => [ |
| 77 |
'type' => 'string', |
| 78 |
'description' => 'Minimum PHP version for the plugin header. Defaults to 8.0.', |
| 79 |
], |
| 80 |
'required_wp_version' => [ |
| 81 |
'type' => 'string', |
| 82 |
'description' => 'Minimum WordPress version for the plugin header. Defaults to 6.0.', |
| 83 |
], |
| 84 |
'contributors' => [ |
| 85 |
'type' => 'string', |
| 86 |
'description' => 'readme.txt contributors line.', |
| 87 |
], |
| 88 |
'short_description' => [ |
| 89 |
'type' => 'string', |
| 90 |
'description' => 'Short description for the plugin header and readme.txt.', |
| 91 |
], |
| 92 |
'tags' => [ |
| 93 |
'type' => 'string', |
| 94 |
'description' => 'Comma-separated readme.txt tags.', |
| 95 |
], |
| 96 |
'text_domain' => [ |
| 97 |
'type' => 'string', |
| 98 |
'description' => 'Translation text domain. Defaults to the plugin slug.', |
| 99 |
], |
| 100 |
'faq' => [ |
| 101 |
'type' => 'string', |
| 102 |
'description' => 'readme.txt FAQ section content.', |
| 103 |
], |
| 104 |
'changelog' => [ |
| 105 |
'type' => 'string', |
| 106 |
'description' => 'readme.txt changelog section content.', |
| 107 |
], |
| 108 |
], |
| 109 |
'required' => ['builder', 'folder', 'plugin_name'], |
| 110 |
'additionalProperties' => false, |
| 111 |
], |
| 112 |
'output_schema' => [ |
| 113 |
'type' => 'object', |
| 114 |
'properties' => [ |
| 115 |
'success' => ['type' => 'boolean'], |
| 116 |
'message' => ['type' => 'string'], |
| 117 |
'download_url' => ['type' => 'string'], |
| 118 |
'zip_file_name' => ['type' => 'string'], |
| 119 |
'plugin_slug' => ['type' => 'string'], |
| 120 |
'response' => ['type' => ['object', 'array']], |
| 121 |
], |
| 122 |
], |
| 123 |
'execute_callback' => 'wdesignkit_mcp_download_plugin', |
| 124 |
'permission_callback' => 'wdesignkit_mcp_permission_callback', |
| 125 |
'meta' => [ |
| 126 |
'public' => true, |
| 127 |
'show_in_rest' => true, |
| 128 |
'mcp' => ['public' => true], |
| 129 |
'annotations' => [ |
| 130 |
'instructions' => implode("\n", [ |
| 131 |
'Packages one local widget as a standalone WordPress plugin ZIP. Requires cloud login.', |
| 132 |
'Get builder + folder from wdesignkit/list-widgets.', |
| 133 |
'plugin_name is required. plugin_slug must match ^[a-z][a-z0-9-]*$ — it is derived from plugin_name when omitted.', |
| 134 |
'Returns download_url: a temporary link to the ZIP, valid for about an hour.', |
| 135 |
'The generated plugin gets per-plugin class/function prefixes, so it can be installed on a site', |
| 136 |
'that already runs WDesignKit with the same widget without a class-redeclaration fatal.', |
| 137 |
]), |
| 138 |
'readonly' => false, |
| 139 |
'destructive' => false, |
| 140 |
'idempotent' => false, |
| 141 |
], |
| 142 |
], |
| 143 |
]); |
| 144 |
|
| 145 |
/** |
| 146 |
* Collect the class and top-level function names declared in a PHP file. |
| 147 |
* |
| 148 |
* Uses the tokenizer rather than regex so class methods (which must keep their names) are |
| 149 |
* never confused with top-level functions, and so names inside strings or comments are not |
| 150 |
* mistaken for declarations. |
| 151 |
* |
| 152 |
* @param string $php PHP source. |
| 153 |
* @return string[] Declared identifiers, in declaration order. |
| 154 |
*/ |
| 155 |
function wdesignkit_mcp_dl_declared_identifiers(string $php): array { |
| 156 |
if (!function_exists('token_get_all')) { |
| 157 |
return []; |
| 158 |
} |
| 159 |
|
| 160 |
$tokens = @token_get_all($php); |
| 161 |
if (!is_array($tokens)) { |
| 162 |
return []; |
| 163 |
} |
| 164 |
|
| 165 |
$names = []; |
| 166 |
$depth = 0; |
| 167 |
$class_depths = []; |
| 168 |
$expect = ''; |
| 169 |
|
| 170 |
foreach ($tokens as $token) { |
| 171 |
if (is_string($token)) { |
| 172 |
if ('{' === $token) { |
| 173 |
$depth++; |
| 174 |
} elseif ('}' === $token) { |
| 175 |
$depth--; |
| 176 |
// Leaving a class body — stop treating functions as methods. |
| 177 |
while (!empty($class_depths) && end($class_depths) > $depth) { |
| 178 |
array_pop($class_depths); |
| 179 |
} |
| 180 |
} |
| 181 |
$expect = ''; |
| 182 |
continue; |
| 183 |
} |
| 184 |
|
| 185 |
[$id, $text] = $token; |
| 186 |
|
| 187 |
if (T_WHITESPACE === $id || T_COMMENT === $id || T_DOC_COMMENT === $id) { |
| 188 |
continue; |
| 189 |
} |
| 190 |
|
| 191 |
if (T_CLASS === $id) { |
| 192 |
$expect = 'class'; |
| 193 |
continue; |
| 194 |
} |
| 195 |
|
| 196 |
if (T_FUNCTION === $id) { |
| 197 |
$expect = 'function'; |
| 198 |
continue; |
| 199 |
} |
| 200 |
|
| 201 |
if (T_STRING === $id && '' !== $expect) { |
| 202 |
if ('class' === $expect) { |
| 203 |
$names[] = $text; |
| 204 |
$class_depths[] = $depth + 1; |
| 205 |
} elseif (empty($class_depths)) { |
| 206 |
// Top-level function only; methods keep their names. |
| 207 |
$names[] = $text; |
| 208 |
} |
| 209 |
} |
| 210 |
|
| 211 |
$expect = ''; |
| 212 |
} |
| 213 |
|
| 214 |
return array_values(array_unique($names)); |
| 215 |
} |
| 216 |
|
| 217 |
/** |
| 218 |
* Insert the WDKIT_DLPFX_ placeholder in front of every identifier the packager must rename. |
| 219 |
* |
| 220 |
* @param string $php Stored widget PHP. |
| 221 |
* @param string $builder Builder slug. |
| 222 |
* @return string PHP with placeholders, ready for the cloud packager. |
| 223 |
*/ |
| 224 |
function wdesignkit_mcp_dl_prefix_php(string $php, string $builder): string { |
| 225 |
if ('' === trim($php)) { |
| 226 |
return $php; |
| 227 |
} |
| 228 |
|
| 229 |
// Asset URLs: the generators emit an uploads path on THIS site |
| 230 |
// ($baseurl . '/wdesignkit/<builder>/<folder>/<file>.css'), which does not exist on |
| 231 |
// whatever site the downloaded plugin is installed on. Hand the packager the |
| 232 |
// WDKIT_SERVER_PATH token it already knows how to repoint at the packaged assets. |
| 233 |
$php = preg_replace('#\$baseurl\s*\.\s*\'/wdesignkit/#', "WDKIT_SERVER_PATH . '/", $php); |
| 234 |
|
| 235 |
foreach (wdesignkit_mcp_dl_declared_identifiers($php) as $name) { |
| 236 |
$php = preg_replace( |
| 237 |
'/\b' . preg_quote($name, '/') . '\b/', |
| 238 |
'WDKIT_DLPFX_' . $name, |
| 239 |
$php |
| 240 |
); |
| 241 |
} |
| 242 |
|
| 243 |
// Elementor/Bricks widget name string ('wb-<hash>'). The quote immediately before "wb-" |
| 244 |
// keeps this away from the Gutenberg block name 'wdkit/wb-<hash>', which the packager |
| 245 |
// deliberately leaves alone and guards at registration instead. |
| 246 |
if (in_array($builder, ['elementor', 'bricks'], true)) { |
| 247 |
$php = preg_replace('/([\'"])wb-/', '${1}WDKIT_DLPFX_wb-', $php); |
| 248 |
} |
| 249 |
|
| 250 |
// Elementor widget classes are registered by the file itself in a downloaded plugin — |
| 251 |
// the generated plugin's main class only require_once's this file. |
| 252 |
if ('elementor' === $builder |
| 253 |
&& false === strpos($php, 'widgets_manager->register') |
| 254 |
&& preg_match('/\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\s+extends\s+\\\\?Widget_Base\b/', $php, $m) |
| 255 |
) { |
| 256 |
$php = rtrim($php) . "\n\n\\Elementor\\Plugin::instance()->widgets_manager->register( new {$m[1]}() );\n"; |
| 257 |
} |
| 258 |
|
| 259 |
return $php; |
| 260 |
} |
| 261 |
|
| 262 |
/** |
| 263 |
* Prefix the classes declared in a widget's editor JS (Gutenberg builds wrap the block in one). |
| 264 |
* |
| 265 |
* @param string $js Stored widget JS. |
| 266 |
* @return string |
| 267 |
*/ |
| 268 |
function wdesignkit_mcp_dl_prefix_js(string $js): string { |
| 269 |
if ('' === trim($js)) { |
| 270 |
return $js; |
| 271 |
} |
| 272 |
|
| 273 |
if (!preg_match_all('/\bclass\s+([A-Za-z_$][A-Za-z0-9_$]*)/', $js, $matches)) { |
| 274 |
return $js; |
| 275 |
} |
| 276 |
|
| 277 |
foreach (array_unique($matches[1]) as $name) { |
| 278 |
$js = preg_replace( |
| 279 |
'/\b' . preg_quote($name, '/') . '\b/', |
| 280 |
'WDKIT_DLPFX_' . $name, |
| 281 |
$js |
| 282 |
); |
| 283 |
} |
| 284 |
|
| 285 |
return $js; |
| 286 |
} |
| 287 |
|
| 288 |
function wdesignkit_mcp_download_plugin(array $input): array { |
| 289 |
// Timeout guard: the cloud builds and zips the plugin during this request. |
| 290 |
set_time_limit(120); |
| 291 |
|
| 292 |
if (!defined('WDKIT_BUILDER_PATH') || !defined('WDKIT_SERVER_API_URL')) { |
| 293 |
return ['success' => false, 'message' => 'WDesignKit plugin is not active.']; |
| 294 |
} |
| 295 |
|
| 296 |
$auth = wdesignkit_mcp_template_get_auth(); |
| 297 |
if (empty($auth['logged_in'])) { |
| 298 |
return ['success' => false, 'message' => $auth['message'] ?? 'Not logged in to WDesignKit cloud.']; |
| 299 |
} |
| 300 |
|
| 301 |
$builder = sanitize_text_field((string) ($input['builder'] ?? '')); |
| 302 |
$folder = sanitize_file_name((string) ($input['folder'] ?? '')); |
| 303 |
|
| 304 |
if (!in_array($builder, ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'], true)) { |
| 305 |
return ['success' => false, 'message' => 'Invalid builder type.']; |
| 306 |
} |
| 307 |
|
| 308 |
$widget_dir = WDKIT_BUILDER_PATH . '/' . $builder . '/' . $folder; |
| 309 |
|
| 310 |
if ('' === $folder || !is_dir($widget_dir)) { |
| 311 |
return ['success' => false, 'message' => "Widget folder not found: {$builder}/{$folder}"]; |
| 312 |
} |
| 313 |
|
| 314 |
$real_widget = realpath($widget_dir); |
| 315 |
$real_base = realpath(WDKIT_BUILDER_PATH); |
| 316 |
if (!$real_widget || !$real_base || strpos($real_widget, $real_base . DIRECTORY_SEPARATOR) !== 0) { |
| 317 |
return ['success' => false, 'message' => 'Invalid widget path.']; |
| 318 |
} |
| 319 |
|
| 320 |
// --- Plugin metadata ------------------------------------------------------------ |
| 321 |
$plugin_name = trim(sanitize_text_field((string) ($input['plugin_name'] ?? ''))); |
| 322 |
|
| 323 |
if ('' === $plugin_name) { |
| 324 |
return ['success' => false, 'message' => 'plugin_name is required.']; |
| 325 |
} |
| 326 |
|
| 327 |
$plugin_slug = strtolower(trim((string) ($input['plugin_slug'] ?? ''))); |
| 328 |
if ('' === $plugin_slug) { |
| 329 |
$plugin_slug = sanitize_title($plugin_name); |
| 330 |
} |
| 331 |
|
| 332 |
// Mirror the cloud's own slug rule so a bad slug fails here with a usable message |
| 333 |
// instead of coming back as a generic 400 from the packager. |
| 334 |
if (!preg_match('/^[a-z][a-z0-9-]{0,99}$/', $plugin_slug)) { |
| 335 |
return [ |
| 336 |
'success' => false, |
| 337 |
'message' => "Invalid plugin_slug '{$plugin_slug}'. Use lowercase letters, numbers, and hyphens only, starting with a letter.", |
| 338 |
]; |
| 339 |
} |
| 340 |
|
| 341 |
$plugin_prefix = strtolower(trim((string) ($input['plugin_prefix'] ?? ''))); |
| 342 |
if ('' === $plugin_prefix) { |
| 343 |
// First slug segment keeps generated identifiers short and readable. |
| 344 |
$plugin_prefix = explode('-', $plugin_slug)[0]; |
| 345 |
} |
| 346 |
|
| 347 |
if (!preg_match('/^[a-z][a-z0-9_-]{0,49}$/', $plugin_prefix)) { |
| 348 |
return [ |
| 349 |
'success' => false, |
| 350 |
'message' => "Invalid plugin_prefix '{$plugin_prefix}'. Use lowercase letters, numbers, hyphens, and underscores only, starting with a letter.", |
| 351 |
]; |
| 352 |
} |
| 353 |
|
| 354 |
// --- Widget files --------------------------------------------------------------- |
| 355 |
$files = array_diff(@scandir($widget_dir) ?: [], ['.', '..']); |
| 356 |
$json_data = null; |
| 357 |
$php_file = ''; |
| 358 |
$css_file = ''; |
| 359 |
$js_file = ''; |
| 360 |
$react_file = ''; |
| 361 |
|
| 362 |
foreach ($files as $f) { |
| 363 |
$path = $widget_dir . '/' . $f; |
| 364 |
$ext = pathinfo($f, PATHINFO_EXTENSION); |
| 365 |
|
| 366 |
if ('index.js' === $f) { |
| 367 |
// Gutenberg frontend loader — packaged separately from the editor JS. |
| 368 |
$react_file = (string) @file_get_contents($path); |
| 369 |
continue; |
| 370 |
} |
| 371 |
|
| 372 |
if ('json' === $ext && null === $json_data) { |
| 373 |
$raw = @file_get_contents($path); |
| 374 |
$json_data = (false !== $raw) ? json_decode($raw, true) : null; |
| 375 |
} elseif ('php' === $ext && '' === $php_file) { |
| 376 |
$php_file = (string) @file_get_contents($path); |
| 377 |
} elseif ('css' === $ext && '' === $css_file) { |
| 378 |
$css_file = (string) @file_get_contents($path); |
| 379 |
} elseif ('js' === $ext && '' === $js_file) { |
| 380 |
$js_file = (string) @file_get_contents($path); |
| 381 |
} |
| 382 |
} |
| 383 |
|
| 384 |
if ('' === trim($php_file)) { |
| 385 |
return ['success' => false, 'message' => "No PHP file found in {$builder}/{$folder}. The widget cannot be packaged without it."]; |
| 386 |
} |
| 387 |
|
| 388 |
$widgetdata = $json_data['widget_data']['widgetdata'] ?? []; |
| 389 |
if (empty($widgetdata) || !is_array($widgetdata)) { |
| 390 |
return ['success' => false, 'message' => "Widget JSON config missing or unreadable in {$builder}/{$folder}."]; |
| 391 |
} |
| 392 |
|
| 393 |
// The packager reads widgetdata.type to decide which plugin skeleton to build. |
| 394 |
$widgetdata['type'] = $builder; |
| 395 |
|
| 396 |
// --- Prefix identifiers so the ZIP can coexist with WDesignKit -------------------- |
| 397 |
$php_file = wdesignkit_mcp_dl_prefix_php($php_file, $builder); |
| 398 |
$js_file = wdesignkit_mcp_dl_prefix_js($js_file); |
| 399 |
|
| 400 |
if ('' !== $react_file) { |
| 401 |
$react_file = wdesignkit_mcp_dl_prefix_js($react_file); |
| 402 |
} |
| 403 |
|
| 404 |
$args = [ |
| 405 |
'token' => $auth['token'], |
| 406 |
'pluginName' => $plugin_name, |
| 407 |
'pluginSlug' => $plugin_slug, |
| 408 |
'pluginPrefix' => $plugin_prefix, |
| 409 |
// version is honoured by the cloud packager's plugin header. |
| 410 |
'version' => sanitize_text_field((string) ($input['version'] ?? '1.0.0')), |
| 411 |
'authorName' => sanitize_text_field((string) ($input['author_name'] ?? '')), |
| 412 |
'authorUrl' => esc_url_raw((string) ($input['author_url'] ?? '')), |
| 413 |
'licenceUrl' => esc_url_raw((string) ($input['licence_url'] ?? '')), |
| 414 |
'requiredPhpVersion' => sanitize_text_field((string) ($input['required_php_version'] ?? '')), |
| 415 |
'requiredWpVersion' => sanitize_text_field((string) ($input['required_wp_version'] ?? '')), |
| 416 |
'contributors' => sanitize_text_field((string) ($input['contributors'] ?? '')), |
| 417 |
'shortDescription' => sanitize_text_field((string) ($input['short_description'] ?? '')), |
| 418 |
'tags' => sanitize_text_field((string) ($input['tags'] ?? '')), |
| 419 |
'textDomain' => sanitize_text_field((string) ($input['text_domain'] ?? '')), |
| 420 |
'faq' => wp_kses_post((string) ($input['faq'] ?? '')), |
| 421 |
'changelog' => wp_kses_post((string) ($input['changelog'] ?? '')), |
| 422 |
'php_file' => $php_file, |
| 423 |
'js_file' => $js_file, |
| 424 |
'css_file' => $css_file, |
| 425 |
'widgetdata' => wp_json_encode($widgetdata), |
| 426 |
'unique_id' => get_option('wdkit_unique_id', ''), |
| 427 |
]; |
| 428 |
|
| 429 |
if ('' !== $react_file) { |
| 430 |
$args['js_react_file'] = $react_file; |
| 431 |
} |
| 432 |
|
| 433 |
$response = wp_remote_post( |
| 434 |
WDKIT_SERVER_API_URL . 'api/v2/plugin/download/get', |
| 435 |
[ |
| 436 |
'method' => 'POST', |
| 437 |
'body' => $args, |
| 438 |
'timeout' => 90, |
| 439 |
] |
| 440 |
); |
| 441 |
|
| 442 |
if (is_wp_error($response)) { |
| 443 |
return ['success' => false, 'message' => $response->get_error_message()]; |
| 444 |
} |
| 445 |
|
| 446 |
$status = wp_remote_retrieve_response_code($response); |
| 447 |
$body = wp_remote_retrieve_body($response); |
| 448 |
$data = json_decode($body, true); |
| 449 |
|
| 450 |
if (!is_array($data)) { |
| 451 |
return [ |
| 452 |
'success' => false, |
| 453 |
'message' => "Cloud returned status {$status} with a non-JSON body while building the plugin ZIP.", |
| 454 |
]; |
| 455 |
} |
| 456 |
|
| 457 |
if (200 !== (int) $status || empty($data['success'])) { |
| 458 |
return [ |
| 459 |
'success' => false, |
| 460 |
'message' => $data['message'] ?? $data['massage'] ?? "Cloud returned status {$status}.", |
| 461 |
'response' => $data, |
| 462 |
]; |
| 463 |
} |
| 464 |
|
| 465 |
$download_url = $data['data']['download_url'] ?? ''; |
| 466 |
$zip_name = $data['data']['zip_file_name'] ?? ''; |
| 467 |
|
| 468 |
if ('' === $download_url) { |
| 469 |
return [ |
| 470 |
'success' => false, |
| 471 |
'message' => 'The cloud reported success but returned no download URL.', |
| 472 |
'response' => $data, |
| 473 |
]; |
| 474 |
} |
| 475 |
|
| 476 |
return [ |
| 477 |
'success' => true, |
| 478 |
'message' => "Plugin '{$plugin_name}' packaged from {$builder}/{$folder}. Download it from download_url (the link expires after about an hour).", |
| 479 |
'download_url' => $download_url, |
| 480 |
'zip_file_name' => $zip_name, |
| 481 |
'plugin_slug' => $plugin_slug, |
| 482 |
'response' => $data, |
| 483 |
]; |
| 484 |
} |
| 485 |
|