| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Upgrade API: WP_Automatic_Updater class 4 * 5 * @package WordPress 6 * @subpackage Upgrader 7 * @since 4.6.0 8 */ 9 10 /** 11 * Core class used for handling automatic background updates. 12 * 13 * @since 3.7.0 14 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php. 15 * 16 * @phpstan-type Core_Update_Offer object{ 17 * current: string, 18 * version: string, 19 * php_version: string, 20 * mysql_version: string, 21 * new_files?: bool|string, 22 * notify_email?: bool|string, 23 * autoupdate?: bool, 24 * disable_autoupdate?: bool, 25 * }&stdClass 26 * @phpstan-type Plugin_Update_Offer object{ 27 * slug: string, 28 * plugin: string, 29 * new_version: string, 30 * current_version?: string|false, 31 * requires_php?: string|false|null, 32 * autoupdate?: bool, 33 * disable_autoupdate?: bool, 34 * }&stdClass 35 * @phpstan-type Theme_Update_Offer object{ 36 * theme: string, 37 * new_version: string, 38 * current_version?: string|false, 39 * requires_php?: string|false|null, 40 * autoupdate?: bool, 41 * disable_autoupdate?: bool, 42 * }&stdClass 43 * @phpstan-type Translation_Update_Offer object{ 44 * language: string, 45 * autoupdate?: bool, 46 * disable_autoupdate?: bool, 47 * }&stdClass 48 * @phpstan-type Core_Update_Result object{ 49 * item: Core_Update_Offer, 50 * result: string|WP_Error, 51 * name: string, 52 * messages: string[], 53 * }&stdClass 54 * @phpstan-type Plugin_Update_Result object{ 55 * item: Plugin_Update_Offer, 56 * result: true|WP_Error|null, 57 * name: string, 58 * messages: string[], 59 * }&stdClass 60 * @phpstan-type Theme_Update_Result object{ 61 * item: Theme_Update_Offer, 62 * result: true|WP_Error|null, 63 * name: string|false, 64 * messages: string[], 65 * }&stdClass 66 * @phpstan-type Translation_Update_Result object{ 67 * item: Translation_Update_Offer, 68 * result: array<mixed>|true|WP_Error|null, 69 * name: string, 70 * messages: string[], 71 * }&stdClass 72 */ 73 #[AllowDynamicProperties] 74 class WP_Automatic_Updater { 75 76 /** 77 * Tracks update results during processing. 78 * 79 * @var array<string, object[]> 80 * @phpstan-var array{ 81 * core?: list<Core_Update_Result>, 82 * plugin?: list<Plugin_Update_Result>, 83 * theme?: list<Theme_Update_Result>, 84 * translation?: list<Translation_Update_Result>, 85 * } 86 */ 87 protected $update_results = array(); 88 89 /** 90 * Determines whether the entire automatic updater is disabled. 91 * 92 * @since 3.7.0 93 * 94 * @return bool True if the automatic updater is disabled, false otherwise. 95 */ 96 public function is_disabled() { 97 // Background updates are disabled if you don't want file changes. 98 if ( ! wp_is_file_mod_allowed( 'automatic_updater' ) ) { 99 return true; 100 } 101 102 if ( wp_installing() ) { 103 return true; 104 } 105 106 // More fine grained control can be done through the WP_AUTO_UPDATE_CORE constant and filters. 107 $disabled = defined( 'AUTOMATIC_UPDATER_DISABLED' ) && AUTOMATIC_UPDATER_DISABLED; 108 109 /** 110 * Filters whether to entirely disable background updates. 111 * 112 * There are more fine-grained filters and controls for selective disabling. 113 * This filter parallels the AUTOMATIC_UPDATER_DISABLED constant in name. 114 * 115 * This also disables update notification emails. That may change in the future. 116 * 117 * @since 3.7.0 118 * 119 * @param bool $disabled Whether the updater should be disabled. 120 */ 121 return apply_filters( 'automatic_updater_disabled', $disabled ); 122 } 123 124 /** 125 * Checks whether access to a given directory is allowed. 126 * 127 * This is used when detecting version control checkouts. Takes into account 128 * the PHP `open_basedir` restrictions, so that WordPress does not try to access 129 * directories it is not allowed to. 130 * 131 * @since 6.2.0 132 * 133 * @param string $dir The directory to check. 134 * @return bool True if access to the directory is allowed, false otherwise. 135 */ 136 public function is_allowed_dir( $dir ) { 137 if ( is_string( $dir ) ) { 138 $dir = trim( $dir ); 139 } 140 141 if ( ! is_string( $dir ) || '' === $dir ) { 142 _doing_it_wrong( 143 __METHOD__, 144 sprintf( 145 /* translators: %s: The "$dir" argument. */ 146 __( 'The "%s" argument must be a non-empty string.' ), 147 '$dir' 148 ), 149 '6.2.0' 150 ); 151 152 return false; 153 } 154 155 $open_basedir = ini_get( 'open_basedir' ); 156 157 if ( empty( $open_basedir ) ) { 158 return true; 159 } 160 161 $open_basedir_list = explode( PATH_SEPARATOR, $open_basedir ); 162 163 foreach ( $open_basedir_list as $basedir ) { 164 if ( '' !== trim( $basedir ) && str_starts_with( $dir, $basedir ) ) { 165 return true; 166 } 167 } 168 169 return false; 170 } 171 172 /** 173 * Checks for version control checkouts. 174 * 175 * Checks for Subversion, Git, Mercurial, and Bazaar. It recursively looks up the 176 * filesystem to the top of the drive, erring on the side of detecting a VCS 177 * checkout somewhere. 178 * 179 * ABSPATH is always checked in addition to whatever `$context` is (which may be the 180 * wp-content directory, for example). The underlying assumption is that if you are 181 * using version control *anywhere*, then you should be making decisions for 182 * how things get updated. 183 * 184 * @since 3.7.0 185 * 186 * @param string $context The filesystem path to check, in addition to ABSPATH. 187 * @return bool True if a VCS checkout was discovered at `$context` or ABSPATH, 188 * or anywhere higher. False otherwise. 189 */ 190 public function is_vcs_checkout( $context ) { 191 $context_dirs = array( untrailingslashit( $context ) ); 192 if ( ABSPATH !== $context ) { 193 $context_dirs[] = untrailingslashit( ABSPATH ); 194 } 195 196 $vcs_dirs = array( '.svn', '.git', '.hg', '.bzr' ); 197 $check_dirs = array(); 198 199 foreach ( $context_dirs as $context_dir ) { 200 // Walk up from $context_dir to the root. 201 do { 202 $check_dirs[] = $context_dir; 203 204 // Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here. 205 if ( dirname( $context_dir ) === $context_dir ) { 206 break; 207 } 208 209 // Continue one level at a time. 210 } while ( $context_dir = dirname( $context_dir ) ); 211 } 212 213 $check_dirs = array_unique( $check_dirs ); 214 $checkout = false; 215 216 // Search all directories we've found for evidence of version control. 217 foreach ( $vcs_dirs as $vcs_dir ) { 218 foreach ( $check_dirs as $check_dir ) { 219 if ( ! $this->is_allowed_dir( $check_dir ) ) { 220 continue; 221 } 222 223 $checkout = is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" ); 224 if ( $checkout ) { 225 break 2; 226 } 227 } 228 } 229 230 /** 231 * Filters whether the automatic updater should consider a filesystem 232 * location to be potentially managed by a version control system. 233 * 234 * @since 3.7.0 235 * 236 * @param bool $checkout Whether a VCS checkout was discovered at `$context` 237 * or ABSPATH, or anywhere higher. 238 * @param string $context The filesystem context (a path) against which 239 * filesystem status should be checked. 240 */ 241 return apply_filters( 'automatic_updates_is_vcs_checkout', $checkout, $context ); 242 } 243 244 /** 245 * Tests to see if we can and should update a specific item. 246 * 247 * @since 3.7.0 248 * 249 * @global wpdb $wpdb WordPress database abstraction object. 250 * 251 * @param string $type The type of update being checked: 'core', 'theme', 252 * 'plugin', 'translation'. 253 * @param object $item The update offer. 254 * @param string $context The filesystem context (a path) against which filesystem 255 * access and status should be checked. 256 * @return bool True if the item should be updated, false otherwise. 257 * 258 * @phpstan-param 'core'|'theme'|'plugin'|'translation' $type 259 * @phpstan-param (Core_Update_Offer|Plugin_Update_Offer|Theme_Update_Offer|Translation_Update_Offer) $item 260 */ 261 public function should_update( $type, $item, $context ) { 262 // Used to see if WP_Filesystem is set up to allow unattended updates. 263 $skin = new Automatic_Upgrader_Skin(); 264 265 if ( $this->is_disabled() ) { 266 return false; 267 } 268 269 // Only relax the filesystem checks when the update doesn't include new files. 270 $allow_relaxed_file_ownership = false; 271 if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) { 272 $allow_relaxed_file_ownership = true; 273 } 274 275 // If we can't do an auto core update, we may still be able to email the user. 276 if ( ! $skin->request_filesystem_credentials( false, $context, $allow_relaxed_file_ownership ) 277 || $this->is_vcs_checkout( $context ) 278 ) { 279 if ( 'core' === $type ) { 280 $this->send_core_update_notification_email( $item ); 281 } 282 return false; 283 } 284 285 /* 286 * Next up, is this an item we can update? 287 * 288 * The inline @phpstan-var tags narrow $item by $type. This should be replaced with a conditional 289 * parameter type once PHPStan supports narrowing nested conditional parameter types. 290 */ 291 if ( 'core' === $type ) { 292 /** @phpstan-var Core_Update_Offer $item */ 293 $update = Core_Upgrader::should_update_to_version( $item->current ); 294 } elseif ( 'plugin' === $type || 'theme' === $type ) { 295 /** @phpstan-var (Plugin_Update_Offer|Theme_Update_Offer) $item */ 296 $update = ! empty( $item->autoupdate ); 297 298 if ( ! $update && wp_is_auto_update_enabled_for_type( $type ) ) { 299 // Check if the site admin has enabled auto-updates by default for the specific item. 300 $auto_updates = (array) get_site_option( "auto_update_{$type}s", array() ); 301 $update = in_array( $item->{$type}, $auto_updates, true ); 302 } 303 } else { 304 $update = ! empty( $item->autoupdate ); 305 } 306 307 // If the `disable_autoupdate` flag is set, override any user-choice, but allow filters. 308 if ( ! empty( $item->disable_autoupdate ) ) { 309 $update = false; 310 } 311 312 /** 313 * Filters whether to automatically update core, a plugin, a theme, or a language. 314 * 315 * The dynamic portion of the hook name, `$type`, refers to the type of update 316 * being checked. 317 * 318 * Possible hook names include: 319 * 320 * - `auto_update_core` 321 * - `auto_update_plugin` 322 * - `auto_update_theme` 323 * - `auto_update_translation` 324 * 325 * Since WordPress 3.7, minor and development versions of core, and translations have 326 * been auto-updated by default. New installs on WordPress 5.6 or higher will also 327 * auto-update major versions by default. Starting in 5.6, older sites can opt-in to 328 * major version auto-updates, and auto-updates for plugins and themes. 329 * 330 * See the {@see 'allow_dev_auto_core_updates'}, {@see 'allow_minor_auto_core_updates'}, 331 * and {@see 'allow_major_auto_core_updates'} filters for a more straightforward way to 332 * adjust core updates. 333 * 334 * @since 3.7.0 335 * @since 5.5.0 The `$update` parameter accepts the value of null. 336 * 337 * @param bool|null $update Whether to update. The value of null is internally used 338 * to detect whether nothing has hooked into this filter. 339 * @param object $item The update offer. 340 */ 341 $update = apply_filters( "auto_update_{$type}", $update, $item ); 342 343 if ( ! $update ) { 344 if ( 'core' === $type ) { 345 $this->send_core_update_notification_email( $item ); 346 } 347 return false; 348 } 349 350 // If it's a core update, are we actually compatible with its requirements? 351 if ( 'core' === $type ) { 352 global $wpdb; 353 354 /** @phpstan-var Core_Update_Offer $item */ 355 $php_compat = version_compare( PHP_VERSION, $item->php_version, '>=' ); 356 if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && empty( $wpdb->is_mysql ) ) { 357 $mysql_compat = true; 358 } else { 359 $mysql_compat = version_compare( $wpdb->db_version(), $item->mysql_version, '>=' ); 360 } 361 362 if ( ! $php_compat || ! $mysql_compat ) { 363 return false; 364 } 365 } 366 367 // If updating a plugin or theme, ensure the minimum PHP version requirements are satisfied. 368 if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) { 369 /** @phpstan-var (Plugin_Update_Offer|Theme_Update_Offer) $item */ 370 if ( ! empty( $item->requires_php ) && version_compare( PHP_VERSION, $item->requires_php, '<' ) ) { 371 return false; 372 } 373 } 374 375 return true; 376 } 377 378 /** 379 * Notifies an administrator of a core update. 380 * 381 * @since 3.7.0 382 * 383 * @param object $item The update offer. 384 * @return bool True if the site administrator is notified of a core update, 385 * false otherwise. 386 */ 387 protected function send_core_update_notification_email( $item ) { 388 $notified = get_site_option( 'auto_core_update_notified' ); 389 390 // Don't notify if we've already notified the same email address of the same version. 391 if ( $notified 392 && get_site_option( 'admin_email' ) === $notified['email'] 393 && $notified['version'] === $item->current 394 ) { 395 return false; 396 } 397 398 // See if we need to notify users of a core update. 399 $notify = ! empty( $item->notify_email ); 400 401 /** 402 * Filters whether to notify the site administrator of a new core update. 403 * 404 * By default, administrators are notified when the update offer received 405 * from WordPress.org sets a particular flag. This allows some discretion 406 * in if and when to notify. 407 * 408 * This filter is only evaluated once per release. If the same email address 409 * was already notified of the same new version, WordPress won't repeatedly 410 * email the administrator. 411 * 412 * This filter is also used on about.php to check if a plugin has disabled 413 * these notifications. 414 * 415 * @since 3.7.0 416 * 417 * @param bool $notify Whether the site administrator is notified. 418 * @param object $item The update offer. 419 */ 420 if ( ! apply_filters( 'send_core_update_notification_email', $notify, $item ) ) { 421 return false; 422 } 423 424 $this->send_email( 'manual', $item ); 425 return true; 426 } 427 428 /** 429 * Updates an item, if appropriate. 430 * 431 * @since 3.7.0 432 * 433 * @param string $type The type of update being checked: 'core', 'theme', 'plugin', 'translation'. 434 * @param object $item The update offer. 435 * @return WP_Error|bool|string|array|null False if the item should not be updated, otherwise the result 436 * from the upgrader: the new WordPress version for core, true for 437 * a successful plugin or theme update, the installation result for 438 * a translation, or a WP_Error on failure. Null for a plugin, theme, 439 * or translation if the update failed before the package was 440 * installed. 441 * 442 * @phpstan-param 'core'|'theme'|'plugin'|'translation' $type 443 * @phpstan-param (Core_Update_Offer|Plugin_Update_Offer|Theme_Update_Offer|Translation_Update_Offer) $item 444 * @phpstan-return ( 445 * $type is 'core' ? string|false|WP_Error : ( 446 * $type is 'translation' ? array<mixed>|bool|WP_Error|null : bool|WP_Error|null 447 * ) 448 * ) 449 */ 450 public function update( $type, $item ) { 451 $skin = new Automatic_Upgrader_Skin(); 452 453 /* 454 * The inline @phpstan-var tags narrow $item by $type. This should be replaced with a conditional 455 * parameter type once PHPStan supports narrowing nested conditional parameter types. 456 */ 457 $upgrader = null; 458 $context = null; 459 switch ( $type ) { 460 case 'core': 461 /** @phpstan-var Core_Update_Offer $item */ 462 // The Core upgrader doesn't use the Upgrader's skin during the actual main part of the upgrade, instead, firing a filter. 463 add_filter( 'update_feedback', array( $skin, 'feedback' ) ); 464 $upgrader = new Core_Upgrader( $skin ); 465 $context = ABSPATH; 466 break; 467 case 'plugin': 468 /** @phpstan-var Plugin_Update_Offer $item */ 469 $upgrader = new Plugin_Upgrader( $skin ); 470 $context = WP_PLUGIN_DIR; // We don't support custom Plugin directories, or updates for WPMU_PLUGIN_DIR. 471 break; 472 case 'theme': 473 /** @phpstan-var Theme_Update_Offer $item */ 474 $upgrader = new Theme_Upgrader( $skin ); 475 $context = get_theme_root( $item->theme ); 476 break; 477 case 'translation': 478 /** @phpstan-var Translation_Update_Offer $item */ 479 $upgrader = new Language_Pack_Upgrader( $skin ); 480 $context = WP_CONTENT_DIR; // WP_LANG_DIR; 481 break; 482 } 483 484 // Determine whether we can and should perform this update. 485 if ( ! $this->should_update( $type, $item, $context ) ) { 486 return false; 487 } 488 489 /** 490 * Fires immediately prior to an auto-update. 491 * 492 * @since 4.4.0 493 * 494 * @param string $type The type of update being checked: 'core', 'theme', 'plugin', or 'translation'. 495 * @param object $item The update offer. 496 * @param string $context The filesystem context (a path) against which filesystem access and status 497 * should be checked. 498 */ 499 do_action( 'pre_auto_update', $type, $item, $context ); 500 501 $item_name = null; 502 $upgrader_item = $item; 503 switch ( $type ) { 504 case 'core': 505 /** @phpstan-var Core_Update_Offer $item */ 506 /* translators: %s: WordPress version. */ 507 $skin->feedback( __( 'Updating to WordPress %s' ), $item->version ); 508 /* translators: %s: WordPress version. */ 509 $item_name = sprintf( __( 'WordPress %s' ), $item->version ); 510 break; 511 case 'theme': 512 /** @phpstan-var Theme_Update_Offer $item */ 513 $upgrader_item = $item->theme; 514 $theme = wp_get_theme( $upgrader_item ); 515 $item_name = $theme->get( 'Name' ); 516 // Add the current version so that it can be reported in the notification email. 517 $item->current_version = $theme->get( 'Version' ); 518 if ( empty( $item->current_version ) ) { 519 $item->current_version = false; 520 } 521 /* translators: %s: Theme name. */ 522 $skin->feedback( __( 'Updating theme: %s' ), $item_name ); 523 break; 524 case 'plugin': 525 /** @phpstan-var Plugin_Update_Offer $item */ 526 $upgrader_item = $item->plugin; 527 $plugin_data = get_plugin_data( $context . '/' . $upgrader_item ); 528 $item_name = $plugin_data['Name']; 529 // Add the current version so that it can be reported in the notification email. 530 $item->current_version = $plugin_data['Version']; 531 if ( empty( $item->current_version ) ) { 532 $item->current_version = false; 533 } 534 /* translators: %s: Plugin name. */ 535 $skin->feedback( __( 'Updating plugin: %s' ), $item_name ); 536 break; 537 case 'translation': 538 /** @phpstan-var Translation_Update_Offer $item */ 539 /** @var Language_Pack_Upgrader $upgrader */ 540 $language_item_name = $upgrader->get_name_for_update( $item ); 541 /* translators: %s: Project name (plugin, theme, or WordPress). */ 542 $item_name = sprintf( __( 'Translations for %s' ), $language_item_name ); 543 /* translators: 1: Project name (plugin, theme, or WordPress), 2: Language. */ 544 $skin->feedback( sprintf( __( 'Updating translations for %1$s (%2$s)…' ), $language_item_name, $item->language ) ); 545 break; 546 } 547 548 $allow_relaxed_file_ownership = false; 549 if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) { 550 $allow_relaxed_file_ownership = true; 551 } 552 553 $was_active = null; 554 $is_debug = WP_DEBUG && WP_DEBUG_LOG; 555 if ( 'plugin' === $type ) { 556 /** @phpstan-var Plugin_Update_Offer $item */ 557 /** @var string $upgrader_item */ 558 $was_active = is_plugin_active( $upgrader_item ); 559 if ( $is_debug ) { 560 error_log( ' Upgrading plugin ' . var_export( $item->slug, true ) . '...' ); 561 } 562 } 563 564 if ( 'theme' === $type && $is_debug ) { 565 /** @phpstan-var Theme_Update_Offer $item */ 566 error_log( ' Upgrading theme ' . var_export( $item->theme, true ) . '...' ); 567 } 568 569 /* 570 * Enable maintenance mode before upgrading the plugin or theme. 571 * 572 * This avoids potential non-fatal errors being detected 573 * while scraping for a fatal error if some files are still 574 * being moved. 575 * 576 * While these checks are intended only for plugins, 577 * maintenance mode is enabled for all upgrade types as any 578 * update could contain an error or warning, which could cause 579 * the scrape to miss a fatal error in the plugin update. 580 */ 581 if ( 'translation' !== $type ) { 582 $upgrader->maintenance_mode( true ); 583 } 584 585 // Boom, this site's about to get a whole new splash of paint! 586 $upgrade_result = $upgrader->upgrade( 587 $upgrader_item, 588 array( 589 'clear_update_cache' => false, 590 // Always use partial builds if possible for core updates. 591 'pre_check_md5' => false, 592 // Only available for core updates. 593 'attempt_rollback' => true, 594 // Allow relaxed file ownership in some scenarios. 595 'allow_relaxed_file_ownership' => $allow_relaxed_file_ownership, 596 ) 597 ); 598 599 /* 600 * After WP_Upgrader::upgrade() completes, maintenance mode is disabled. 601 * 602 * Re-enable maintenance mode while attempting to detect fatal errors 603 * and potentially rolling back. 604 * 605 * This avoids errors if the site is visited while fatal errors exist 606 * or while files are still being moved. 607 */ 608 if ( 'translation' !== $type ) { 609 $upgrader->maintenance_mode( true ); 610 } 611 612 // If the filesystem is unavailable, false is returned. 613 if ( false === $upgrade_result ) { 614 $upgrade_result = new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) ); 615 } 616 617 if ( 'core' === $type ) { 618 if ( is_wp_error( $upgrade_result ) 619 && ( 'up_to_date' === $upgrade_result->get_error_code() 620 || 'locked' === $upgrade_result->get_error_code() ) 621 ) { 622 // Allow visitors to browse the site again. 623 $upgrader->maintenance_mode( false ); 624 625 /* 626 * These aren't actual errors, treat it as a skipped-update instead 627 * to avoid triggering the post-core update failure routines. 628 */ 629 return false; 630 } 631 632 // Core doesn't output this, so let's append it, so we don't get confused. 633 if ( is_wp_error( $upgrade_result ) ) { 634 $upgrade_result->add( 'installation_failed', __( 'Installation failed.' ) ); 635 $skin->error( $upgrade_result ); 636 } else { 637 $skin->feedback( __( 'WordPress updated successfully.' ) ); 638 } 639 } 640 641 if ( 'theme' === $type && $is_debug ) { 642 /** @phpstan-var Theme_Update_Offer $item */ 643 error_log( ' Theme ' . var_export( $item->theme, true ) . ' has been upgraded.' ); 644 } 645 646 if ( 'plugin' === $type ) { 647 /** @phpstan-var Plugin_Update_Offer $item */ 648 /** @var string $upgrader_item */ 649 if ( $is_debug ) { 650 error_log( ' Plugin ' . var_export( $item->slug, true ) . ' has been upgraded.' ); 651 if ( is_plugin_inactive( $upgrader_item ) ) { 652 error_log( ' ' . var_export( $upgrader_item, true ) . ' is inactive and will not be checked for fatal errors.' ); 653 } 654 } 655 656 if ( $was_active && ! is_wp_error( $upgrade_result ) ) { 657 658 /* 659 * The usual time limit is five minutes. However, as a loopback request 660 * is about to be performed, increase the time limit to account for this. 661 */ 662 if ( function_exists( 'set_time_limit' ) ) { 663 set_time_limit( 10 * MINUTE_IN_SECONDS ); 664 } 665 666 /* 667 * Avoids a race condition when there are 2 sequential plugins that have 668 * fatal errors. It seems a slight delay is required for the loopback to 669 * use the updated plugin code in the request. This can cause the second 670 * plugin's fatal error checking to be inaccurate, and may also affect 671 * subsequent plugin checks. 672 */ 673 sleep( 2 ); 674 675 if ( $this->has_fatal_error() ) { 676 $upgrade_result = new WP_Error(); 677 $temp_backup = array( 678 array( 679 'dir' => 'plugins', 680 'slug' => $item->slug, 681 'src' => WP_PLUGIN_DIR, 682 ), 683 ); 684 685 $backup_restored = $upgrader->restore_temp_backup( $temp_backup ); 686 if ( is_wp_error( $backup_restored ) ) { 687 $upgrade_result->add( 688 'plugin_update_fatal_error_rollback_failed', 689 sprintf( 690 /* translators: %s: The plugin's slug. */ 691 __( "The update for '%s' contained a fatal error. The previously installed version could not be restored." ), 692 $item->slug 693 ) 694 ); 695 696 $upgrade_result->merge_from( $backup_restored ); 697 } else { 698 $upgrade_result->add( 699 'plugin_update_fatal_error_rollback_successful', 700 sprintf( 701 /* translators: %s: The plugin's slug. */ 702 __( "The update for '%s' contained a fatal error. The previously installed version has been restored." ), 703 $item->slug 704 ) 705 ); 706 707 $backup_deleted = $upgrader->delete_temp_backup( $temp_backup ); 708 if ( is_wp_error( $backup_deleted ) ) { 709 $upgrade_result->merge_from( $backup_deleted ); 710 } 711 } 712 713 /* 714 * Should emails not be working, log the message(s) so that 715 * the log file contains context for the fatal error, 716 * and whether a rollback was performed. 717 * 718 * `trigger_error()` is not used as it outputs a stack trace 719 * to this location rather than to the fatal error, which will 720 * appear above this entry in the log file. 721 */ 722 if ( $is_debug ) { 723 error_log( ' ' . implode( "\n", $upgrade_result->get_error_messages() ) ); 724 } 725 } elseif ( $is_debug ) { 726 error_log( ' The update for ' . var_export( $item->slug, true ) . ' has no fatal errors.' ); 727 } 728 } 729 } 730 731 // All processes are complete. Allow visitors to browse the site again. 732 if ( 'translation' !== $type ) { 733 $upgrader->maintenance_mode( false ); 734 } 735 736 $this->update_results[ $type ][] = (object) array( 737 'item' => $item, 738 'result' => $upgrade_result, 739 'name' => $item_name, 740 'messages' => $skin->get_upgrade_messages(), 741 ); 742 743 return $upgrade_result; 744 } 745 746 /** 747 * Kicks off the background update process, looping through all pending updates. 748 * 749 * @since 3.7.0 750 */ 751 public function run() { 752 if ( $this->is_disabled() ) { 753 return; 754 } 755 756 if ( ! is_main_network() || ! is_main_site() ) { 757 return; 758 } 759 760 if ( ! WP_Upgrader::create_lock( 'auto_updater' ) ) { 761 return; 762 } 763 764 $is_debug = WP_DEBUG && WP_DEBUG_LOG; 765 766 if ( $is_debug ) { 767 error_log( 'Automatic updates starting...' ); 768 } 769 770 // Don't automatically run these things, as we'll handle it ourselves. 771 remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 ); 772 remove_action( 'upgrader_process_complete', 'wp_version_check' ); 773 remove_action( 'upgrader_process_complete', 'wp_update_plugins' ); 774 remove_action( 'upgrader_process_complete', 'wp_update_themes' ); 775 776 // Next, plugins. 777 wp_update_plugins(); // Check for plugin updates. 778 $plugin_updates = get_site_transient( 'update_plugins' ); 779 if ( $plugin_updates && ! empty( $plugin_updates->response ) ) { 780 if ( $is_debug ) { 781 error_log( ' Automatic plugin updates starting...' ); 782 } 783 784 foreach ( $plugin_updates->response as $plugin ) { 785 $this->update( 'plugin', $plugin ); 786 } 787 788 // Force refresh of plugin update information. 789 wp_clean_plugins_cache(); 790 791 if ( $is_debug ) { 792 error_log( ' Automatic plugin updates complete.' ); 793 } 794 } 795 796 // Next, those themes we all love. 797 wp_update_themes(); // Check for theme updates. 798 $theme_updates = get_site_transient( 'update_themes' ); 799 if ( $theme_updates && ! empty( $theme_updates->response ) ) { 800 if ( $is_debug ) { 801 error_log( ' Automatic theme updates starting...' ); 802 } 803 804 foreach ( $theme_updates->response as $theme ) { 805 $this->update( 'theme', (object) $theme ); 806 } 807 // Force refresh of theme update information. 808 wp_clean_themes_cache(); 809 810 if ( $is_debug ) { 811 error_log( ' Automatic theme updates complete.' ); 812 } 813 } 814 815 if ( $is_debug ) { 816 error_log( 'Automatic updates complete.' ); 817 } 818 819 // Next, process any core update. 820 wp_version_check(); // Check for core updates. 821 $core_update = find_core_auto_update(); 822 823 if ( $core_update ) { 824 $this->update( 'core', $core_update ); 825 } 826 827 /* 828 * Clean up, and check for any pending translations. 829 * (Core_Upgrader checks for core updates.) 830 */ 831 $theme_stats = array(); 832 if ( isset( $this->update_results['theme'] ) ) { 833 foreach ( $this->update_results['theme'] as $upgrade ) { 834 $theme_stats[ $upgrade->item->theme ] = ( true === $upgrade->result ); 835 } 836 } 837 wp_update_themes( $theme_stats ); // Check for theme updates. 838 839 $plugin_stats = array(); 840 if ( isset( $this->update_results['plugin'] ) ) { 841 foreach ( $this->update_results['plugin'] as $upgrade ) { 842 $plugin_stats[ $upgrade->item->plugin ] = ( true === $upgrade->result ); 843 } 844 } 845 wp_update_plugins( $plugin_stats ); // Check for plugin updates. 846 847 // Finally, process any new translations. 848 $language_updates = wp_get_translation_updates(); 849 if ( $language_updates ) { 850 foreach ( $language_updates as $update ) { 851 $this->update( 'translation', $update ); 852 } 853 854 // Clear existing caches. 855 wp_clean_update_cache(); 856 857 wp_version_check(); // Check for core updates. 858 wp_update_themes(); // Check for theme updates. 859 wp_update_plugins(); // Check for plugin updates. 860 } 861 862 // Send debugging email to admin for all development installations. 863 if ( ! empty( $this->update_results ) ) { 864 $development_version = str_contains( wp_get_wp_version(), '-' ); 865 866 /** 867 * Filters whether to send a debugging email for each automatic background update. 868 * 869 * @since 3.7.0 870 * 871 * @param bool $development_version By default, emails are sent if the 872 * install is a development version. 873 * Return false to avoid the email. 874 */ 875 if ( apply_filters( 'automatic_updates_send_debug_email', $development_version ) ) { 876 $this->send_debug_email(); 877 } 878 879 if ( ! empty( $this->update_results['core'] ) ) { 880 $this->after_core_update( $this->update_results['core'][0] ); 881 } elseif ( ! empty( $this->update_results['plugin'] ) || ! empty( $this->update_results['theme'] ) ) { 882 $this->after_plugin_theme_update( $this->update_results ); 883 } 884 885 /** 886 * Fires after all automatic updates have run. 887 * 888 * @since 3.8.0 889 * 890 * @param array $update_results The results of all attempted updates. 891 */ 892 do_action( 'automatic_updates_complete', $this->update_results ); 893 } 894 895 WP_Upgrader::release_lock( 'auto_updater' ); 896 } 897 898 /** 899 * Checks whether to send an email and avoid processing future updates after 900 * attempting a core update. 901 * 902 * @since 3.7.0 903 * 904 * @param object $update_result The result of the core update. Includes the update offer and result. 905 */ 906 protected function after_core_update( $update_result ) { 907 $wp_version = wp_get_wp_version(); 908 909 $core_update = $update_result->item; 910 $result = $update_result->result; 911 912 if ( ! is_wp_error( $result ) ) { 913 $this->send_email( 'success', $core_update ); 914 return; 915 } 916 917 $error_code = $result->get_error_code(); 918 919 /* 920 * Any of these WP_Error codes are critical failures, as in they occurred after we started to copy core files. 921 * We should not try to perform a background update again until there is a successful one-click update performed by the user. 922 */ 923 $critical = false; 924 if ( 'disk_full' === $error_code || str_contains( $error_code, '__copy_dir' ) ) { 925 $critical = true; 926 } elseif ( 'rollback_was_required' === $error_code && is_wp_error( $result->get_error_data()->rollback ) ) { 927 // A rollback is only critical if it failed too. 928 $critical = true; 929 $rollback_result = $result->get_error_data()->rollback; 930 } elseif ( str_contains( $error_code, 'do_rollback' ) ) { 931 $critical = true; 932 } 933 934 if ( $critical ) { 935 $critical_data = array( 936 'attempted' => $core_update->current, 937 'current' => $wp_version, 938 'error_code' => $error_code, 939 'error_data' => $result->get_error_data(), 940 'timestamp' => time(), 941 'critical' => true, 942 ); 943 if ( isset( $rollback_result ) ) { 944 $critical_data['rollback_code'] = $rollback_result->get_error_code(); 945 $critical_data['rollback_data'] = $rollback_result->get_error_data(); 946 } 947 update_site_option( 'auto_core_update_failed', $critical_data ); 948 $this->send_email( 'critical', $core_update, $result ); 949 return; 950 } 951 952 /* 953 * Any other WP_Error code (like download_failed or files_not_writable) occurs before 954 * we tried to copy over core files. Thus, the failures are early and graceful. 955 * 956 * We should avoid trying to perform a background update again for the same version. 957 * But we can try again if another version is released. 958 * 959 * For certain 'transient' failures, like download_failed, we should allow retries. 960 * In fact, let's schedule a special update for an hour from now. (It's possible 961 * the issue could actually be on WordPress.org's side.) If that one fails, then email. 962 */ 963 $send = true; 964 $transient_failures = array( 'incompatible_archive', 'download_failed', 'insane_distro', 'locked' ); 965 if ( in_array( $error_code, $transient_failures, true ) && ! get_site_option( 'auto_core_update_failed' ) ) { 966 wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'wp_maybe_auto_update' ); 967 $send = false; 968 } 969 970 $notified = get_site_option( 'auto_core_update_notified' ); 971 972 // Don't notify if we've already notified the same email address of the same version of the same notification type. 973 if ( $notified 974 && 'fail' === $notified['type'] 975 && get_site_option( 'admin_email' ) === $notified['email'] 976 && $notified['version'] === $core_update->current 977 ) { 978 $send = false; 979 } 980 981 update_site_option( 982 'auto_core_update_failed', 983 array( 984 'attempted' => $core_update->current, 985 'current' => $wp_version, 986 'error_code' => $error_code, 987 'error_data' => $result->get_error_data(), 988 'timestamp' => time(), 989 'retry' => in_array( $error_code, $transient_failures, true ), 990 ) 991 ); 992 993 if ( $send ) { 994 $this->send_email( 'fail', $core_update, $result ); 995 } 996 } 997 998 /** 999 * Sends an email upon the completion or failure of a background core update. 1000 * 1001 * @since 3.7.0 1002 * 1003 * @param string $type The type of email to send. Can be one of 'success', 'fail', 'manual', 'critical'. 1004 * @param object $core_update The update offer that was attempted. 1005 * @param mixed $result Optional. The result for the core update. Can be WP_Error. 1006 */ 1007 protected function send_email( $type, $core_update, $result = null ) { 1008 update_site_option( 1009 'auto_core_update_notified', 1010 array( 1011 'type' => $type, 1012 'email' => get_site_option( 'admin_email' ), 1013 'version' => $core_update->current, 1014 'timestamp' => time(), 1015 ) 1016 ); 1017 1018 $next_user_core_update = get_preferred_from_update_core(); 1019 1020 // If the update transient is empty, use the update we just performed. 1021 if ( ! $next_user_core_update ) { 1022 $next_user_core_update = $core_update; 1023 } 1024 1025 if ( 'upgrade' === $next_user_core_update->response 1026 && version_compare( $next_user_core_update->version, $core_update->version, '>' ) 1027 ) { 1028 $newer_version_available = true; 1029 } else { 1030 $newer_version_available = false; 1031 } 1032 1033 /** 1034 * Filters whether to send an email following an automatic background core update. 1035 * 1036 * @since 3.7.0 1037 * 1038 * @param bool $send Whether to send the email. Default true. 1039 * @param string $type The type of email to send. Can be one of 1040 * 'success', 'fail', 'critical'. 1041 * @param object $core_update The update offer that was attempted. 1042 * @param mixed $result The result for the core update. Can be WP_Error. 1043 */ 1044 if ( 'manual' !== $type && ! apply_filters( 'auto_core_update_send_email', true, $type, $core_update, $result ) ) { 1045 return; 1046 } 1047 1048 $admin_user = get_user_by( 'email', get_site_option( 'admin_email' ) ); 1049 1050 if ( $admin_user ) { 1051 $switched_locale = switch_to_user_locale( $admin_user->ID ); 1052 } else { 1053 $switched_locale = switch_to_locale( get_locale() ); 1054 } 1055 1056 switch ( $type ) { 1057 case 'success': // We updated. 1058 /* translators: Site updated notification email subject. 1: Site title, 2: WordPress version. */ 1059 $subject = __( '[%1$s] Your site has updated to WordPress %2$s' ); 1060 break; 1061 1062 case 'fail': // We tried to update but couldn't. 1063 case 'manual': // We can't update (and made no attempt). 1064 /* translators: Update available notification email subject. 1: Site title, 2: WordPress version. */ 1065 $subject = __( '[%1$s] WordPress %2$s is available. Please update!' ); 1066 break; 1067 1068 case 'critical': // We tried to update, started to copy files, then things went wrong. 1069 /* translators: Site down notification email subject. 1: Site title. */ 1070 $subject = __( '[%1$s] URGENT: Your site may be down due to a failed update' ); 1071 break; 1072 1073 default: 1074 return; 1075 } 1076 1077 // If the auto-update is not to the latest version, say that the current version of WP is available instead. 1078 $version = 'success' === $type ? $core_update->current : $next_user_core_update->current; 1079 $subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), $version ); 1080 1081 $body = ''; 1082 1083 switch ( $type ) { 1084 case 'success': 1085 $body .= sprintf( 1086 /* translators: 1: Home URL, 2: WordPress version. */ 1087 __( 'Howdy! Your site at %1$s has been updated automatically to WordPress %2$s.' ), 1088 home_url(), 1089 $core_update->current 1090 ); 1091 $body .= "\n\n"; 1092 if ( ! $newer_version_available ) { 1093 $body .= __( 'No further action is needed on your part.' ) . ' '; 1094 } 1095 1096 // Can only reference the About screen if their update was successful. 1097 list( $about_version ) = explode( '-', $core_update->current, 2 ); 1098 /* translators: %s: WordPress version. */ 1099 $body .= sprintf( __( 'For more on version %s, see the About WordPress screen:' ), $about_version ); 1100 $body .= "\n" . admin_url( 'about.php' ); 1101 1102 if ( $newer_version_available ) { 1103 /* translators: %s: WordPress latest version. */ 1104 $body .= "\n\n" . sprintf( __( 'WordPress %s is also now available.' ), $next_user_core_update->current ) . ' '; 1105 $body .= __( 'Updating is easy and only takes a few moments:' ); 1106 $body .= "\n" . network_admin_url( 'update-core.php' ); 1107 } 1108 1109 break; 1110 1111 case 'fail': 1112 case 'manual': 1113 $body .= sprintf( 1114 /* translators: 1: Home URL, 2: WordPress version. */ 1115 __( 'Please update your site at %1$s to WordPress %2$s.' ), 1116 home_url(), 1117 $next_user_core_update->current 1118 ); 1119 1120 $body .= "\n\n"; 1121 1122 /* 1123 * Don't show this message if there is a newer version available. 1124 * Potential for confusion, and also not useful for them to know at this point. 1125 */ 1126 if ( 'fail' === $type && ! $newer_version_available ) { 1127 $body .= __( 'An attempt was made, but your site could not be updated automatically.' ) . ' '; 1128 } 1129 1130 $body .= __( 'Updating is easy and only takes a few moments:' ); 1131 $body .= "\n" . network_admin_url( 'update-core.php' ); 1132 break; 1133 1134 case 'critical': 1135 if ( $newer_version_available ) { 1136 $body .= sprintf( 1137 /* translators: 1: Home URL, 2: WordPress version. */ 1138 __( 'Your site at %1$s experienced a critical failure while trying to update WordPress to version %2$s.' ), 1139 home_url(), 1140 $core_update->current 1141 ); 1142 } else { 1143 $body .= sprintf( 1144 /* translators: 1: Home URL, 2: WordPress latest version. */ 1145 __( 'Your site at %1$s experienced a critical failure while trying to update to the latest version of WordPress, %2$s.' ), 1146 home_url(), 1147 $core_update->current 1148 ); 1149 } 1150 1151 $body .= "\n\n" . __( "This means your site may be offline or broken. Don't panic; this can be fixed." ); 1152 1153 $body .= "\n\n" . __( "Please check out your site now. It's possible that everything is working. If it says you need to update, you should do so:" ); 1154 $body .= "\n" . network_admin_url( 'update-core.php' ); 1155 break; 1156 } 1157 1158 $critical_support = 'critical' === $type && ! empty( $core_update->support_email ); 1159 if ( $critical_support ) { 1160 // Support offer if available. 1161 $body .= "\n\n" . sprintf( 1162 /* translators: %s: Support email address. */ 1163 __( 'The WordPress team is willing to help you. Forward this email to %s and the team will work with you to make sure your site is working.' ), 1164 $core_update->support_email 1165 ); 1166 } else { 1167 // Add a note about the support forums. 1168 $body .= "\n\n" . __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' ); 1169 $body .= "\n" . __( 'https://wordpress.org/support/forums/' ); 1170 } 1171 1172 // Updates are important! 1173 if ( 'success' !== $type || $newer_version_available ) { 1174 $body .= "\n\n" . __( 'Keeping your site updated is important for security. It also makes the internet a safer place for you and your readers.' ); 1175 } 1176 1177 if ( $critical_support ) { 1178 $body .= ' ' . __( "Reach out to WordPress Core developers to ensure you'll never have this problem again." ); 1179 } 1180 1181 // If things are successful and we're now on the latest, mention plugins and themes if any are out of date. 1182 if ( 'success' === $type && ! $newer_version_available && ( get_plugin_updates() || get_theme_updates() ) ) { 1183 $body .= "\n\n" . __( 'You also have some plugins or themes with updates available. Update them now:' ); 1184 $body .= "\n" . network_admin_url(); 1185 } 1186 1187 $body .= "\n\n" . __( 'The WordPress Team' ) . "\n"; 1188 1189 if ( 'critical' === $type && is_wp_error( $result ) ) { 1190 $body .= "\n***\n\n"; 1191 /* translators: %s: WordPress version. */ 1192 $body .= sprintf( __( 'Your site was running version %s.' ), get_bloginfo( 'version' ) ); 1193 $body .= ' ' . __( 'Some data that describes the error your site encountered has been put together.' ); 1194 $body .= ' ' . __( 'Your hosting company, support forum volunteers, or a friendly developer may be able to use this information to help you:' ); 1195 1196 /* 1197 * If we had a rollback and we're still critical, then the rollback failed too. 1198 * Loop through all errors (the main WP_Error, the update result, the rollback result) for code, data, etc. 1199 */ 1200 if ( 'rollback_was_required' === $result->get_error_code() ) { 1201 $errors = array( $result, $result->get_error_data()->update, $result->get_error_data()->rollback ); 1202 } else { 1203 $errors = array( $result ); 1204 } 1205 1206 foreach ( $errors as $error ) { 1207 if ( ! is_wp_error( $error ) ) { 1208 continue; 1209 } 1210 1211 $error_code = $error->get_error_code(); 1212 /* translators: %s: Error code. */ 1213 $body .= "\n\n" . sprintf( __( 'Error code: %s' ), $error_code ); 1214 1215 if ( 'rollback_was_required' === $error_code ) { 1216 continue; 1217 } 1218 1219 if ( $error->get_error_message() ) { 1220 $body .= "\n" . $error->get_error_message(); 1221 } 1222 1223 $error_data = $error->get_error_data(); 1224 if ( $error_data ) { 1225 $body .= "\n" . implode( ', ', (array) $error_data ); 1226 } 1227 } 1228 1229 $body .= "\n"; 1230 } 1231 1232 $to = get_site_option( 'admin_email' ); 1233 $headers = ''; 1234 1235 $email = compact( 'to', 'subject', 'body', 'headers' ); 1236 1237 /** 1238 * Filters the email sent following an automatic background core update. 1239 * 1240 * @since 3.7.0 1241 * 1242 * @param array $email { 1243 * Array of email arguments that will be passed to wp_mail(). 1244 * 1245 * @type string $to The email recipient. An array of emails 1246 * can be returned, as handled by wp_mail(). 1247 * @type string $subject The email's subject. 1248 * @type string $body The email message body. 1249 * @type string $headers Any email headers, defaults to no headers. 1250 * } 1251 * @param string $type The type of email being sent. Can be one of 1252 * 'success', 'fail', 'manual', 'critical'. 1253 * @param object $core_update The update offer that was attempted. 1254 * @param mixed $result The result for the core update. Can be WP_Error. 1255 */ 1256 $email = apply_filters( 'auto_core_update_email', $email, $type, $core_update, $result ); 1257 1258 wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); 1259 1260 if ( $switched_locale ) { 1261 restore_previous_locale(); 1262 } 1263 } 1264 1265 /** 1266 * Checks whether an email should be sent after attempting plugin or theme updates. 1267 * 1268 * @since 5.5.0 1269 * 1270 * @param array $update_results The results of update tasks. 1271 */ 1272 protected function after_plugin_theme_update( $update_results ) { 1273 $successful_updates = array(); 1274 $failed_updates = array(); 1275 1276 if ( ! empty( $update_results['plugin'] ) ) { 1277 /** 1278 * Filters whether to send an email following an automatic background plugin update. 1279 * 1280 * @since 5.5.0 1281 * @since 5.5.1 Added the `$update_results` parameter. 1282 * 1283 * @param bool $enabled True if plugin update notifications are enabled, false otherwise. 1284 * @param array $update_results The results of plugins update tasks. 1285 */ 1286 $notifications_enabled = apply_filters( 'auto_plugin_update_send_email', true, $update_results['plugin'] ); 1287 1288 if ( $notifications_enabled ) { 1289 foreach ( $update_results['plugin'] as $update_result ) { 1290 if ( true === $update_result->result ) { 1291 $successful_updates['plugin'][] = $update_result; 1292 } else { 1293 $failed_updates['plugin'][] = $update_result; 1294 } 1295 } 1296 } 1297 } 1298 1299 if ( ! empty( $update_results['theme'] ) ) { 1300 /** 1301 * Filters whether to send an email following an automatic background theme update. 1302 * 1303 * @since 5.5.0 1304 * @since 5.5.1 Added the `$update_results` parameter. 1305 * 1306 * @param bool $enabled True if theme update notifications are enabled, false otherwise. 1307 * @param array $update_results The results of theme update tasks. 1308 */ 1309 $notifications_enabled = apply_filters( 'auto_theme_update_send_email', true, $update_results['theme'] ); 1310 1311 if ( $notifications_enabled ) { 1312 foreach ( $update_results['theme'] as $update_result ) { 1313 if ( true === $update_result->result ) { 1314 $successful_updates['theme'][] = $update_result; 1315 } else { 1316 $failed_updates['theme'][] = $update_result; 1317 } 1318 } 1319 } 1320 } 1321 1322 if ( empty( $successful_updates ) && empty( $failed_updates ) ) { 1323 return; 1324 } 1325 1326 if ( empty( $failed_updates ) ) { 1327 $this->send_plugin_theme_email( 'success', $successful_updates, $failed_updates ); 1328 } elseif ( empty( $successful_updates ) ) { 1329 $this->send_plugin_theme_email( 'fail', $successful_updates, $failed_updates ); 1330 } else { 1331 $this->send_plugin_theme_email( 'mixed', $successful_updates, $failed_updates ); 1332 } 1333 } 1334 1335 /** 1336 * Sends an email upon the completion or failure of a plugin or theme background update. 1337 * 1338 * @since 5.5.0 1339 * 1340 * @param string $type The type of email to send. Can be one of 'success', 'fail', 'mixed'. 1341 * @param array $successful_updates A list of updates that succeeded. 1342 * @param array $failed_updates A list of updates that failed. 1343 */ 1344 protected function send_plugin_theme_email( $type, $successful_updates, $failed_updates ) { 1345 // No updates were attempted. 1346 if ( empty( $successful_updates ) && empty( $failed_updates ) ) { 1347 return; 1348 } 1349 1350 $unique_failures = false; 1351 $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); 1352 1353 /* 1354 * When only failures have occurred, an email should only be sent if there are unique failures. 1355 * A failure is considered unique if an email has not been sent for an update attempt failure 1356 * to a plugin or theme with the same new_version. 1357 */ 1358 if ( 'fail' === $type ) { 1359 foreach ( $failed_updates as $update_type => $failures ) { 1360 foreach ( $failures as $failed_update ) { 1361 if ( ! isset( $past_failure_emails[ $failed_update->item->{$update_type} ] ) ) { 1362 $unique_failures = true; 1363 continue; 1364 } 1365 1366 // Check that the failure represents a new failure based on the new_version. 1367 if ( version_compare( $past_failure_emails[ $failed_update->item->{$update_type} ], $failed_update->item->new_version, '<' ) ) { 1368 $unique_failures = true; 1369 } 1370 } 1371 } 1372 1373 if ( ! $unique_failures ) { 1374 return; 1375 } 1376 } 1377 1378 $admin_user = get_user_by( 'email', get_site_option( 'admin_email' ) ); 1379 1380 if ( $admin_user ) { 1381 $switched_locale = switch_to_user_locale( $admin_user->ID ); 1382 } else { 1383 $switched_locale = switch_to_locale( get_locale() ); 1384 } 1385 1386 $body = array(); 1387 $successful_plugins = ( ! empty( $successful_updates['plugin'] ) ); 1388 $successful_themes = ( ! empty( $successful_updates['theme'] ) ); 1389 $failed_plugins = ( ! empty( $failed_updates['plugin'] ) ); 1390 $failed_themes = ( ! empty( $failed_updates['theme'] ) ); 1391 1392 switch ( $type ) { 1393 case 'success': 1394 if ( $successful_plugins && $successful_themes ) { 1395 /* translators: %s: Site title. */ 1396 $subject = __( '[%s] Some plugins and themes have automatically updated' ); 1397 $body[] = sprintf( 1398 /* translators: %s: Home URL. */ 1399 __( 'Howdy! Some plugins and themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ), 1400 home_url() 1401 ); 1402 } elseif ( $successful_plugins ) { 1403 /* translators: %s: Site title. */ 1404 $subject = __( '[%s] Some plugins were automatically updated' ); 1405 $body[] = sprintf( 1406 /* translators: %s: Home URL. */ 1407 __( 'Howdy! Some plugins have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ), 1408 home_url() 1409 ); 1410 } else { 1411 /* translators: %s: Site title. */ 1412 $subject = __( '[%s] Some themes were automatically updated' ); 1413 $body[] = sprintf( 1414 /* translators: %s: Home URL. */ 1415 __( 'Howdy! Some themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ), 1416 home_url() 1417 ); 1418 } 1419 1420 break; 1421 case 'fail': 1422 case 'mixed': 1423 if ( $failed_plugins && $failed_themes ) { 1424 /* translators: %s: Site title. */ 1425 $subject = __( '[%s] Some plugins and themes have failed to update' ); 1426 $body[] = sprintf( 1427 /* translators: %s: Home URL. */ 1428 __( 'Howdy! Plugins and themes failed to update on your site at %s.' ), 1429 home_url() 1430 ); 1431 } elseif ( $failed_plugins ) { 1432 /* translators: %s: Site title. */ 1433 $subject = __( '[%s] Some plugins have failed to update' ); 1434 $body[] = sprintf( 1435 /* translators: %s: Home URL. */ 1436 __( 'Howdy! Plugins failed to update on your site at %s.' ), 1437 home_url() 1438 ); 1439 } else { 1440 /* translators: %s: Site title. */ 1441 $subject = __( '[%s] Some themes have failed to update' ); 1442 $body[] = sprintf( 1443 /* translators: %s: Home URL. */ 1444 __( 'Howdy! Themes failed to update on your site at %s.' ), 1445 home_url() 1446 ); 1447 } 1448 1449 break; 1450 } 1451 1452 if ( in_array( $type, array( 'fail', 'mixed' ), true ) ) { 1453 $body[] = "\n"; 1454 $body[] = __( 'Please check your site now. It’s possible that everything is working. If there are updates available, you should update.' ); 1455 $body[] = "\n"; 1456 1457 // List failed plugin updates. 1458 if ( ! empty( $failed_updates['plugin'] ) ) { 1459 $body[] = __( 'The following plugins failed to update. If there was a fatal error in the update, the previously installed version has been restored.' ); 1460 1461 foreach ( $failed_updates['plugin'] as $item ) { 1462 $body_message = ''; 1463 $item_url = ''; 1464 1465 if ( ! empty( $item->item->url ) ) { 1466 $item_url = ' : ' . esc_url( $item->item->url ); 1467 } 1468 1469 if ( $item->item->current_version ) { 1470 $body_message .= sprintf( 1471 /* translators: 1: Plugin name, 2: Current version number, 3: New version number, 4: Plugin URL. */ 1472 __( '- %1$s (from version %2$s to %3$s)%4$s' ), 1473 html_entity_decode( $item->name ), 1474 $item->item->current_version, 1475 $item->item->new_version, 1476 $item_url 1477 ); 1478 } else { 1479 $body_message .= sprintf( 1480 /* translators: 1: Plugin name, 2: Version number, 3: Plugin URL. */ 1481 __( '- %1$s version %2$s%3$s' ), 1482 html_entity_decode( $item->name ), 1483 $item->item->new_version, 1484 $item_url 1485 ); 1486 } 1487 1488 $body[] = $body_message; 1489 1490 $past_failure_emails[ $item->item->plugin ] = $item->item->new_version; 1491 } 1492 1493 $body[] = "\n"; 1494 } 1495 1496 // List failed theme updates. 1497 if ( ! empty( $failed_updates['theme'] ) ) { 1498 $body[] = __( 'These themes failed to update:' ); 1499 1500 foreach ( $failed_updates['theme'] as $item ) { 1501 if ( $item->item->current_version ) { 1502 $body[] = sprintf( 1503 /* translators: 1: Theme name, 2: Current version number, 3: New version number. */ 1504 __( '- %1$s (from version %2$s to %3$s)' ), 1505 html_entity_decode( $item->name ), 1506 $item->item->current_version, 1507 $item->item->new_version 1508 ); 1509 } else { 1510 $body[] = sprintf( 1511 /* translators: 1: Theme name, 2: Version number. */ 1512 __( '- %1$s version %2$s' ), 1513 html_entity_decode( $item->name ), 1514 $item->item->new_version 1515 ); 1516 } 1517 1518 $past_failure_emails[ $item->item->theme ] = $item->item->new_version; 1519 } 1520 1521 $body[] = "\n"; 1522 } 1523 } 1524 1525 // List successful updates. 1526 if ( in_array( $type, array( 'success', 'mixed' ), true ) ) { 1527 $body[] = "\n"; 1528 1529 // List successful plugin updates. 1530 if ( ! empty( $successful_updates['plugin'] ) ) { 1531 $body[] = __( 'These plugins are now up to date:' ); 1532 1533 foreach ( $successful_updates['plugin'] as $item ) { 1534 $body_message = ''; 1535 $item_url = ''; 1536 1537 if ( ! empty( $item->item->url ) ) { 1538 $item_url = ' : ' . esc_url( $item->item->url ); 1539 } 1540 1541 if ( $item->item->current_version ) { 1542 $body_message .= sprintf( 1543 /* translators: 1: Plugin name, 2: Current version number, 3: New version number, 4: Plugin URL. */ 1544 __( '- %1$s (from version %2$s to %3$s)%4$s' ), 1545 html_entity_decode( $item->name ), 1546 $item->item->current_version, 1547 $item->item->new_version, 1548 $item_url 1549 ); 1550 } else { 1551 $body_message .= sprintf( 1552 /* translators: 1: Plugin name, 2: Version number, 3: Plugin URL. */ 1553 __( '- %1$s version %2$s%3$s' ), 1554 html_entity_decode( $item->name ), 1555 $item->item->new_version, 1556 $item_url 1557 ); 1558 } 1559 $body[] = $body_message; 1560 1561 unset( $past_failure_emails[ $item->item->plugin ] ); 1562 } 1563 1564 $body[] = "\n"; 1565 } 1566 1567 // List successful theme updates. 1568 if ( ! empty( $successful_updates['theme'] ) ) { 1569 $body[] = __( 'These themes are now up to date:' ); 1570 1571 foreach ( $successful_updates['theme'] as $item ) { 1572 if ( $item->item->current_version ) { 1573 $body[] = sprintf( 1574 /* translators: 1: Theme name, 2: Current version number, 3: New version number. */ 1575 __( '- %1$s (from version %2$s to %3$s)' ), 1576 html_entity_decode( $item->name ), 1577 $item->item->current_version, 1578 $item->item->new_version 1579 ); 1580 } else { 1581 $body[] = sprintf( 1582 /* translators: 1: Theme name, 2: Version number. */ 1583 __( '- %1$s version %2$s' ), 1584 html_entity_decode( $item->name ), 1585 $item->item->new_version 1586 ); 1587 } 1588 1589 unset( $past_failure_emails[ $item->item->theme ] ); 1590 } 1591 1592 $body[] = "\n"; 1593 } 1594 } 1595 1596 if ( $failed_plugins ) { 1597 $body[] = sprintf( 1598 /* translators: %s: Plugins screen URL. */ 1599 __( 'To manage plugins on your site, visit the Plugins page: %s' ), 1600 admin_url( 'plugins.php' ) 1601 ); 1602 $body[] = "\n"; 1603 } 1604 1605 if ( $failed_themes ) { 1606 $body[] = sprintf( 1607 /* translators: %s: Themes screen URL. */ 1608 __( 'To manage themes on your site, visit the Themes page: %s' ), 1609 admin_url( 'themes.php' ) 1610 ); 1611 $body[] = "\n"; 1612 } 1613 1614 // Add a note about the support forums. 1615 $body[] = __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' ); 1616 $body[] = __( 'https://wordpress.org/support/forums/' ); 1617 $body[] = "\n" . __( 'The WordPress Team' ); 1618 1619 if ( '' !== get_option( 'blogname' ) ) { 1620 $site_title = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); 1621 } else { 1622 $site_title = parse_url( home_url(), PHP_URL_HOST ); 1623 } 1624 1625 $body = implode( "\n", $body ); 1626 $to = get_site_option( 'admin_email' ); 1627 $subject = sprintf( $subject, $site_title ); 1628 $headers = ''; 1629 1630 $email = compact( 'to', 'subject', 'body', 'headers' ); 1631 1632 /** 1633 * Filters the email sent following an automatic background update for plugins and themes. 1634 * 1635 * @since 5.5.0 1636 * 1637 * @param array $email { 1638 * Array of email arguments that will be passed to wp_mail(). 1639 * 1640 * @type string $to The email recipient. An array of emails 1641 * can be returned, as handled by wp_mail(). 1642 * @type string $subject The email's subject. 1643 * @type string $body The email message body. 1644 * @type string $headers Any email headers, defaults to no headers. 1645 * } 1646 * @param string $type The type of email being sent. Can be one of 'success', 'fail', 'mixed'. 1647 * @param array $successful_updates A list of updates that succeeded. 1648 * @param array $failed_updates A list of updates that failed. 1649 */ 1650 $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); 1651 1652 $result = wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); 1653 1654 if ( $result ) { 1655 update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); 1656 } 1657 1658 if ( $switched_locale ) { 1659 restore_previous_locale(); 1660 } 1661 } 1662 1663 /** 1664 * Prepares and sends an email of a full log of background update results, useful for debugging and geekery. 1665 * 1666 * @since 3.7.0 1667 */ 1668 protected function send_debug_email() { 1669 $admin_user = get_user_by( 'email', get_site_option( 'admin_email' ) ); 1670 1671 if ( $admin_user ) { 1672 $switched_locale = switch_to_user_locale( $admin_user->ID ); 1673 } else { 1674 $switched_locale = switch_to_locale( get_locale() ); 1675 } 1676 1677 $body = array(); 1678 $failures = 0; 1679 1680 /* translators: %s: Network home URL. */ 1681 $body[] = sprintf( __( 'WordPress site: %s' ), network_home_url( '/' ) ); 1682 1683 // Core. 1684 if ( isset( $this->update_results['core'] ) ) { 1685 $result = $this->update_results['core'][0]; 1686 1687 if ( $result->result && ! is_wp_error( $result->result ) ) { 1688 /* translators: %s: WordPress version. */ 1689 $body[] = sprintf( __( 'SUCCESS: WordPress was successfully updated to %s' ), $result->name ); 1690 } else { 1691 /* translators: %s: WordPress version. */ 1692 $body[] = sprintf( __( 'FAILED: WordPress failed to update to %s' ), $result->name ); 1693 ++$failures; 1694 } 1695 1696 $body[] = ''; 1697 } 1698 1699 // Plugins, Themes, Translations. 1700 foreach ( array( 'plugin', 'theme', 'translation' ) as $type ) { 1701 if ( ! isset( $this->update_results[ $type ] ) ) { 1702 continue; 1703 } 1704 1705 $success_items = wp_list_filter( $this->update_results[ $type ], array( 'result' => true ) ); 1706 1707 if ( $success_items ) { 1708 $messages = array( 1709 'plugin' => __( 'The following plugins were successfully updated:' ), 1710 'theme' => __( 'The following themes were successfully updated:' ), 1711 'translation' => __( 'The following translations were successfully updated:' ), 1712 ); 1713 1714 $body[] = $messages[ $type ]; 1715 foreach ( wp_list_pluck( $success_items, 'name' ) as $name ) { 1716 /* translators: %s: Name of plugin / theme / translation. */ 1717 $body[] = ' * ' . sprintf( __( 'SUCCESS: %s' ), $name ); 1718 } 1719 } 1720 1721 if ( $success_items !== $this->update_results[ $type ] ) { 1722 // Failed updates. 1723 $messages = array( 1724 'plugin' => __( 'The following plugins failed to update:' ), 1725 'theme' => __( 'The following themes failed to update:' ), 1726 'translation' => __( 'The following translations failed to update:' ), 1727 ); 1728 1729 $body[] = $messages[ $type ]; 1730 1731 foreach ( $this->update_results[ $type ] as $item ) { 1732 if ( ! $item->result || is_wp_error( $item->result ) ) { 1733 /* translators: %s: Name of plugin / theme / translation. */ 1734 $body[] = ' * ' . sprintf( __( 'FAILED: %s' ), $item->name ); 1735 ++$failures; 1736 } 1737 } 1738 } 1739 1740 $body[] = ''; 1741 } 1742 1743 if ( '' !== get_bloginfo( 'name' ) ) { 1744 $site_title = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); 1745 } else { 1746 $site_title = parse_url( home_url(), PHP_URL_HOST ); 1747 } 1748 1749 if ( $failures ) { 1750 $body[] = trim( 1751 __( 1752 "BETA TESTING? 1753 ============= 1754 1755 This debugging email is sent when you are using a development version of WordPress. 1756 1757 If you think these failures might be due to a bug in WordPress, could you report it? 1758 * Open a thread in the support forums: https://wordpress.org/support/forum/alphabeta 1759 * Or, if you're comfortable writing a bug report: https://core.trac.wordpress.org/ 1760 1761 Thanks! -- The WordPress Team" 1762 ) 1763 ); 1764 $body[] = ''; 1765 1766 /* translators: Background update failed notification email subject. %s: Site title. */ 1767 $subject = sprintf( __( '[%s] Background Update Failed' ), $site_title ); 1768 } else { 1769 /* translators: Background update finished notification email subject. %s: Site title. */ 1770 $subject = sprintf( __( '[%s] Background Update Finished' ), $site_title ); 1771 } 1772 1773 $body[] = trim( 1774 __( 1775 'UPDATE LOG 1776 ==========' 1777 ) 1778 ); 1779 $body[] = ''; 1780 1781 foreach ( array( 'core', 'plugin', 'theme', 'translation' ) as $type ) { 1782 if ( ! isset( $this->update_results[ $type ] ) ) { 1783 continue; 1784 } 1785 1786 foreach ( $this->update_results[ $type ] as $update ) { 1787 $body[] = $update->name; 1788 $body[] = str_repeat( '-', strlen( $update->name ) ); 1789 1790 foreach ( $update->messages as $message ) { 1791 $body[] = ' ' . html_entity_decode( str_replace( '…', '...', $message ) ); 1792 } 1793 1794 if ( is_wp_error( $update->result ) ) { 1795 $results = array( 'update' => $update->result ); 1796 1797 // If we rolled back, we want to know an error that occurred then too. 1798 if ( 'rollback_was_required' === $update->result->get_error_code() ) { 1799 $results = (array) $update->result->get_error_data(); 1800 } 1801 1802 foreach ( $results as $result_type => $result ) { 1803 if ( ! is_wp_error( $result ) ) { 1804 continue; 1805 } 1806 1807 if ( 'rollback' === $result_type ) { 1808 /* translators: 1: Error code, 2: Error message. */ 1809 $body[] = ' ' . sprintf( __( 'Rollback Error: [%1$s] %2$s' ), $result->get_error_code(), $result->get_error_message() ); 1810 } else { 1811 /* translators: 1: Error code, 2: Error message. */ 1812 $body[] = ' ' . sprintf( __( 'Error: [%1$s] %2$s' ), $result->get_error_code(), $result->get_error_message() ); 1813 } 1814 1815 if ( $result->get_error_data() ) { 1816 $body[] = ' ' . implode( ', ', (array) $result->get_error_data() ); 1817 } 1818 } 1819 } 1820 1821 $body[] = ''; 1822 } 1823 } 1824 1825 $email = array( 1826 'to' => get_site_option( 'admin_email' ), 1827 'subject' => $subject, 1828 'body' => implode( "\n", $body ), 1829 'headers' => '', 1830 ); 1831 1832 /** 1833 * Filters the debug email that can be sent following an automatic 1834 * background core update. 1835 * 1836 * @since 3.8.0 1837 * 1838 * @param array $email { 1839 * Array of email arguments that will be passed to wp_mail(). 1840 * 1841 * @type string $to The email recipient. An array of emails 1842 * can be returned, as handled by wp_mail(). 1843 * @type string $subject Email subject. 1844 * @type string $body Email message body. 1845 * @type string $headers Any email headers. Default empty. 1846 * } 1847 * @param int $failures The number of failures encountered while upgrading. 1848 * @param mixed $results The results of all attempted updates. 1849 */ 1850 $email = apply_filters( 'automatic_updates_debug_email', $email, $failures, $this->update_results ); 1851 1852 wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); 1853 1854 if ( $switched_locale ) { 1855 restore_previous_locale(); 1856 } 1857 } 1858 1859 /** 1860 * Performs a loopback request to check for potential fatal errors. 1861 * 1862 * Fatal errors cannot be detected unless maintenance mode is enabled. 1863 * 1864 * @since 6.6.0 1865 * 1866 * @global int $upgrading The Unix timestamp marking when upgrading WordPress began. 1867 * 1868 * @return bool Whether a fatal error was detected. 1869 */ 1870 protected function has_fatal_error() { 1871 global $upgrading; 1872 1873 $maintenance_file = ABSPATH . '.maintenance'; 1874 if ( ! file_exists( $maintenance_file ) ) { 1875 return false; 1876 } 1877 1878 require $maintenance_file; 1879 if ( ! is_int( $upgrading ) ) { 1880 return false; 1881 } 1882 1883 $scrape_key = md5( $upgrading ); 1884 $scrape_nonce = (string) $upgrading; 1885 $transient = 'scrape_key_' . $scrape_key; 1886 set_transient( $transient, $scrape_nonce, 30 ); 1887 1888 $cookies = wp_unslash( $_COOKIE ); 1889 $scrape_params = array( 1890 'wp_scrape_key' => $scrape_key, 1891 'wp_scrape_nonce' => $scrape_nonce, 1892 ); 1893 $headers = array( 1894 'Cache-Control' => 'no-cache', 1895 ); 1896 1897 // Include Basic auth in the loopback request. 1898 if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) { 1899 $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) ); 1900 } 1901 1902 // Time to wait for loopback request to finish. 1903 $timeout = 50; // 50 seconds. 1904 1905 $is_debug = WP_DEBUG && WP_DEBUG_LOG; 1906 if ( $is_debug ) { 1907 error_log( ' Scraping home page...' ); 1908 } 1909 1910 $needle_start = "###### wp_scraping_result_start:$scrape_key ######"; 1911 $needle_end = "###### wp_scraping_result_end:$scrape_key ######"; 1912 $url = add_query_arg( $scrape_params, home_url( '/' ) ); 1913 1914 /** This filter is documented in wp-includes/class-wp-http-streams.php */ 1915 $sslverify = apply_filters( 'https_local_ssl_verify', false, $url ); 1916 $response = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) ); 1917 1918 if ( is_wp_error( $response ) ) { 1919 if ( $is_debug ) { 1920 error_log( 'Loopback request failed: ' . $response->get_error_message() ); 1921 } 1922 return true; 1923 } 1924 1925 // If this outputs `true` in the log, it means there were no fatal errors detected. 1926 if ( $is_debug ) { 1927 error_log( var_export( substr( $response['body'], strpos( $response['body'], '###### wp_scraping_result_start:' ) ), true ) ); 1928 } 1929 1930 $body = wp_remote_retrieve_body( $response ); 1931 $scrape_result_position = strpos( $body, $needle_start ); 1932 $result = null; 1933 1934 if ( false !== $scrape_result_position ) { 1935 $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) ); 1936 $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) ); 1937 $result = json_decode( trim( $error_output ), true ); 1938 } 1939 1940 delete_transient( $transient ); 1941 1942 // Only fatal errors will result in a 'type' key. 1943 return isset( $result['type'] ); 1944 } 1945 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Oct 17 08:20:33 2026 | Cross-referenced by PHPXref |