[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Upgrade API: Plugin_Upgrader class 4 * 5 * @package WordPress 6 * @subpackage Upgrader 7 * @since 4.6.0 8 */ 9 10 /** 11 * Core class used for upgrading/installing plugins. 12 * 13 * It is designed to upgrade/install plugins from a local zip, remote zip URL, 14 * or uploaded zip file. 15 * 16 * @since 2.8.0 17 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php. 18 * 19 * @see WP_Upgrader 20 */ 21 class Plugin_Upgrader extends WP_Upgrader { 22 23 /** 24 * Plugin upgrade result. 25 * 26 * @since 2.8.0 27 * @var array|WP_Error $result 28 * 29 * @see WP_Upgrader::$result 30 */ 31 public $result; 32 33 /** 34 * Whether a bulk upgrade/installation is being performed. 35 * 36 * @since 2.9.0 37 * @var bool $bulk 38 */ 39 public $bulk = false; 40 41 /** 42 * New plugin info. 43 * 44 * @since 5.5.0 45 * @var array $new_plugin_data 46 * 47 * @see check_package() 48 */ 49 public $new_plugin_data = array(); 50 51 /** 52 * Initialize the upgrade strings. 53 * 54 * @since 2.8.0 55 */ 56 public function upgrade_strings() { 57 $this->strings['up_to_date'] = __( 'The plugin is at the latest version.' ); 58 $this->strings['no_package'] = __( 'Update package not available.' ); 59 /* translators: %s: Package URL. */ 60 $this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s…' ), '<span class="code">%s</span>' ); 61 $this->strings['unpack_package'] = __( 'Unpacking the update…' ); 62 $this->strings['remove_old'] = __( 'Removing the old version of the plugin…' ); 63 $this->strings['remove_old_failed'] = __( 'Could not remove the old plugin.' ); 64 $this->strings['process_failed'] = __( 'Plugin update failed.' ); 65 $this->strings['process_success'] = __( 'Plugin updated successfully.' ); 66 $this->strings['process_bulk_success'] = __( 'Plugins updated successfully.' ); 67 } 68 69 /** 70 * Initialize the installation strings. 71 * 72 * @since 2.8.0 73 */ 74 public function install_strings() { 75 $this->strings['no_package'] = __( 'Installation package not available.' ); 76 /* translators: %s: Package URL. */ 77 $this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s…' ), '<span class="code">%s</span>' ); 78 $this->strings['unpack_package'] = __( 'Unpacking the package…' ); 79 $this->strings['installing_package'] = __( 'Installing the plugin…' ); 80 $this->strings['remove_old'] = __( 'Removing the current plugin…' ); 81 $this->strings['remove_old_failed'] = __( 'Could not remove the current plugin.' ); 82 $this->strings['no_files'] = __( 'The plugin contains no files.' ); 83 $this->strings['process_failed'] = __( 'Plugin installation failed.' ); 84 $this->strings['process_success'] = __( 'Plugin installed successfully.' ); 85 /* translators: 1: Plugin name, 2: Plugin version. */ 86 $this->strings['process_success_specific'] = __( 'Successfully installed the plugin <strong>%1$s %2$s</strong>.' ); 87 88 if ( ! empty( $this->skin->overwrite ) ) { 89 if ( 'update-plugin' === $this->skin->overwrite ) { 90 $this->strings['installing_package'] = __( 'Updating the plugin…' ); 91 $this->strings['process_failed'] = __( 'Plugin update failed.' ); 92 $this->strings['process_success'] = __( 'Plugin updated successfully.' ); 93 } 94 95 if ( 'downgrade-plugin' === $this->skin->overwrite ) { 96 $this->strings['installing_package'] = __( 'Downgrading the plugin…' ); 97 $this->strings['process_failed'] = __( 'Plugin downgrade failed.' ); 98 $this->strings['process_success'] = __( 'Plugin downgraded successfully.' ); 99 } 100 } 101 } 102 103 /** 104 * Install a plugin package. 105 * 106 * @since 2.8.0 107 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. 108 * 109 * @param string $package The full local path or URI of the package. 110 * @param array $args { 111 * Optional. Other arguments for installing a plugin package. Default empty array. 112 * 113 * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. 114 * Default true. 115 * } 116 * @return bool|WP_Error True if the installation was successful, false or a WP_Error otherwise. 117 */ 118 public function install( $package, $args = array() ) { 119 $defaults = array( 120 'clear_update_cache' => true, 121 'overwrite_package' => false, // Do not overwrite files. 122 ); 123 $parsed_args = wp_parse_args( $args, $defaults ); 124 125 $this->init(); 126 $this->install_strings(); 127 128 add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); 129 130 if ( $parsed_args['clear_update_cache'] ) { 131 // Clear cache so wp_update_plugins() knows about the new plugin. 132 add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 ); 133 } 134 135 $this->run( 136 array( 137 'package' => $package, 138 'destination' => WP_PLUGIN_DIR, 139 'clear_destination' => $parsed_args['overwrite_package'], 140 'clear_working' => true, 141 'hook_extra' => array( 142 'type' => 'plugin', 143 'action' => 'install', 144 ), 145 ) 146 ); 147 148 remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 ); 149 remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); 150 151 if ( ! $this->result || is_wp_error( $this->result ) ) { 152 return $this->result; 153 } 154 155 // Force refresh of plugin update information. 156 wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); 157 158 if ( $parsed_args['overwrite_package'] ) { 159 /** 160 * Fires when the upgrader has successfully overwritten a currently installed 161 * plugin or theme with an uploaded zip package. 162 * 163 * @since 5.5.0 164 * 165 * @param string $package The package file. 166 * @param array $data The new plugin or theme data. 167 * @param string $package_type The package type ('plugin' or 'theme'). 168 */ 169 do_action( 'upgrader_overwrote_package', $package, $this->new_plugin_data, 'plugin' ); 170 } 171 172 return true; 173 } 174 175 /** 176 * Upgrade a plugin. 177 * 178 * @since 2.8.0 179 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. 180 * 181 * @param string $plugin Path to the plugin file relative to the plugins directory. 182 * @param array $args { 183 * Optional. Other arguments for upgrading a plugin package. Default empty array. 184 * 185 * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. 186 * Default true. 187 * } 188 * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise. 189 */ 190 public function upgrade( $plugin, $args = array() ) { 191 $defaults = array( 192 'clear_update_cache' => true, 193 ); 194 $parsed_args = wp_parse_args( $args, $defaults ); 195 196 $this->init(); 197 $this->upgrade_strings(); 198 199 $current = get_site_transient( 'update_plugins' ); 200 if ( ! isset( $current->response[ $plugin ] ) ) { 201 $this->skin->before(); 202 $this->skin->set_result( false ); 203 $this->skin->error( 'up_to_date' ); 204 $this->skin->after(); 205 return false; 206 } 207 208 // Get the URL to the zip file. 209 $r = $current->response[ $plugin ]; 210 211 add_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ), 10, 2 ); 212 add_filter( 'upgrader_pre_install', array( $this, 'active_before' ), 10, 2 ); 213 add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 ); 214 add_filter( 'upgrader_post_install', array( $this, 'active_after' ), 10, 2 ); 215 // There's a Trac ticket to move up the directory for zips which are made a bit differently, useful for non-.org plugins. 216 // 'source_selection' => array( $this, 'source_selection' ), 217 if ( $parsed_args['clear_update_cache'] ) { 218 // Clear cache so wp_update_plugins() knows about the new plugin. 219 add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 ); 220 } 221 222 $this->run( 223 array( 224 'package' => $r->package, 225 'destination' => WP_PLUGIN_DIR, 226 'clear_destination' => true, 227 'clear_working' => true, 228 'hook_extra' => array( 229 'plugin' => $plugin, 230 'type' => 'plugin', 231 'action' => 'update', 232 ), 233 ) 234 ); 235 236 // Cleanup our hooks, in case something else does a upgrade on this connection. 237 remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 ); 238 remove_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ) ); 239 remove_filter( 'upgrader_pre_install', array( $this, 'active_before' ) ); 240 remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) ); 241 remove_filter( 'upgrader_post_install', array( $this, 'active_after' ) ); 242 243 if ( ! $this->result || is_wp_error( $this->result ) ) { 244 return $this->result; 245 } 246 247 // Force refresh of plugin update information. 248 wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); 249 250 // Ensure any future auto-update failures trigger a failure email by removing 251 // the last failure notification from the list when plugins update successfully. 252 $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); 253 254 if ( isset( $past_failure_emails[ $plugin ] ) ) { 255 unset( $past_failure_emails[ $plugin ] ); 256 update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); 257 } 258 259 return true; 260 } 261 262 /** 263 * Bulk upgrade several plugins at once. 264 * 265 * @since 2.8.0 266 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. 267 * 268 * @param string[] $plugins Array of paths to plugin files relative to the plugins directory. 269 * @param array $args { 270 * Optional. Other arguments for upgrading several plugins at once. 271 * 272 * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. Default true. 273 * } 274 * @return array|false An array of results indexed by plugin file, or false if unable to connect to the filesystem. 275 */ 276 public function bulk_upgrade( $plugins, $args = array() ) { 277 $defaults = array( 278 'clear_update_cache' => true, 279 ); 280 $parsed_args = wp_parse_args( $args, $defaults ); 281 282 $this->init(); 283 $this->bulk = true; 284 $this->upgrade_strings(); 285 286 $current = get_site_transient( 'update_plugins' ); 287 288 add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 ); 289 290 $this->skin->header(); 291 292 // Connect to the filesystem first. 293 $res = $this->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) ); 294 if ( ! $res ) { 295 $this->skin->footer(); 296 return false; 297 } 298 299 $this->skin->bulk_header(); 300 301 /* 302 * Only start maintenance mode if: 303 * - running Multisite and there are one or more plugins specified, OR 304 * - a plugin with an update available is currently active. 305 * @todo For multisite, maintenance mode should only kick in for individual sites if at all possible. 306 */ 307 $maintenance = ( is_multisite() && ! empty( $plugins ) ); 308 foreach ( $plugins as $plugin ) { 309 $maintenance = $maintenance || ( is_plugin_active( $plugin ) && isset( $current->response[ $plugin ] ) ); 310 } 311 if ( $maintenance ) { 312 $this->maintenance_mode( true ); 313 } 314 315 $results = array(); 316 317 $this->update_count = count( $plugins ); 318 $this->update_current = 0; 319 foreach ( $plugins as $plugin ) { 320 $this->update_current++; 321 $this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true ); 322 323 if ( ! isset( $current->response[ $plugin ] ) ) { 324 $this->skin->set_result( 'up_to_date' ); 325 $this->skin->before(); 326 $this->skin->feedback( 'up_to_date' ); 327 $this->skin->after(); 328 $results[ $plugin ] = true; 329 continue; 330 } 331 332 // Get the URL to the zip file. 333 $r = $current->response[ $plugin ]; 334 335 $this->skin->plugin_active = is_plugin_active( $plugin ); 336 337 $result = $this->run( 338 array( 339 'package' => $r->package, 340 'destination' => WP_PLUGIN_DIR, 341 'clear_destination' => true, 342 'clear_working' => true, 343 'is_multi' => true, 344 'hook_extra' => array( 345 'plugin' => $plugin, 346 ), 347 ) 348 ); 349 350 $results[ $plugin ] = $result; 351 352 // Prevent credentials auth screen from displaying multiple times. 353 if ( false === $result ) { 354 break; 355 } 356 } // End foreach $plugins. 357 358 $this->maintenance_mode( false ); 359 360 // Force refresh of plugin update information. 361 wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); 362 363 /** This action is documented in wp-admin/includes/class-wp-upgrader.php */ 364 do_action( 365 'upgrader_process_complete', 366 $this, 367 array( 368 'action' => 'update', 369 'type' => 'plugin', 370 'bulk' => true, 371 'plugins' => $plugins, 372 ) 373 ); 374 375 $this->skin->bulk_footer(); 376 377 $this->skin->footer(); 378 379 // Cleanup our hooks, in case something else does a upgrade on this connection. 380 remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) ); 381 382 // Ensure any future auto-update failures trigger a failure email by removing 383 // the last failure notification from the list when plugins update successfully. 384 $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); 385 386 foreach ( $results as $plugin => $result ) { 387 // Maintain last failure notification when plugins failed to update manually. 388 if ( ! $result || is_wp_error( $result ) || ! isset( $past_failure_emails[ $plugin ] ) ) { 389 continue; 390 } 391 392 unset( $past_failure_emails[ $plugin ] ); 393 } 394 395 update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); 396 397 return $results; 398 } 399 400 /** 401 * Checks that the source package contains a valid plugin. 402 * 403 * Hooked to the {@see 'upgrader_source_selection'} filter by Plugin_Upgrader::install(). 404 * 405 * @since 3.3.0 406 * 407 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. 408 * @global string $wp_version The WordPress version string. 409 * 410 * @param string $source The path to the downloaded package source. 411 * @return string|WP_Error The source as passed, or a WP_Error object on failure. 412 */ 413 public function check_package( $source ) { 414 global $wp_filesystem, $wp_version; 415 416 $this->new_plugin_data = array(); 417 418 if ( is_wp_error( $source ) ) { 419 return $source; 420 } 421 422 $working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit( WP_CONTENT_DIR ), $source ); 423 if ( ! is_dir( $working_directory ) ) { // Sanity check, if the above fails, let's not prevent installation. 424 return $source; 425 } 426 427 // Check that the folder contains at least 1 valid plugin. 428 $files = glob( $working_directory . '*.php' ); 429 if ( $files ) { 430 foreach ( $files as $file ) { 431 $info = get_plugin_data( $file, false, false ); 432 if ( ! empty( $info['Name'] ) ) { 433 $this->new_plugin_data = $info; 434 break; 435 } 436 } 437 } 438 439 if ( empty( $this->new_plugin_data ) ) { 440 return new WP_Error( 'incompatible_archive_no_plugins', $this->strings['incompatible_archive'], __( 'No valid plugins were found.' ) ); 441 } 442 443 $requires_php = isset( $info['RequiresPHP'] ) ? $info['RequiresPHP'] : null; 444 $requires_wp = isset( $info['RequiresWP'] ) ? $info['RequiresWP'] : null; 445 446 if ( ! is_php_version_compatible( $requires_php ) ) { 447 $error = sprintf( 448 /* translators: 1: Current PHP version, 2: Version required by the uploaded plugin. */ 449 __( 'The PHP version on your server is %1$s, however the uploaded plugin requires %2$s.' ), 450 phpversion(), 451 $requires_php 452 ); 453 454 return new WP_Error( 'incompatible_php_required_version', $this->strings['incompatible_archive'], $error ); 455 } 456 457 if ( ! is_wp_version_compatible( $requires_wp ) ) { 458 $error = sprintf( 459 /* translators: 1: Current WordPress version, 2: Version required by the uploaded plugin. */ 460 __( 'Your WordPress version is %1$s, however the uploaded plugin requires %2$s.' ), 461 $wp_version, 462 $requires_wp 463 ); 464 465 return new WP_Error( 'incompatible_wp_required_version', $this->strings['incompatible_archive'], $error ); 466 } 467 468 return $source; 469 } 470 471 /** 472 * Retrieve the path to the file that contains the plugin info. 473 * 474 * This isn't used internally in the class, but is called by the skins. 475 * 476 * @since 2.8.0 477 * 478 * @return string|false The full path to the main plugin file, or false. 479 */ 480 public function plugin_info() { 481 if ( ! is_array( $this->result ) ) { 482 return false; 483 } 484 if ( empty( $this->result['destination_name'] ) ) { 485 return false; 486 } 487 488 // Ensure to pass with leading slash. 489 $plugin = get_plugins( '/' . $this->result['destination_name'] ); 490 if ( empty( $plugin ) ) { 491 return false; 492 } 493 494 // Assume the requested plugin is the first in the list. 495 $pluginfiles = array_keys( $plugin ); 496 497 return $this->result['destination_name'] . '/' . $pluginfiles[0]; 498 } 499 500 /** 501 * Deactivates a plugin before it is upgraded. 502 * 503 * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade(). 504 * 505 * @since 2.8.0 506 * @since 4.1.0 Added a return value. 507 * 508 * @param bool|WP_Error $response The installation response before the installation has started. 509 * @param array $plugin Plugin package arguments. 510 * @return bool|WP_Error The original `$response` parameter or WP_Error. 511 */ 512 public function deactivate_plugin_before_upgrade( $response, $plugin ) { 513 514 if ( is_wp_error( $response ) ) { // Bypass. 515 return $response; 516 } 517 518 // When in cron (background updates) don't deactivate the plugin, as we require a browser to reactivate it. 519 if ( wp_doing_cron() ) { 520 return $response; 521 } 522 523 $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; 524 if ( empty( $plugin ) ) { 525 return new WP_Error( 'bad_request', $this->strings['bad_request'] ); 526 } 527 528 if ( is_plugin_active( $plugin ) ) { 529 // Deactivate the plugin silently, Prevent deactivation hooks from running. 530 deactivate_plugins( $plugin, true ); 531 } 532 533 return $response; 534 } 535 536 /** 537 * Turns on maintenance mode before attempting to background update an active plugin. 538 * 539 * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade(). 540 * 541 * @since 5.4.0 542 * 543 * @param bool|WP_Error $response The installation response before the installation has started. 544 * @param array $plugin Plugin package arguments. 545 * @return bool|WP_Error The original `$response` parameter or WP_Error. 546 */ 547 public function active_before( $response, $plugin ) { 548 if ( is_wp_error( $response ) ) { 549 return $response; 550 } 551 552 // Only enable maintenance mode when in cron (background update). 553 if ( ! wp_doing_cron() ) { 554 return $response; 555 } 556 557 $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; 558 559 // Only run if plugin is active. 560 if ( ! is_plugin_active( $plugin ) ) { 561 return $response; 562 } 563 564 // Change to maintenance mode. Bulk edit handles this separately. 565 if ( ! $this->bulk ) { 566 $this->maintenance_mode( true ); 567 } 568 569 return $response; 570 } 571 572 /** 573 * Turns off maintenance mode after upgrading an active plugin. 574 * 575 * Hooked to the {@see 'upgrader_post_install'} filter by Plugin_Upgrader::upgrade(). 576 * 577 * @since 5.4.0 578 * 579 * @param bool|WP_Error $response The installation response after the installation has finished. 580 * @param array $plugin Plugin package arguments. 581 * @return bool|WP_Error The original `$response` parameter or WP_Error. 582 */ 583 public function active_after( $response, $plugin ) { 584 if ( is_wp_error( $response ) ) { 585 return $response; 586 } 587 588 // Only disable maintenance mode when in cron (background update). 589 if ( ! wp_doing_cron() ) { 590 return $response; 591 } 592 593 $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; 594 595 // Only run if plugin is active. 596 if ( ! is_plugin_active( $plugin ) ) { 597 return $response; 598 } 599 600 // Time to remove maintenance mode. Bulk edit handles this separately. 601 if ( ! $this->bulk ) { 602 $this->maintenance_mode( false ); 603 } 604 605 return $response; 606 } 607 608 /** 609 * Deletes the old plugin during an upgrade. 610 * 611 * Hooked to the {@see 'upgrader_clear_destination'} filter by 612 * Plugin_Upgrader::upgrade() and Plugin_Upgrader::bulk_upgrade(). 613 * 614 * @since 2.8.0 615 * 616 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. 617 * 618 * @param bool|WP_Error $removed Whether the destination was cleared. 619 * True on success, WP_Error on failure. 620 * @param string $local_destination The local package destination. 621 * @param string $remote_destination The remote package destination. 622 * @param array $plugin Extra arguments passed to hooked filters. 623 * @return bool|WP_Error 624 */ 625 public function delete_old_plugin( $removed, $local_destination, $remote_destination, $plugin ) { 626 global $wp_filesystem; 627 628 if ( is_wp_error( $removed ) ) { 629 return $removed; // Pass errors through. 630 } 631 632 $plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : ''; 633 if ( empty( $plugin ) ) { 634 return new WP_Error( 'bad_request', $this->strings['bad_request'] ); 635 } 636 637 $plugins_dir = $wp_filesystem->wp_plugins_dir(); 638 $this_plugin_dir = trailingslashit( dirname( $plugins_dir . $plugin ) ); 639 640 if ( ! $wp_filesystem->exists( $this_plugin_dir ) ) { // If it's already vanished. 641 return $removed; 642 } 643 644 // If plugin is in its own directory, recursively delete the directory. 645 // Base check on if plugin includes directory separator AND that it's not the root plugin folder. 646 if ( strpos( $plugin, '/' ) && $this_plugin_dir !== $plugins_dir ) { 647 $deleted = $wp_filesystem->delete( $this_plugin_dir, true ); 648 } else { 649 $deleted = $wp_filesystem->delete( $plugins_dir . $plugin ); 650 } 651 652 if ( ! $deleted ) { 653 return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] ); 654 } 655 656 return true; 657 } 658 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Dec 6 01:00:02 2024 | Cross-referenced by PHPXref 0.7.1 |