class-avcf-abilities-base.php
3 weeks ago
class-avcf-abilities-block-navigation.php
3 weeks ago
class-avcf-abilities-cache.php
3 weeks ago
class-avcf-abilities-content.php
3 weeks ago
class-avcf-abilities-core.php
3 weeks ago
class-avcf-abilities-global-styles.php
3 weeks ago
class-avcf-abilities-gutenberg.php
3 weeks ago
class-avcf-abilities-media.php
3 weeks ago
class-avcf-abilities-metadata.php
3 weeks ago
class-avcf-abilities-navigation.php
3 weeks ago
class-avcf-abilities-patterns.php
3 weeks ago
class-avcf-abilities-plugins.php
3 weeks ago
class-avcf-abilities-settings.php
3 weeks ago
class-avcf-abilities-taxonomies.php
3 weeks ago
class-avcf-abilities-templates.php
3 weeks ago
class-avcf-abilities-theme-files.php
3 weeks ago
class-avcf-abilities-themes.php
3 weeks ago
class-avcf-abilities-users.php
3 weeks ago
class-avcf-abilities-core.php
604 lines
| 1 | <?php |
| 2 | /** |
| 3 | * WordPress core / environment MCP abilities. |
| 4 | * |
| 5 | * Reports on the host environment: WordPress version and pending updates, |
| 6 | * PHP version and WordPress support status, database server version, and |
| 7 | * multisite configuration. Built so the AI layer can audit a site before |
| 8 | * recommending major operations (upgrades, theme switches, plugin installs) |
| 9 | * that depend on the runtime environment. |
| 10 | * |
| 11 | * Exposed abilities: |
| 12 | * atarim/get-core-status WP + PHP + DB + multisite snapshot in one call. |
| 13 | * atarim/check-core-updates Force a fresh WordPress.org update check. |
| 14 | * atarim/update-core Update core to the latest (or a specific offered) version. |
| 15 | * atarim/reinstall-core Re-install the current version to repair core files. |
| 16 | * |
| 17 | * Note: abilities are exposed to the MCP server automatically (dynamic |
| 18 | * enumeration minus the avcf_mcp_blocked_abilities blacklist); no manual |
| 19 | * registration list needs editing when adding abilities here. |
| 20 | * |
| 21 | * @package atarim-visual-collaboration |
| 22 | */ |
| 23 | |
| 24 | if ( ! defined('ABSPATH') ) { |
| 25 | exit; |
| 26 | } |
| 27 | |
| 28 | class AVCF_Abilities_Core extends AVCF_Abilities_Base { |
| 29 | |
| 30 | /** |
| 31 | * Register all core / environment abilities. |
| 32 | * Called from AVCF_MCP::avcf_mcp_register_abilities() on wp_abilities_api_init. |
| 33 | */ |
| 34 | public function register() { |
| 35 | |
| 36 | // ---- get-core-status ---- |
| 37 | wp_register_ability( 'atarim/get-core-status', [ |
| 38 | 'label' => 'Get Core Status', |
| 39 | 'description' => 'Returns a snapshot of the WordPress core, PHP, and database environment plus pending core update info and multisite configuration. Use this to audit the site before recommending or attempting major operations like theme switches, large plugin installs, or PHP-dependent upgrades.', |
| 40 | 'category' => 'atarim', |
| 41 | 'input_schema' => [ |
| 42 | 'type' => 'object', |
| 43 | 'properties' => new \stdClass(), |
| 44 | 'additionalProperties' => false, |
| 45 | ], |
| 46 | 'output_schema' => [ |
| 47 | 'type' => 'object', |
| 48 | 'properties' => [ |
| 49 | 'wordpress' => [ |
| 50 | 'type' => 'object', |
| 51 | 'properties' => [ |
| 52 | 'version' => [ 'type' => 'string' ], |
| 53 | 'latest_version' => [ 'type' => 'string' ], |
| 54 | 'update_available' => [ 'type' => 'boolean' ], |
| 55 | 'update_type' => [ 'type' => 'string' ], |
| 56 | 'automatic_updates_enabled' => [ 'type' => 'boolean' ], |
| 57 | 'language' => [ 'type' => 'string' ], |
| 58 | ], |
| 59 | ], |
| 60 | 'php' => [ |
| 61 | 'type' => 'object', |
| 62 | 'properties' => [ |
| 63 | 'version' => [ 'type' => 'string' ], |
| 64 | 'recommended_minimum' => [ 'type' => 'string' ], |
| 65 | 'is_supported_by_wp' => [ 'type' => 'boolean' ], |
| 66 | 'memory_limit' => [ 'type' => 'string' ], |
| 67 | 'max_execution_time' => [ 'type' => 'integer' ], |
| 68 | ], |
| 69 | ], |
| 70 | 'database' => [ |
| 71 | 'type' => 'object', |
| 72 | 'properties' => [ |
| 73 | 'server_version' => [ 'type' => 'string' ], |
| 74 | 'server_type' => [ 'type' => 'string' ], |
| 75 | 'charset' => [ 'type' => 'string' ], |
| 76 | 'collate' => [ 'type' => 'string' ], |
| 77 | ], |
| 78 | ], |
| 79 | 'multisite' => [ |
| 80 | 'type' => 'object', |
| 81 | 'properties' => [ |
| 82 | 'is_multisite' => [ 'type' => 'boolean' ], |
| 83 | 'is_main_site' => [ 'type' => 'boolean' ], |
| 84 | 'site_count' => [ 'type' => 'integer' ], |
| 85 | 'network_id' => [ 'type' => 'integer' ], |
| 86 | ], |
| 87 | ], |
| 88 | 'message' => [ 'type' => 'string' ], |
| 89 | ], |
| 90 | 'required' => [ 'wordpress', 'php', 'database', 'multisite' ], |
| 91 | ], |
| 92 | 'execute_callback' => function( $input = [] ) { |
| 93 | global $wp_version, $wpdb; |
| 94 | |
| 95 | // --- WordPress core --- |
| 96 | if ( ! function_exists( 'get_core_updates' ) ) { |
| 97 | require_once ABSPATH . 'wp-admin/includes/update.php'; |
| 98 | } |
| 99 | |
| 100 | $core_updates = get_core_updates(); |
| 101 | $latest_version = $wp_version; |
| 102 | $update_available = false; |
| 103 | $update_type = 'none'; |
| 104 | |
| 105 | if ( is_array( $core_updates ) && ! empty( $core_updates ) ) { |
| 106 | foreach ( $core_updates as $update ) { |
| 107 | if ( isset( $update->response ) && $update->response === 'upgrade' && isset( $update->current ) ) { |
| 108 | $latest_version = $update->current; |
| 109 | $update_available = true; |
| 110 | // Determine major vs minor vs patch. |
| 111 | $current_parts = explode( '.', $wp_version ); |
| 112 | $latest_parts = explode( '.', $latest_version ); |
| 113 | if ( isset( $current_parts[0], $latest_parts[0] ) && $current_parts[0] !== $latest_parts[0] ) { |
| 114 | $update_type = 'major'; |
| 115 | } elseif ( isset( $current_parts[1], $latest_parts[1] ) && ( ! isset( $current_parts[1] ) || ! isset( $latest_parts[1] ) || $current_parts[1] !== $latest_parts[1] ) ) { |
| 116 | $update_type = 'minor'; |
| 117 | } else { |
| 118 | $update_type = 'patch'; |
| 119 | } |
| 120 | break; |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | // WordPress auto-update preferences. WP exposes these via constants and filters. |
| 126 | $auto_updates_enabled = ! defined( 'AUTOMATIC_UPDATER_DISABLED' ) || ! AUTOMATIC_UPDATER_DISABLED; |
| 127 | // Core minor updates default-on in modern WP; respect explicit user opt-out. |
| 128 | if ( defined( 'WP_AUTO_UPDATE_CORE' ) ) { |
| 129 | $auto_updates_enabled = (bool) WP_AUTO_UPDATE_CORE; |
| 130 | } |
| 131 | |
| 132 | // --- PHP --- |
| 133 | // WordPress's recommended PHP minimum, surfaced in admin notices. |
| 134 | // Current as of WP 6.x: 7.4 absolute minimum, 8.1+ recommended. |
| 135 | // We expose WP's hard-floor (RECOMMENDED_PHP_VERSION) when defined. |
| 136 | $php_recommended = ''; |
| 137 | if ( defined( 'WP_PHP_MIN_VERSION' ) ) { |
| 138 | $php_recommended = (string) WP_PHP_MIN_VERSION; |
| 139 | } elseif ( function_exists( 'wp_check_php_version' ) ) { |
| 140 | // wp_check_php_version() returns a structure with recommended_version |
| 141 | $check = wp_check_php_version(); |
| 142 | if ( is_array( $check ) && isset( $check['recommended_version'] ) ) { |
| 143 | $php_recommended = (string) $check['recommended_version']; |
| 144 | } |
| 145 | } |
| 146 | if ( $php_recommended === '' ) { |
| 147 | // Fallback to the value WP has used as the floor for several years. |
| 148 | $php_recommended = '7.4'; |
| 149 | } |
| 150 | |
| 151 | $php_supported = version_compare( PHP_VERSION, $php_recommended, '>=' ); |
| 152 | |
| 153 | // --- Database --- |
| 154 | $db_version = ''; |
| 155 | $db_type = ''; |
| 156 | if ( $wpdb && method_exists( $wpdb, 'db_server_info' ) ) { |
| 157 | $db_version = (string) $wpdb->db_version(); |
| 158 | $raw_info = (string) $wpdb->db_server_info(); |
| 159 | $db_type = ( stripos( $raw_info, 'mariadb' ) !== false ) ? 'MariaDB' : 'MySQL'; |
| 160 | } elseif ( $wpdb ) { |
| 161 | $db_version = (string) $wpdb->db_version(); |
| 162 | $db_type = 'MySQL'; |
| 163 | } |
| 164 | |
| 165 | // --- Multisite --- |
| 166 | $is_multisite = is_multisite(); |
| 167 | $multisite = [ |
| 168 | 'is_multisite' => $is_multisite, |
| 169 | 'is_main_site' => $is_multisite ? is_main_site() : true, |
| 170 | 'site_count' => $is_multisite ? (int) get_blog_count() : 1, |
| 171 | 'network_id' => $is_multisite ? (int) get_current_network_id() : 0, |
| 172 | ]; |
| 173 | |
| 174 | $message = sprintf( |
| 175 | 'WordPress %s on PHP %s, %s %s. %s', |
| 176 | $wp_version, |
| 177 | PHP_VERSION, |
| 178 | $db_type !== '' ? $db_type : 'database', |
| 179 | $db_version, |
| 180 | $update_available |
| 181 | ? sprintf( 'Core update available: %s (%s).', $latest_version, $update_type ) |
| 182 | : 'Core is up to date.' |
| 183 | ); |
| 184 | |
| 185 | return [ |
| 186 | 'wordpress' => [ |
| 187 | 'version' => $wp_version, |
| 188 | 'latest_version' => $latest_version, |
| 189 | 'update_available' => $update_available, |
| 190 | 'update_type' => $update_type, |
| 191 | 'automatic_updates_enabled' => (bool) $auto_updates_enabled, |
| 192 | 'language' => (string) get_locale(), |
| 193 | ], |
| 194 | 'php' => [ |
| 195 | 'version' => PHP_VERSION, |
| 196 | 'recommended_minimum' => $php_recommended, |
| 197 | 'is_supported_by_wp' => $php_supported, |
| 198 | 'memory_limit' => (string) ini_get( 'memory_limit' ), |
| 199 | 'max_execution_time' => (int) ini_get( 'max_execution_time' ), |
| 200 | ], |
| 201 | 'database' => [ |
| 202 | 'server_version' => $db_version, |
| 203 | 'server_type' => $db_type, |
| 204 | 'charset' => isset( $wpdb->charset ) ? (string) $wpdb->charset : '', |
| 205 | 'collate' => isset( $wpdb->collate ) ? (string) $wpdb->collate : '', |
| 206 | ], |
| 207 | 'multisite' => $multisite, |
| 208 | 'message' => $message, |
| 209 | ]; |
| 210 | }, |
| 211 | 'permission_callback' => function() { |
| 212 | // Reading environment info is moderately sensitive (PHP version, server type can |
| 213 | // help an attacker fingerprint the host). Gate on update_core which is held by |
| 214 | // administrators only, matching WP's own dashboard widget visibility. |
| 215 | return current_user_can( 'update_core' ); |
| 216 | }, |
| 217 | 'meta' => [ |
| 218 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 219 | 'annotations' => [ |
| 220 | 'readonly' => true, |
| 221 | 'destructive' => false, |
| 222 | 'idempotent' => true, |
| 223 | ], |
| 224 | ], |
| 225 | ] ); |
| 226 | |
| 227 | // ---- check-core-updates ---- |
| 228 | wp_register_ability( 'atarim/check-core-updates', [ |
| 229 | 'label' => 'Check For Core Updates', |
| 230 | 'description' => 'Forces a fresh check with WordPress.org for core updates, bypassing the cached result (which can be up to ~12 hours stale), and reports whether an update is available. Use this to get an authoritative, up-to-the-minute answer before deciding whether to run update-core.', |
| 231 | 'category' => 'atarim', |
| 232 | 'input_schema' => [ |
| 233 | 'type' => 'object', |
| 234 | 'properties' => new \stdClass(), |
| 235 | 'additionalProperties' => false, |
| 236 | ], |
| 237 | 'output_schema' => [ |
| 238 | 'type' => 'object', |
| 239 | 'properties' => [ |
| 240 | 'current_version' => [ 'type' => 'string' ], |
| 241 | 'latest_version' => [ 'type' => 'string' ], |
| 242 | 'update_available' => [ 'type' => 'boolean' ], |
| 243 | 'update_type' => [ 'type' => 'string' ], |
| 244 | 'checked_at' => [ 'type' => 'string' ], |
| 245 | 'message' => [ 'type' => 'string' ], |
| 246 | ], |
| 247 | 'required' => [ 'current_version', 'latest_version', 'update_available', 'message' ], |
| 248 | ], |
| 249 | 'execute_callback' => function( $input = [] ) { |
| 250 | global $wp_version; |
| 251 | |
| 252 | if ( ! function_exists( 'get_core_updates' ) ) { |
| 253 | require_once ABSPATH . 'wp-admin/includes/update.php'; |
| 254 | } |
| 255 | |
| 256 | // Force a fresh remote check (bypasses the update_core transient). |
| 257 | wp_version_check( [], true ); |
| 258 | |
| 259 | $updates = get_core_updates(); |
| 260 | $current_version = $wp_version; |
| 261 | $latest_version = $wp_version; |
| 262 | $update_available = false; |
| 263 | $update_type = 'none'; |
| 264 | |
| 265 | if ( is_array( $updates ) && ! empty( $updates ) ) { |
| 266 | foreach ( $updates as $update ) { |
| 267 | if ( isset( $update->response ) && $update->response === 'upgrade' && isset( $update->current ) ) { |
| 268 | $latest_version = (string) $update->current; |
| 269 | $update_available = true; |
| 270 | $update_type = $this->avcf_core_update_type( $current_version, $latest_version ); |
| 271 | break; |
| 272 | } |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | return [ |
| 277 | 'current_version' => $current_version, |
| 278 | 'latest_version' => $latest_version, |
| 279 | 'update_available' => $update_available, |
| 280 | 'update_type' => $update_type, |
| 281 | 'checked_at' => gmdate( 'c' ), |
| 282 | 'message' => $update_available |
| 283 | ? sprintf( 'Core update available: %s -> %s (%s).', $current_version, $latest_version, $update_type ) |
| 284 | : 'WordPress core is up to date.', |
| 285 | ]; |
| 286 | }, |
| 287 | 'permission_callback' => function() { |
| 288 | return current_user_can( 'update_core' ); |
| 289 | }, |
| 290 | 'meta' => [ |
| 291 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 292 | 'annotations' => [ |
| 293 | 'readonly' => false, |
| 294 | 'destructive' => false, |
| 295 | 'idempotent' => true, |
| 296 | ], |
| 297 | ], |
| 298 | ] ); |
| 299 | |
| 300 | // ---- update-core ---- |
| 301 | wp_register_ability( 'atarim/update-core', [ |
| 302 | 'label' => 'Update WordPress Core', |
| 303 | 'description' => 'Updates WordPress core to the latest available update, or to a specific version that WordPress.org is currently offering for this site. Only offered versions are accepted — arbitrary version strings and downgrades are rejected. Runs the database upgrade routine afterward so the site is not left needing a manual DB update. High impact: on constrained hosts the download/extract can exceed max_execution_time (the update may still complete server-side). Requires direct filesystem access (no FTP/SSH credential prompts are possible here).', |
| 304 | 'category' => 'atarim', |
| 305 | 'input_schema' => [ |
| 306 | 'type' => 'object', |
| 307 | 'properties' => [ |
| 308 | 'version' => [ |
| 309 | 'type' => 'string', |
| 310 | 'description' => 'Optional. Pin to a specific version WordPress is currently offering as an update (e.g. "6.5.2"). If omitted, updates to the latest offered update. Rejected if the version is not an offered update.', |
| 311 | ], |
| 312 | 'locale' => [ |
| 313 | 'type' => 'string', |
| 314 | 'description' => 'Optional WordPress locale for the update package (e.g. "en_US"). Defaults to the site locale.', |
| 315 | ], |
| 316 | ], |
| 317 | 'additionalProperties' => false, |
| 318 | ], |
| 319 | 'output_schema' => [ |
| 320 | 'type' => 'object', |
| 321 | 'properties' => [ |
| 322 | 'success' => [ 'type' => 'boolean' ], |
| 323 | 'updated' => [ 'type' => 'boolean' ], |
| 324 | 'from_version' => [ 'type' => 'string' ], |
| 325 | 'to_version' => [ 'type' => 'string' ], |
| 326 | 'update_type' => [ 'type' => 'string' ], |
| 327 | 'db_upgraded' => [ 'type' => 'boolean' ], |
| 328 | 'message' => [ 'type' => 'string' ], |
| 329 | ], |
| 330 | 'required' => [ 'success', 'updated', 'message' ], |
| 331 | ], |
| 332 | 'execute_callback' => function( $input = [] ) { |
| 333 | global $wp_version; |
| 334 | |
| 335 | $pre = $this->avcf_core_update_preflight(); |
| 336 | if ( $pre !== null ) { |
| 337 | return [ 'success' => false, 'updated' => false, 'message' => $pre ]; |
| 338 | } |
| 339 | |
| 340 | $locale = ( isset( $input['locale'] ) && $input['locale'] !== '' ) ? (string) $input['locale'] : get_locale(); |
| 341 | $from_version = $wp_version; |
| 342 | |
| 343 | // Act on current data. |
| 344 | wp_version_check( [], true ); |
| 345 | $updates = get_core_updates(); |
| 346 | if ( ! is_array( $updates ) || empty( $updates ) ) { |
| 347 | return [ |
| 348 | 'success' => true, |
| 349 | 'updated' => false, |
| 350 | 'from_version' => $from_version, |
| 351 | 'to_version' => $from_version, |
| 352 | 'update_type' => 'none', |
| 353 | 'db_upgraded' => false, |
| 354 | 'message' => 'No core updates are available; WordPress is up to date.', |
| 355 | ]; |
| 356 | } |
| 357 | |
| 358 | $want_version = isset( $input['version'] ) ? trim( (string) $input['version'] ) : ''; |
| 359 | $target = null; |
| 360 | foreach ( $updates as $update ) { |
| 361 | if ( ! isset( $update->response ) || $update->response !== 'upgrade' || ! isset( $update->current ) ) { |
| 362 | continue; |
| 363 | } |
| 364 | if ( $want_version !== '' ) { |
| 365 | if ( (string) $update->current === $want_version ) { |
| 366 | $target = $update; |
| 367 | break; |
| 368 | } |
| 369 | } else { |
| 370 | $target = $update; |
| 371 | break; |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | if ( $target === null ) { |
| 376 | if ( $want_version !== '' ) { |
| 377 | return [ |
| 378 | 'success' => false, |
| 379 | 'updated' => false, |
| 380 | 'from_version' => $from_version, |
| 381 | 'to_version' => $from_version, |
| 382 | 'message' => sprintf( 'Version "%s" is not currently offered as a core update for this site. Use check-core-updates to see what is available.', $want_version ), |
| 383 | ]; |
| 384 | } |
| 385 | return [ |
| 386 | 'success' => true, |
| 387 | 'updated' => false, |
| 388 | 'from_version' => $from_version, |
| 389 | 'to_version' => $from_version, |
| 390 | 'update_type' => 'none', |
| 391 | 'db_upgraded' => false, |
| 392 | 'message' => 'No core upgrade offer found; WordPress is up to date.', |
| 393 | ]; |
| 394 | } |
| 395 | |
| 396 | $to_version = (string) $target->current; |
| 397 | $update_type = $this->avcf_core_update_type( $from_version, $to_version ); |
| 398 | |
| 399 | // Prefer the requested locale's package if a different one is available. |
| 400 | if ( $locale !== '' && isset( $target->locale ) && $target->locale !== $locale ) { |
| 401 | $located = find_core_update( $to_version, $locale ); |
| 402 | if ( $located ) { |
| 403 | $target = $located; |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | $skin = new \WP_Ajax_Upgrader_Skin(); |
| 408 | $upgrader = new \Core_Upgrader( $skin ); |
| 409 | $result = $upgrader->upgrade( $target ); |
| 410 | |
| 411 | if ( is_wp_error( $result ) ) { |
| 412 | return [ |
| 413 | 'success' => false, |
| 414 | 'updated' => false, |
| 415 | 'from_version' => $from_version, |
| 416 | 'to_version' => $to_version, |
| 417 | 'update_type' => $update_type, |
| 418 | 'db_upgraded' => false, |
| 419 | 'message' => 'Core update failed: ' . $result->get_error_message(), |
| 420 | ]; |
| 421 | } |
| 422 | if ( $result === false ) { |
| 423 | return [ |
| 424 | 'success' => false, |
| 425 | 'updated' => false, |
| 426 | 'from_version' => $from_version, |
| 427 | 'to_version' => $to_version, |
| 428 | 'update_type' => $update_type, |
| 429 | 'db_upgraded' => false, |
| 430 | 'message' => 'Core update did not complete (the upgrader returned no result).', |
| 431 | ]; |
| 432 | } |
| 433 | |
| 434 | // Run the DB upgrade routine so the site is not left "needs database update". |
| 435 | $db_upgraded = false; |
| 436 | if ( ! function_exists( 'wp_upgrade' ) ) { |
| 437 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; |
| 438 | } |
| 439 | if ( function_exists( 'wp_upgrade' ) ) { |
| 440 | wp_upgrade(); |
| 441 | $db_upgraded = true; |
| 442 | } |
| 443 | |
| 444 | return [ |
| 445 | 'success' => true, |
| 446 | 'updated' => true, |
| 447 | 'from_version' => $from_version, |
| 448 | 'to_version' => $to_version, |
| 449 | 'update_type' => $update_type, |
| 450 | 'db_upgraded' => $db_upgraded, |
| 451 | 'message' => sprintf( 'WordPress updated from %s to %s (%s).', $from_version, $to_version, $update_type ), |
| 452 | ]; |
| 453 | }, |
| 454 | 'permission_callback' => function() { |
| 455 | return current_user_can( 'update_core' ); |
| 456 | }, |
| 457 | 'meta' => [ |
| 458 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 459 | 'annotations' => [ |
| 460 | 'readonly' => false, |
| 461 | 'destructive' => false, |
| 462 | 'idempotent' => false, |
| 463 | ], |
| 464 | ], |
| 465 | ] ); |
| 466 | |
| 467 | // ---- reinstall-core ---- |
| 468 | wp_register_ability( 'atarim/reinstall-core', [ |
| 469 | 'label' => 'Reinstall WordPress Core', |
| 470 | 'description' => 'Re-installs the currently running WordPress version to repair corrupted or modified core files (equivalent to the dashboard "Re-install now" button). Does not change the version. Requires direct filesystem access.', |
| 471 | 'category' => 'atarim', |
| 472 | 'input_schema' => [ |
| 473 | 'type' => 'object', |
| 474 | 'properties' => [ |
| 475 | 'locale' => [ |
| 476 | 'type' => 'string', |
| 477 | 'description' => 'Optional WordPress locale for the package (e.g. "en_US"). Defaults to the site locale.', |
| 478 | ], |
| 479 | ], |
| 480 | 'additionalProperties' => false, |
| 481 | ], |
| 482 | 'output_schema' => [ |
| 483 | 'type' => 'object', |
| 484 | 'properties' => [ |
| 485 | 'success' => [ 'type' => 'boolean' ], |
| 486 | 'reinstalled' => [ 'type' => 'boolean' ], |
| 487 | 'version' => [ 'type' => 'string' ], |
| 488 | 'message' => [ 'type' => 'string' ], |
| 489 | ], |
| 490 | 'required' => [ 'success', 'reinstalled', 'message' ], |
| 491 | ], |
| 492 | 'execute_callback' => function( $input = [] ) { |
| 493 | global $wp_version; |
| 494 | |
| 495 | $pre = $this->avcf_core_update_preflight(); |
| 496 | if ( $pre !== null ) { |
| 497 | return [ 'success' => false, 'reinstalled' => false, 'version' => $wp_version, 'message' => $pre ]; |
| 498 | } |
| 499 | |
| 500 | $locale = ( isset( $input['locale'] ) && $input['locale'] !== '' ) ? (string) $input['locale'] : get_locale(); |
| 501 | |
| 502 | // Refresh, then locate the package for the CURRENT version (reinstall). |
| 503 | wp_version_check( [], true ); |
| 504 | $update = find_core_update( $wp_version, $locale ); |
| 505 | if ( ! $update ) { |
| 506 | return [ |
| 507 | 'success' => false, |
| 508 | 'reinstalled' => false, |
| 509 | 'version' => $wp_version, |
| 510 | 'message' => sprintf( 'No reinstall package is available for the current version (%s, locale %s).', $wp_version, $locale ), |
| 511 | ]; |
| 512 | } |
| 513 | |
| 514 | $skin = new \WP_Ajax_Upgrader_Skin(); |
| 515 | $upgrader = new \Core_Upgrader( $skin ); |
| 516 | $result = $upgrader->upgrade( $update ); |
| 517 | |
| 518 | if ( is_wp_error( $result ) ) { |
| 519 | return [ |
| 520 | 'success' => false, |
| 521 | 'reinstalled' => false, |
| 522 | 'version' => $wp_version, |
| 523 | 'message' => 'Core reinstall failed: ' . $result->get_error_message(), |
| 524 | ]; |
| 525 | } |
| 526 | if ( $result === false ) { |
| 527 | return [ |
| 528 | 'success' => false, |
| 529 | 'reinstalled' => false, |
| 530 | 'version' => $wp_version, |
| 531 | 'message' => 'Core reinstall did not complete (the upgrader returned no result).', |
| 532 | ]; |
| 533 | } |
| 534 | |
| 535 | return [ |
| 536 | 'success' => true, |
| 537 | 'reinstalled' => true, |
| 538 | 'version' => $wp_version, |
| 539 | 'message' => sprintf( 'WordPress %s core files re-installed successfully.', $wp_version ), |
| 540 | ]; |
| 541 | }, |
| 542 | 'permission_callback' => function() { |
| 543 | return current_user_can( 'update_core' ); |
| 544 | }, |
| 545 | 'meta' => [ |
| 546 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 547 | 'annotations' => [ |
| 548 | 'readonly' => false, |
| 549 | 'destructive' => false, |
| 550 | 'idempotent' => true, |
| 551 | ], |
| 552 | ], |
| 553 | ] ); |
| 554 | } |
| 555 | |
| 556 | /** |
| 557 | * Shared safety preflight for core file operations (update / reinstall). |
| 558 | * Loads the upgrader includes and blocks when the environment cannot safely |
| 559 | * perform a core file operation. Returns null when OK, or an error string. |
| 560 | */ |
| 561 | private function avcf_core_update_preflight() { |
| 562 | if ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ) { |
| 563 | return 'File modifications are disabled on this site (DISALLOW_FILE_MODS), so core file operations are unavailable.'; |
| 564 | } |
| 565 | |
| 566 | if ( ! function_exists( 'get_filesystem_method' ) ) { |
| 567 | require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 568 | } |
| 569 | if ( function_exists( 'get_filesystem_method' ) && get_filesystem_method() !== 'direct' ) { |
| 570 | return 'The filesystem is not directly writable (FTP/SSH credentials would be required, which cannot be provided here). Core file operations are unavailable on this host configuration.'; |
| 571 | } |
| 572 | |
| 573 | if ( ! function_exists( 'get_core_updates' ) ) { |
| 574 | require_once ABSPATH . 'wp-admin/includes/update.php'; |
| 575 | } |
| 576 | if ( ! class_exists( '\Core_Upgrader' ) ) { |
| 577 | require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; |
| 578 | } |
| 579 | |
| 580 | // Give the download/extract more headroom where the host permits it. |
| 581 | @set_time_limit( 300 ); |
| 582 | |
| 583 | return null; |
| 584 | } |
| 585 | |
| 586 | /** |
| 587 | * Classify a version delta as major / minor / patch. |
| 588 | */ |
| 589 | private function avcf_core_update_type( $from, $to ) { |
| 590 | $f = explode( '.', (string) $from ); |
| 591 | $t = explode( '.', (string) $to ); |
| 592 | $f0 = isset( $f[0] ) ? $f[0] : ''; |
| 593 | $t0 = isset( $t[0] ) ? $t[0] : ''; |
| 594 | $f1 = isset( $f[1] ) ? $f[1] : ''; |
| 595 | $t1 = isset( $t[1] ) ? $t[1] : ''; |
| 596 | if ( $f0 !== $t0 ) { |
| 597 | return 'major'; |
| 598 | } |
| 599 | if ( $f1 !== $t1 ) { |
| 600 | return 'minor'; |
| 601 | } |
| 602 | return 'patch'; |
| 603 | } |
| 604 | } |