| 1 |
<?php |
| 2 |
/** |
| 3 |
* Abilities: List available rollback versions and execute a WDesignKit plugin rollback. |
| 4 |
*/ |
| 5 |
|
| 6 |
declare(strict_types=1); |
| 7 |
|
| 8 |
if (!defined('ABSPATH')) { |
| 9 |
exit(); |
| 10 |
} |
| 11 |
|
| 12 |
wp_register_ability('wdesignkit/list-rollback-versions', [ |
| 13 |
'label' => __('List WDesignKit Rollback Versions', 'sprout-mcp'), |
| 14 |
'description' => __( |
| 15 |
'Fetches the list of stable previous WDesignKit plugin versions available for rollback from wordpress.org. Excludes beta, RC, trunk, and dev versions, and any version equal to or newer than the currently installed version.', |
| 16 |
'sprout-mcp', |
| 17 |
), |
| 18 |
'category' => 'wdesignkit', |
| 19 |
'input_schema' => [ |
| 20 |
'type' => 'object', |
| 21 |
'properties' => (object) [], |
| 22 |
'additionalProperties' => false, |
| 23 |
], |
| 24 |
'output_schema' => [ |
| 25 |
'type' => 'object', |
| 26 |
'properties' => [ |
| 27 |
'success' => ['type' => 'boolean'], |
| 28 |
'message' => ['type' => 'string'], |
| 29 |
'current_version' => ['type' => 'string'], |
| 30 |
'versions' => ['type' => 'array', 'items' => ['type' => 'string']], |
| 31 |
], |
| 32 |
], |
| 33 |
'execute_callback' => 'wdesignkit_mcp_list_rollback_versions', |
| 34 |
'permission_callback' => 'sprout_mcp_permission_callback', |
| 35 |
'meta' => [ |
| 36 |
'show_in_rest' => true, |
| 37 |
'mcp' => ['public' => true], |
| 38 |
'annotations' => [ |
| 39 |
'instructions' => implode("\n", [ |
| 40 |
'Lists stable versions older than the current install that can be rolled back to.', |
| 41 |
'Requires a network request to the wordpress.org plugins API.', |
| 42 |
'Pass one of the returned version strings to wdesignkit/rollback to downgrade.', |
| 43 |
]), |
| 44 |
'readonly' => true, |
| 45 |
'destructive' => false, |
| 46 |
'idempotent' => true, |
| 47 |
], |
| 48 |
], |
| 49 |
]); |
| 50 |
|
| 51 |
wp_register_ability('wdesignkit/rollback', [ |
| 52 |
'label' => __('Rollback WDesignKit to Previous Version', 'sprout-mcp'), |
| 53 |
'description' => __( |
| 54 |
'Downgrades the WDesignKit plugin to a specified previous stable version using the WordPress Plugin Upgrader. The plugin is automatically re-activated after installation. This replaces the current plugin files and cannot be undone without a further rollback or update. Requires confirm: true.', |
| 55 |
'sprout-mcp', |
| 56 |
), |
| 57 |
'category' => 'wdesignkit', |
| 58 |
'input_schema' => [ |
| 59 |
'type' => 'object', |
| 60 |
'properties' => [ |
| 61 |
'version' => [ |
| 62 |
'type' => 'string', |
| 63 |
'description' => 'Version string to roll back to (from wdesignkit/list-rollback-versions).', |
| 64 |
], |
| 65 |
'confirm' => [ |
| 66 |
'type' => 'boolean', |
| 67 |
'description' => 'Must be true to execute. Omit or false for a dry-run preview.', |
| 68 |
], |
| 69 |
], |
| 70 |
'required' => ['version'], |
| 71 |
'additionalProperties' => false, |
| 72 |
], |
| 73 |
'output_schema' => [ |
| 74 |
'type' => 'object', |
| 75 |
'properties' => [ |
| 76 |
'success' => ['type' => 'boolean'], |
| 77 |
'message' => ['type' => 'string'], |
| 78 |
'version' => ['type' => 'string'], |
| 79 |
'dry_run' => ['type' => 'boolean'], |
| 80 |
'warning' => ['type' => ['string', 'null']], |
| 81 |
], |
| 82 |
], |
| 83 |
'execute_callback' => 'wdesignkit_mcp_rollback', |
| 84 |
'permission_callback' => 'sprout_mcp_permission_callback', |
| 85 |
'meta' => [ |
| 86 |
'show_in_rest' => true, |
| 87 |
'mcp' => ['public' => true], |
| 88 |
'annotations' => [ |
| 89 |
'instructions' => implode("\n", [ |
| 90 |
'Rolls back WDesignKit to a previous version. Destructive — replaces plugin files.', |
| 91 |
'Always call wdesignkit/list-rollback-versions first to validate the version string.', |
| 92 |
'confirm: true is required to execute; without it the call returns a dry-run preview.', |
| 93 |
'The plugin is re-activated automatically. The MCP connection may need to be re-established after rollback.', |
| 94 |
]), |
| 95 |
'readonly' => false, |
| 96 |
'destructive' => true, |
| 97 |
'idempotent' => false, |
| 98 |
], |
| 99 |
], |
| 100 |
]); |
| 101 |
|
| 102 |
function wdesignkit_mcp_list_rollback_versions(array $input): array { |
| 103 |
if (!defined('WDKIT_VERSION')) { |
| 104 |
return ['success' => false, 'message' => 'WDesignKit plugin is not active.', 'versions' => [], 'current_version' => '']; |
| 105 |
} |
| 106 |
|
| 107 |
require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; |
| 108 |
|
| 109 |
$plugin_info = plugins_api('plugin_information', ['slug' => 'wdesignkit']); |
| 110 |
|
| 111 |
if (is_wp_error($plugin_info)) { |
| 112 |
return [ |
| 113 |
'success' => false, |
| 114 |
'message' => $plugin_info->get_error_message(), |
| 115 |
'current_version' => WDKIT_VERSION, |
| 116 |
'versions' => [], |
| 117 |
]; |
| 118 |
} |
| 119 |
|
| 120 |
if (empty($plugin_info->versions) || !is_array($plugin_info->versions)) { |
| 121 |
return [ |
| 122 |
'success' => false, |
| 123 |
'message' => 'No version data returned from wordpress.org.', |
| 124 |
'current_version' => WDKIT_VERSION, |
| 125 |
'versions' => [], |
| 126 |
]; |
| 127 |
} |
| 128 |
|
| 129 |
$versions = []; |
| 130 |
foreach ($plugin_info->versions as $version => $download_link) { |
| 131 |
$is_valid = !preg_match('/(beta|rc|trunk|dev)/i', strtolower($version)); |
| 132 |
$is_valid = apply_filters('wdkit_check_rollback_version', $is_valid, strtolower($version)); |
| 133 |
|
| 134 |
if (!$is_valid || version_compare($version, WDKIT_VERSION, '>=')) { |
| 135 |
continue; |
| 136 |
} |
| 137 |
|
| 138 |
$versions[] = $version; |
| 139 |
} |
| 140 |
|
| 141 |
// Sort descending by semantic version so multi-digit patch numbers are ordered correctly |
| 142 |
// (e.g. "2.2.10" > "2.2.9" > "2.2.2"). krsort / string comparison incorrectly places |
| 143 |
// "2.2.10" after "2.2.2" because "10" < "2" lexicographically. |
| 144 |
usort($versions, static function (string $a, string $b): int { |
| 145 |
return version_compare($b, $a); |
| 146 |
}); |
| 147 |
|
| 148 |
return [ |
| 149 |
'success' => true, |
| 150 |
'message' => count($versions) . ' rollback version(s) available.', |
| 151 |
'current_version' => WDKIT_VERSION, |
| 152 |
'versions' => $versions, |
| 153 |
]; |
| 154 |
} |
| 155 |
|
| 156 |
function wdesignkit_mcp_rollback(array $input): array { |
| 157 |
if (!defined('WDKIT_VERSION') || !defined('WDKIT_PBNAME')) { |
| 158 |
return ['success' => false, 'message' => 'WDesignKit plugin is not active.', 'dry_run' => false]; |
| 159 |
} |
| 160 |
|
| 161 |
$version = sanitize_text_field((string) ($input['version'] ?? '')); |
| 162 |
$confirm = (bool) ($input['confirm'] ?? false); |
| 163 |
|
| 164 |
if ($version === '') { |
| 165 |
return ['success' => false, 'message' => 'version is required.', 'dry_run' => false]; |
| 166 |
} |
| 167 |
|
| 168 |
// Guard 1: same-version or newer — reject before any network call. |
| 169 |
if (version_compare($version, WDKIT_VERSION, '=')) { |
| 170 |
return [ |
| 171 |
'success' => false, |
| 172 |
'message' => "Cannot roll back to the currently installed version {$version}.", |
| 173 |
'version' => $version, |
| 174 |
'dry_run' => !$confirm, |
| 175 |
]; |
| 176 |
} |
| 177 |
if (version_compare($version, WDKIT_VERSION, '>')) { |
| 178 |
return [ |
| 179 |
'success' => false, |
| 180 |
'message' => "Version {$version} is newer than the installed version " . WDKIT_VERSION . ". Use the plugin updater instead.", |
| 181 |
'version' => $version, |
| 182 |
'dry_run' => !$confirm, |
| 183 |
]; |
| 184 |
} |
| 185 |
|
| 186 |
// Guard 2: validate against the available rollback list from wordpress.org. |
| 187 |
// Runs for BOTH dry-run and execute so a "would roll back to X" preview is |
| 188 |
// never shown for a version that does not exist in the release history. |
| 189 |
require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; |
| 190 |
|
| 191 |
$plugin_info = plugins_api('plugin_information', ['slug' => 'wdesignkit']); |
| 192 |
|
| 193 |
if (is_wp_error($plugin_info) || empty($plugin_info->versions) || !is_array($plugin_info->versions)) { |
| 194 |
return [ |
| 195 |
'success' => false, |
| 196 |
'message' => 'Could not fetch version list from wordpress.org.', |
| 197 |
'dry_run' => !$confirm, |
| 198 |
]; |
| 199 |
} |
| 200 |
|
| 201 |
$valid_versions = []; |
| 202 |
foreach ($plugin_info->versions as $v => $dl) { |
| 203 |
$is_valid = !preg_match('/(beta|rc|trunk|dev)/i', strtolower($v)); |
| 204 |
$is_valid = apply_filters('wdkit_check_rollback_version', $is_valid, strtolower($v)); |
| 205 |
if ($is_valid && version_compare($v, WDKIT_VERSION, '<')) { |
| 206 |
$valid_versions[] = $v; |
| 207 |
} |
| 208 |
} |
| 209 |
|
| 210 |
if (!in_array($version, $valid_versions, true)) { |
| 211 |
return [ |
| 212 |
'success' => false, |
| 213 |
'message' => "Version '{$version}' is not available for rollback. Call wdesignkit/list-rollback-versions to see available versions.", |
| 214 |
'version' => $version, |
| 215 |
'dry_run' => !$confirm, |
| 216 |
]; |
| 217 |
} |
| 218 |
|
| 219 |
// Version is valid. Return a dry-run preview (success:true) if not confirmed. |
| 220 |
if (!$confirm) { |
| 221 |
return [ |
| 222 |
'success' => true, |
| 223 |
'message' => "Dry run: would roll back WDesignKit from " . WDKIT_VERSION . " to {$version}. Pass confirm: true to execute.", |
| 224 |
'version' => $version, |
| 225 |
'dry_run' => true, |
| 226 |
]; |
| 227 |
} |
| 228 |
|
| 229 |
// Execute — $plugin_info already fetched and validated above. |
| 230 |
$plugin_slug = basename(WDKIT_PBNAME, '.php'); |
| 231 |
$package_url = sprintf('https://downloads.wordpress.org/plugin/%s.%s.zip', $plugin_slug, $version); |
| 232 |
|
| 233 |
$plugin_obj = new \stdClass(); |
| 234 |
$plugin_obj->new_version = $version; |
| 235 |
$plugin_obj->slug = $plugin_slug; |
| 236 |
$plugin_obj->package = $package_url; |
| 237 |
$plugin_obj->url = 'https://wdesignkit.com/'; |
| 238 |
|
| 239 |
$update_transient = get_site_transient('update_plugins'); |
| 240 |
if (!is_object($update_transient)) { |
| 241 |
$update_transient = new \stdClass(); |
| 242 |
} |
| 243 |
$update_transient->response[WDKIT_PBNAME] = $plugin_obj; |
| 244 |
set_site_transient('update_plugins', $update_transient); |
| 245 |
|
| 246 |
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; |
| 247 |
|
| 248 |
$upgrader = new \Plugin_Upgrader(new \WP_Ajax_Upgrader_Skin()); |
| 249 |
$result = $upgrader->upgrade(WDKIT_PBNAME); |
| 250 |
|
| 251 |
if (is_wp_error($result)) { |
| 252 |
return [ |
| 253 |
'success' => false, |
| 254 |
'message' => $result->get_error_message(), |
| 255 |
'dry_run' => false, |
| 256 |
]; |
| 257 |
} |
| 258 |
|
| 259 |
activate_plugin(WDKIT_PBNAME); |
| 260 |
|
| 261 |
return [ |
| 262 |
'success' => true, |
| 263 |
'message' => "WDesignKit rolled back to version {$version} and re-activated.", |
| 264 |
'version' => $version, |
| 265 |
'dry_run' => false, |
| 266 |
// The Plugin Upgrader replaces all plugin PHP files on disk. The currently running |
| 267 |
// MCP process loaded the old plugin code at boot — its handler registry, class |
| 268 |
// definitions, and ability list all reflect the pre-rollback version and will not |
| 269 |
// automatically reload. Every subsequent tool call will fail with "Tool execution |
| 270 |
// failed" until the MCP client reconnects (which triggers a fresh PHP bootstrap). |
| 271 |
'warning' => 'MCP server reconnection required: the plugin files have been replaced. Reconnect the MCP client before making any further tool calls.', |
| 272 |
]; |
| 273 |
} |
| 274 |
|