* @author Maximum Software Inc * @license GPL-3.0-only * @link https://agenistrator.com/ * * SPDX-FileCopyrightText: 2025-2026 Alex Ruban * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc * SPDX-License-Identifier: GPL-3.0-only */ namespace Agenistrator; // Direct access prevention if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Update_Plugins_Tool class. */ class Update_Plugins_Tool extends Tool { /** * Get the tool ID. * * @return string The tool ID. * @since 1.0.0 */ public function get_id() { return 'update_plugins'; } /** * Get the tool name. * * @return string The tool name. * @since 1.0.0 */ public function get_name() { return __( 'Update Plugins', 'agenistrator' ); } /** * Get the tool description. * * @return string The tool description. * @since 1.0.0 */ public function get_description() { return 'Update one or more plugins to their latest versions.'; } /** * Get additional description instructions for the LLM. * * @return string Additional instructions appended to description. * @since 1.0.0 */ public function get_description_instructions() { return 'Use `list_available_updates` to see which plugins have updates available.'; } /** * Get the parameters schema for this tool. * * @return array Parameters schema in JSON Schema format. * @since 1.0.0 */ public function get_parameters() { return array( 'type' => 'object', 'properties' => array( 'plugins' => array( 'type' => 'array', 'description' => 'Array of plugin identifiers to update. Each can be a plugin file path (e.g., "akismet/akismet.php") or slug (e.g., "akismet"). Use "all" as the only element to update all plugins with available updates.', 'items' => array( 'type' => 'string', ), ), ), 'required' => array( 'plugins' ), ); } /** * Check if the current user can execute this tool. * * @return bool True if the current user has permission. * @since 1.0.0 */ public function current_user_can_execute() { return current_user_can( 'update_plugins' ); } /** * Check if this tool is read-only. * * @return bool True if the tool only reads data. * @since 1.0.0 */ public function is_read_only() { return false; } /** * Execute the tool with the given arguments. * * @param array $args The tool arguments. * * @return array The tool execution result. * @since 1.0.0 */ protected function execute( $args ) { // Check if file modifications are disabled if ( ! wp_is_file_mod_allowed( 'plugin_install' ) ) { return $this->error_response( 'File modifications are disabled' ); } if ( ! isset( $args['plugins'] ) || ! is_array( $args['plugins'] ) || empty( $args['plugins'] ) ) { return $this->error_response( 'plugins parameter is required and must be a non-empty array' ); } if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; wp_update_plugins(); $update_plugins = get_site_transient( 'update_plugins' ); $available_updates = ! empty( $update_plugins->response ) ? $update_plugins->response : array(); if ( empty( $available_updates ) ) { return $this->success_response( 'No plugin updates available', array( 'updated' => array(), 'failed' => array(), ) ); } // Determine which plugins to update if ( count( $args['plugins'] ) === 1 && strtolower( $args['plugins'][0] ) === 'all' ) { $plugins_to_update = array_keys( $available_updates ); } else { $plugins_to_update = array(); foreach ( $args['plugins'] as $identifier ) { $found = $this->find_plugin_in_updates( $identifier, $available_updates ); if ( $found ) { $plugins_to_update[] = $found; } } } if ( empty( $plugins_to_update ) ) { return $this->error_response( 'No matching plugins found with available updates' ); } // Capture old versions before update $all_plugins = get_plugins(); $old_versions = array(); foreach ( $plugins_to_update as $file ) { $old_versions[ $file ] = $all_plugins[ $file ]['Version'] ?? 'unknown'; } // Perform bulk update $upgrader = new \Plugin_Upgrader( new \WP_Ajax_Upgrader_Skin() ); $bulk_results = $upgrader->bulk_upgrade( $plugins_to_update ); wp_clean_plugins_cache(); $all_plugins = get_plugins(); // Process results $results = array( 'updated' => array(), 'failed' => array(), ); foreach ( $plugins_to_update as $file ) { $update_info = $available_updates[ $file ]; $name = $update_info->slug ?? dirname( $file ); if ( isset( $bulk_results[ $file ] ) && false !== $bulk_results[ $file ] && ! is_wp_error( $bulk_results[ $file ] ) ) { $results['updated'][] = array( 'file' => $file, 'name' => $name, 'old_version' => $old_versions[ $file ], 'new_version' => $all_plugins[ $file ]['Version'] ?? $update_info->new_version, ); } else { $error = is_wp_error( $bulk_results[ $file ] ?? null ) ? $bulk_results[ $file ]->get_error_message() : 'Update failed'; $results['failed'][] = array( 'file' => $file, 'name' => $name, 'error' => $error, ); } } $message = String_Utils::replace_tags( __( 'Plugin update complete: {updated} updated, {failed} failed', 'agenistrator' ), array( 'updated' => count( $results['updated'] ), 'failed' => count( $results['failed'] ), ) ); return empty( $results['failed'] ) ? $this->success_response( $message, $results ) : $this->error_response( $message, $results ); } /** * Find a plugin in the available updates by various matching strategies. * * This method handles LLM input that could be in various formats: * - Full plugin basename (e.g., "akismet/akismet.php") * - Plugin slug from wordpress.org (e.g., "akismet") * - Plugin directory name (may differ from slug) * - Single-file plugin name (e.g., "hello" for "hello.php") * * @param string $identifier The plugin identifier from LLM input. * @param array $available_updates The update_plugins transient response array. * @return string|null The plugin basename if found, null otherwise. */ private function find_plugin_in_updates( $identifier, $available_updates ) { // Sanitize the identifier to get a clean slug for comparisons $input_slug = WordPress_Utils::get_plugin_slug( $identifier ); // Strategy 1: Direct match on plugin basename (e.g., "akismet/akismet.php") // This handles exact plugin file path input if ( isset( $available_updates[ $identifier ] ) ) { return $identifier; } // Iterate through available updates trying other matching strategies foreach ( $available_updates as $file => $info ) { // Strategy 2: Match against wordpress.org slug property // The slug may differ from directory name (e.g., slug "jetpack" vs directory "jetpack") if ( isset( $info->slug ) ) { $update_slug = sanitize_text_field( $info->slug ); if ( $update_slug === $input_slug ) { return $file; } } // Strategy 3: Match against directory portion of plugin basename // Extract slug from file path (e.g., "akismet/akismet.php" -> "akismet") $file_slug = WordPress_Utils::get_plugin_slug( $file ); if ( $file_slug === $input_slug ) { return $file; } // Strategy 4: Handle single-file plugins (no directory) // For plugins like "hello.php" in the plugins root directory // Input "hello" should match file "hello.php" if ( strpos( $file, '/' ) === false && $file === $input_slug . '.php' ) { return $file; } } return null; } }