Ticket #44458: 44458.9.diff
File 44458.9.diff, 41.8 KB (added by , 7 years ago) |
---|
-
src/wp-admin/css/list-tables.css
diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index c6a0c30348..3066a4b583 100644
a b ul.cat-checklist { 1301 1301 text-decoration: underline; 1302 1302 } 1303 1303 1304 .plugins tr.paused th.check-column { 1305 border-left: 4px solid #d54e21; 1306 } 1307 1308 .plugins tr.paused th, 1309 .plugins tr.paused td { 1310 background-color: #fef7f1; 1311 } 1312 1313 .plugins tr.paused .plugin-title, 1314 .plugins .paused .dashicons-warning { 1315 color: #dc3232; 1316 } 1317 1318 .plugins .paused .error-display p, 1319 .plugins .paused .error-display code { 1320 font-size: 90%; 1321 font-style: italic; 1322 color: rgb( 0, 0, 0, 0.7 ); 1323 } 1324 1325 .plugins .resume-link { 1326 color: #dc3232; 1327 } 1328 1304 1329 .plugin-card .update-now:before { 1305 1330 color: #f56e28; 1306 1331 content: "\f463"; -
src/wp-admin/includes/admin-filters.php
diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 0816b2420b..bc2f769b64 100644
a b 121 121 add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called. 122 122 123 123 add_action( 'admin_notices', 'update_nag', 3 ); 124 add_action( 'admin_notices', 'paused_plugins_notice', 5 ); 124 125 add_action( 'admin_notices', 'maintenance_nag', 10 ); 125 126 126 127 add_filter( 'update_footer', 'core_update_footer' ); -
src/wp-admin/includes/class-wp-plugins-list-table.php
diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index 1f540c9ed2..661c407ef8 100644
a b public function __construct( $args = array() ) { 40 40 ); 41 41 42 42 $status = 'all'; 43 if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search' ) ) ) {43 if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused' ) ) ) { 44 44 $status = $_REQUEST['plugin_status']; 45 45 } 46 46 … … public function prepare_items() { 99 99 'upgrade' => array(), 100 100 'mustuse' => array(), 101 101 'dropins' => array(), 102 'paused' => array(), 102 103 ); 103 104 104 105 $screen = $this->screen; … … public function prepare_items() { 209 210 if ( $show_network_active ) { 210 211 // On the non-network screen, show network-active plugins if allowed 211 212 $plugins['active'][ $plugin_file ] = $plugin_data; 213 if ( is_plugin_paused( $plugin_file ) ) { 214 $plugins['paused'][ $plugin_file ] = $plugin_data; 215 } 212 216 } else { 213 217 // On the non-network screen, filter out network-active plugins 214 218 unset( $plugins['all'][ $plugin_file ] ); … … public function prepare_items() { 218 222 // On the non-network screen, populate the active list with plugins that are individually activated 219 223 // On the network-admin screen, populate the active list with plugins that are network activated 220 224 $plugins['active'][ $plugin_file ] = $plugin_data; 225 if ( is_plugin_paused( $plugin_file ) ) { 226 $plugins['paused'][ $plugin_file ] = $plugin_data; 227 } 221 228 } else { 222 229 if ( isset( $recently_activated[ $plugin_file ] ) ) { 223 230 // Populate the recently activated list with plugins that have been recently activated … … protected function get_views() { 421 428 422 429 switch ( $type ) { 423 430 case 'all': 431 /* translators: %s: plugin count */ 424 432 $text = _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $count, 'plugins' ); 425 433 break; 426 434 case 'active': 435 /* translators: %s: plugin count */ 427 436 $text = _n( 'Active <span class="count">(%s)</span>', 'Active <span class="count">(%s)</span>', $count ); 428 437 break; 429 438 case 'recently_activated': 439 /* translators: %s: plugin count */ 430 440 $text = _n( 'Recently Active <span class="count">(%s)</span>', 'Recently Active <span class="count">(%s)</span>', $count ); 431 441 break; 432 442 case 'inactive': 443 /* translators: %s: plugin count */ 433 444 $text = _n( 'Inactive <span class="count">(%s)</span>', 'Inactive <span class="count">(%s)</span>', $count ); 434 445 break; 435 446 case 'mustuse': 447 /* translators: %s: plugin count */ 436 448 $text = _n( 'Must-Use <span class="count">(%s)</span>', 'Must-Use <span class="count">(%s)</span>', $count ); 437 449 break; 438 450 case 'dropins': 451 /* translators: %s: plugin count */ 439 452 $text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count ); 440 453 break; 454 case 'paused': 455 /* translators: %s: plugin count */ 456 $text = _n( 'Paused <span class="count">(%s)</span>', 'Paused <span class="count">(%s)</span>', $count ); 457 break; 441 458 case 'upgrade': 459 /* translators: %s: plugin count */ 442 460 $text = _n( 'Update Available <span class="count">(%s)</span>', 'Update Available <span class="count">(%s)</span>', $count ); 443 461 break; 444 462 } … … public function single_row( $item ) { 625 643 /* translators: %s: plugin name */ 626 644 $actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Deactivate' ) . '</a>'; 627 645 } 646 if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) { 647 /* translators: %s: plugin name */ 648 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume network execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume network execution' ) . '</a>'; 649 } 628 650 } else { 629 651 if ( current_user_can( 'manage_network_plugins' ) ) { 630 652 /* translators: %s: plugin name */ 631 653 $actions['activate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'activate-plugin_' . $plugin_file ) . '" class="edit" aria-label="' . esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Activate' ) . '</a>'; 632 654 } 655 if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) { 656 /* translators: %s: plugin name */ 657 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume network execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume network execution' ) . '</a>'; 658 } 633 659 if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) { 634 660 /* translators: %s: plugin name */ 635 661 $actions['delete'] = '<a href="' . wp_nonce_url( 'plugins.php?action=delete-selected&checked[]=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'bulk-plugins' ) . '" class="delete" aria-label="' . esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Delete' ) . '</a>'; … … public function single_row( $item ) { 640 666 $actions = array( 641 667 'network_active' => __( 'Network Active' ), 642 668 ); 669 if ( ! $restrict_network_only && current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) { 670 /* translators: %s: plugin name */ 671 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume execution' ) . '</a>'; 672 } 643 673 } elseif ( $restrict_network_only ) { 644 674 $actions = array( 645 675 'network_only' => __( 'Network Only' ), … … public function single_row( $item ) { 649 679 /* translators: %s: plugin name */ 650 680 $actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Deactivate' ) . '</a>'; 651 681 } 682 if ( current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) { 683 /* translators: %s: plugin name */ 684 $actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume execution' ) . '</a>'; 685 } 652 686 } else { 653 687 if ( current_user_can( 'activate_plugin', $plugin_file ) ) { 654 688 /* translators: %s: plugin name */ … … public function single_row( $item ) { 755 789 $class .= ' update'; 756 790 } 757 791 792 $paused = is_plugin_paused( $plugin_file ); 793 $paused_on_network_sites_count = $screen->in_admin( 'network' ) ? count_paused_plugin_sites_for_network( $plugin_file ) : 0; 794 if ( $paused || $paused_on_network_sites_count ) { 795 $class .= ' paused'; 796 } 797 758 798 $plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_name ); 759 799 printf( 760 800 '<tr class="%s" data-slug="%s" data-plugin="%s">', … … public function single_row( $item ) { 833 873 * @param array $plugin_data An array of plugin data. 834 874 * @param string $status Status of the plugin. Defaults are 'All', 'Active', 835 875 * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', 836 * 'Drop-ins', 'Search' .876 * 'Drop-ins', 'Search', 'Paused' 837 877 */ 838 878 $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status ); 839 879 echo implode( ' | ', $plugin_meta ); 840 880 841 echo '</div></td>'; 881 echo '</div>'; 882 883 if ( $paused || $paused_on_network_sites_count ) { 884 $notice_text = __( 'This plugin failed to load properly and was paused within the admin backend.' ); 885 if ( $screen->in_admin( 'network' ) && $paused_on_network_sites_count ) { 886 $notice_text = sprintf( 887 /* translators: %s: number of sites */ 888 _n( 'This plugin failed to load properly and was paused within the admin backend for %s site.', 'This plugin failed to load properly and was paused within the admin backend for %s sites.', $paused_on_network_sites_count ), 889 number_format_i18n( $paused_on_network_sites_count ) 890 ); 891 } 892 893 echo sprintf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text ); 894 895 $error = wp_get_plugin_error( $plugin_file ); 896 897 if ( false !== $error ) { 898 $constants = get_defined_constants( true ); 899 $constants = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal']; 900 901 foreach ( $constants as $constant => $value ) { 902 if ( 0 === strpos( $constant, 'E_' ) ) { 903 $core_errors[ $value ] = $constant; 904 } 905 } 906 907 $error['type'] = $core_errors[ $error['type'] ]; 908 909 echo sprintf( 910 '<div class="error-display"><p>%s</p></div>', 911 sprintf( 912 /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */ 913 __( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ), 914 "<code>{$error['type']}</code>", 915 "<code>{$error['line']}</code>", 916 "<code>{$error['file']}</code>", 917 "<code>{$error['message']}</code>" 918 ) 919 ); 920 } 921 } 922 923 echo '</td>'; 842 924 break; 843 925 default: 844 926 $classes = "$column_name column-$column_name $class"; … … public function single_row( $item ) { 871 953 * @param array $plugin_data An array of plugin data. 872 954 * @param string $status Status of the plugin. Defaults are 'All', 'Active', 873 955 * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', 874 * 'Drop-ins', 'Search' .956 * 'Drop-ins', 'Search', 'Paused'. 875 957 */ 876 958 do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status ); 877 959 … … public function single_row( $item ) { 887 969 * @param array $plugin_data An array of plugin data. 888 970 * @param string $status Status of the plugin. Defaults are 'All', 'Active', 889 971 * 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use', 890 * 'Drop-ins', 'Search' .972 * 'Drop-ins', 'Search', 'Paused' 891 973 */ 892 974 do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status ); 893 975 } -
src/wp-admin/includes/plugin.php
diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index c898fc5169..875c5d9e80 100644
a b function get_dropins() { 438 438 */ 439 439 function _get_dropins() { 440 440 $dropins = array( 441 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE 442 'db.php' => array( __( 'Custom database class.' ), true ), // auto on load 443 'db-error.php' => array( __( 'Custom database error message.' ), true ), // auto on error 444 'install.php' => array( __( 'Custom installation script.' ), true ), // auto on installation 445 'maintenance.php' => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance 446 'object-cache.php' => array( __( 'External object cache.' ), true ), // auto on load 441 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE 442 'db.php' => array( __( 'Custom database class.' ), true ), // auto on load 443 'db-error.php' => array( __( 'Custom database error message.' ), true ), // auto on error 444 'install.php' => array( __( 'Custom installation script.' ), true ), // auto on installation 445 'maintenance.php' => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance 446 'object-cache.php' => array( __( 'External object cache.' ), true ), // auto on load 447 'php-error.php' => array( __( 'Custom PHP error message.' ), true ), // auto on error 448 'shutdown-handler.php' => array( __( 'Custom PHP shutdown handler.' ), true ), // auto on error 447 449 ); 448 450 449 451 if ( is_multisite() ) { … … function is_plugin_inactive( $plugin ) { 496 498 return ! is_plugin_active( $plugin ); 497 499 } 498 500 501 /** 502 * Determines whether a plugin is technically active but was paused while 503 * loading. 504 * 505 * For more information on this and similar theme functions, check out 506 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ 507 * Conditional Tags} article in the Theme Developer Handbook. 508 * 509 * @since 5.1.0 510 * 511 * @param string $plugin Path to the plugin file relative to the plugins directory. 512 * @return bool True, if in the list of paused plugins. False, not in the list. 513 */ 514 function is_plugin_paused( $plugin ) { 515 if ( ! isset( $GLOBALS['_paused_plugins'] ) ) { 516 return false; 517 } 518 519 if ( ! is_plugin_active( $plugin ) && ! is_plugin_active_for_network( $plugin ) ) { 520 return false; 521 } 522 523 list( $plugin ) = explode( '/', $plugin ); 524 525 return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ); 526 } 527 528 /** 529 * Gets the error that was recorded for a paused plugin. 530 * 531 * @since 5.1.0 532 * 533 * @param string $plugin Path to the plugin file relative to the plugins 534 * directory. 535 * @return array|false Array of error information as it was returned by 536 * `error_get_last()`, or false if none was recorded. 537 */ 538 function wp_get_plugin_error( $plugin ) { 539 if ( ! isset( $GLOBALS['_paused_plugins'] ) ) { 540 return false; 541 } 542 543 list( $plugin ) = explode( '/', $plugin ); 544 545 if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) { 546 return false; 547 } 548 549 return $GLOBALS['_paused_plugins'][ $plugin ]; 550 } 551 552 /** 553 * Gets the number of sites on which a specific plugin is paused. 554 * 555 * @since 5.1.0 556 * 557 * @param string $plugin Path to the plugin file relative to the plugins directory. 558 * @return int Site count. 559 */ 560 function count_paused_plugin_sites_for_network( $plugin ) { 561 if ( ! is_multisite() ) { 562 return is_plugin_paused( $plugin ) ? 1 : 0; 563 } 564 565 list( $plugin ) = explode( '/', $plugin ); 566 567 $query_args = array( 568 'count' => true, 569 'number' => 0, 570 'network_id' => get_current_network_id(), 571 'meta_query' => array( 572 wp_paused_plugins()->get_site_meta_query_clause( $plugin ), 573 ), 574 ); 575 576 return get_sites( $query_args ); 577 } 578 499 579 /** 500 580 * Determines whether the plugin is active for the entire network. 501 581 * … … function deactivate_plugins( $plugins, $silent = false, $network_wide = null ) { 693 773 continue; 694 774 } 695 775 776 // Clean up the database before deactivating the plugin. 777 if ( is_plugin_paused( $plugin ) ) { 778 resume_plugin( $plugin ); 779 } 780 696 781 $network_deactivating = false !== $network_wide && is_plugin_active_for_network( $plugin ); 697 782 698 783 if ( ! $silent ) { … … function delete_plugins( $plugins, $deprecated = '' ) { 887 972 uninstall_plugin( $plugin_file ); 888 973 } 889 974 975 // Clean up the database before removing the plugin. 976 if ( is_plugin_paused( $plugin_file ) ) { 977 resume_plugin( $plugin_file ); 978 } 979 890 980 /** 891 981 * Fires immediately before a plugin deletion attempt. 892 982 * … … function delete_plugins( $plugins, $deprecated = '' ) { 959 1049 return true; 960 1050 } 961 1051 1052 /** 1053 * Tries to resume a single plugin. 1054 * 1055 * If a redirect was provided, we first ensure the plugin does not throw fatal 1056 * errors anymore. 1057 * 1058 * The way it works is by setting the redirection to the error before trying to 1059 * include the plugin file. If the plugin fails, then the redirection will not 1060 * be overwritten with the success message and the plugin will not be resumed. 1061 * 1062 * @since 5.1.0 1063 * 1064 * @param string $plugin Single plugin to resume. 1065 * @param string $redirect Optional. URL to redirect to. Default empty string. 1066 * @param bool $network_wide Optional. Whether to resume the plugin for the entire 1067 * network. Default false. 1068 * @return bool|WP_Error True on success, false if `$plugin` was not paused, 1069 * `WP_Error` on failure. 1070 */ 1071 function resume_plugin( $plugin, $redirect = '', $network_wide = false ) { 1072 /* 1073 * We'll override this later if the plugin could be included without 1074 * creating a fatal error. 1075 */ 1076 if ( ! empty( $redirect ) ) { 1077 wp_redirect( 1078 add_query_arg( 1079 '_error_nonce', 1080 wp_create_nonce( 'plugin-resume-error_' . $plugin ), 1081 $redirect 1082 ) 1083 ); 1084 1085 // Load the plugin to test whether it throws a fatal error. 1086 ob_start(); 1087 $plugin_path = WP_PLUGIN_DIR . '/' . $plugin; 1088 wp_register_plugin_realpath( $plugin_path ); 1089 include_once $plugin_path; 1090 ob_clean(); 1091 } 1092 1093 $result = wp_forget_extension_error( 'plugins', $plugin, $network_wide ); 1094 1095 if ( ! $result ) { 1096 return new WP_Error( 1097 'could_not_resume_plugin', 1098 __( 'Could not resume execution of the plugin.' ) 1099 ); 1100 } 1101 1102 return true; 1103 } 1104 962 1105 /** 963 1106 * Validate active plugins 964 1107 * … … function wp_add_privacy_policy_content( $plugin_name, $policy_text ) { 2066 2209 2067 2210 WP_Privacy_Policy_Content::add( $plugin_name, $policy_text ); 2068 2211 } 2212 2213 /** 2214 * Renders an admin notice in case some plugins have been paused due to errors. 2215 * 2216 * @since 5.1.0 2217 * 2218 * @return void 2219 */ 2220 function paused_plugins_notice() { 2221 if ( 'plugins.php' === $GLOBALS['pagenow'] ) { 2222 return; 2223 } 2224 2225 if ( ! current_user_can( 'deactivate_plugins' ) ) { 2226 return; 2227 } 2228 2229 if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) { 2230 return; 2231 } 2232 2233 echo sprintf( 2234 '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p>%s</p></div>', 2235 __( 'One or more plugins failed to load properly.' ), 2236 __( 'You can find more details and make changes on the Plugins screen.' ), 2237 sprintf( 2238 '<a href="%s">%s</a>', 2239 admin_url( 'plugins.php?plugin_status=paused' ), 2240 'Go to the Plugins screen' 2241 ) 2242 ); 2243 } -
src/wp-admin/plugins.php
diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php index c80e96831f..77f1b49fce 100644
a b 389 389 } 390 390 break; 391 391 392 case 'resume': 393 if ( ! current_user_can( 'resume_plugin', $plugin ) ) { 394 wp_die( __( 'Sorry, you are not allowed to resume execution of this plugin.' ) ); 395 } 396 397 if ( is_multisite() && ! is_network_admin() && is_network_only_plugin( $plugin ) ) { 398 wp_redirect( self_admin_url( "plugins.php?plugin_status=$status&paged=$page&s=$s" ) ); 399 exit; 400 } 401 402 check_admin_referer( 'resume-plugin_' . $plugin ); 403 404 $result = resume_plugin( $plugin, self_admin_url( 'plugins.php?error=resuming' ), is_network_admin() ); 405 406 if ( is_wp_error( $result ) ) { 407 wp_die( $result ); 408 } 409 410 wp_redirect( self_admin_url( "plugins.php?resume=true&plugin_status=$status&paged=$page&s=$s" ) ); 411 exit; 412 392 413 default: 393 414 if ( isset( $_POST['checked'] ) ) { 394 415 check_admin_referer( 'bulk-plugins' ); … … 488 509 $_GET['charsout'] 489 510 ); 490 511 $errmsg .= ' ' . __( 'If you notice “headers already sent” messages, problems with syndication feeds or other issues, try deactivating or removing this plugin.' ); 512 } elseif ( 'resuming' === $_GET['error'] ) { 513 $errmsg = __( 'Plugin could not be resumed because it triggered a <strong>fatal error</strong>.' ); 491 514 } else { 492 515 $errmsg = __( 'Plugin could not be activated because it triggered a <strong>fatal error</strong>.' ); 493 516 } … … 541 564 <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Selected plugins <strong>deactivated</strong>.' ); ?></p></div> 542 565 <?php elseif ( 'update-selected' == $action ) : ?> 543 566 <div id="message" class="updated notice is-dismissible"><p><?php _e( 'All selected plugins are up to date.' ); ?></p></div> 567 <?php elseif ( isset( $_GET['resume'] ) ) : ?> 568 <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Execution of plugin <strong>resumed</strong>.' ); ?></p></div> 544 569 <?php endif; ?> 545 570 546 571 <div class="wrap"> -
src/wp-includes/capabilities.php
diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php index 375648e768..768234ed9e 100644
a b function map_meta_cap( $cap, $user_id ) { 464 464 } 465 465 } 466 466 break; 467 case 'resume_plugin': 468 // Even in a multisite, regular administrators should be able to resume a plugin. 469 $caps[] = 'activate_plugins'; 470 break; 467 471 case 'delete_user': 468 472 case 'delete_users': 469 473 // If multisite only super admins can delete users. -
new file src/wp-includes/class-wp-paused-extensions-storage.php
diff --git a/src/wp-includes/class-wp-paused-extensions-storage.php b/src/wp-includes/class-wp-paused-extensions-storage.php new file mode 100644 index 0000000000..1bff89d7b8
- + 1 <?php 2 /** 3 * Error Protection API: WP_Paused_Extensions_Storage class 4 * 5 * @package WordPress 6 * @since 5.1.0 7 */ 8 9 /** 10 * Core class used for storing paused extensions. 11 * 12 * @since 5.1.0 13 */ 14 class WP_Paused_Extensions_Storage { 15 16 /** 17 * Option name for storing paused extensions. 18 * 19 * @since 5.1.0 20 * @var string 21 */ 22 protected $option_name; 23 24 /** 25 * Prefix for paused extensions stored as site metadata. 26 * 27 * @since 5.1.0 28 * @var string 29 */ 30 protected $meta_prefix; 31 32 /** 33 * Constructor. 34 * 35 * @since 5.1.0 36 * 37 * @param string $option_name Option name for storing paused extensions. 38 * @param string $meta_prefix Prefix for paused extensions stored as site metadata. 39 */ 40 public function __construct( $option_name, $meta_prefix ) { 41 $this->option_name = $option_name; 42 $this->meta_prefix = $meta_prefix; 43 } 44 45 /** 46 * Sets an extension error. 47 * 48 * @since 5.1.0 49 * 50 * @param string $extension Plugin or theme directory name. 51 * @param array $error { 52 * Error that was triggered. 53 * 54 * @type string $type The error type. 55 * @type string $file The name of the file in which the error occurred. 56 * @type string $line The line number in which the error occurred. 57 * @type string $message The error message. 58 * } 59 * @return bool True on success, false on failure. 60 */ 61 public function set( $extension, $error ) { 62 if ( ! $this->is_api_loaded() ) { 63 return false; 64 } 65 66 if ( is_multisite() && is_site_meta_supported() ) { 67 // Do not update if the error is already stored. 68 if ( get_site_meta( get_current_blog_id(), $this->meta_prefix . $extension, true ) === $error ) { 69 return true; 70 } 71 72 return (bool) update_site_meta( get_current_blog_id(), $this->meta_prefix . $extension, $error ); 73 } 74 75 $paused_extensions = $this->get_all(); 76 77 // Do not update if the error is already stored. 78 if ( isset( $paused_extensions[ $extension ] ) && $paused_extensions[ $extension ] === $error ) { 79 return true; 80 } 81 82 $paused_extensions[ $extension ] = $error; 83 84 return update_option( $this->option_name, $paused_extensions ); 85 } 86 87 /** 88 * Unsets an extension error. 89 * 90 * @since 5.1.0 91 * 92 * @param string $extension Plugin or theme directory name. 93 * @return bool True on success, false on failure. 94 */ 95 public function unset( $extension ) { 96 if ( ! $this->is_api_loaded() ) { 97 return false; 98 } 99 100 if ( is_multisite() && is_site_meta_supported() ) { 101 // Do not delete if no error is stored. 102 if ( get_site_meta( get_current_blog_id(), $this->meta_prefix . $extension ) === array() ) { 103 return true; 104 } 105 106 return (bool) delete_site_meta( get_current_blog_id(), $this->meta_prefix . $extension ); 107 } 108 109 $paused_extensions = $this->get_all(); 110 111 // Do not delete if no error is stored. 112 if ( ! isset( $paused_extensions[ $extension ] ) ) { 113 return true; 114 } 115 116 // Clean up the entire option if we're unsetting the only error. 117 if ( count( $paused_extensions ) === 1 ) { 118 return delete_option( $this->option_name ); 119 } 120 121 unset( $paused_extensions[ $extension ] ); 122 123 return update_option( $this->option_name, $paused_extensions ); 124 } 125 126 /** 127 * Gets the paused extensions with their errors. 128 * 129 * @since 5.1.0 130 * 131 * @return array Associative array of $extension => $error pairs. 132 */ 133 public function get_all() { 134 if ( ! $this->is_api_loaded() ) { 135 return array(); 136 } 137 138 if ( is_multisite() && is_site_meta_supported() ) { 139 $site_metadata = get_site_meta( get_current_blog_id() ); 140 141 $paused_extensions = array(); 142 foreach ( $site_metadata as $meta_key => $meta_values ) { 143 if ( 0 !== strpos( $meta_key, $this->meta_prefix ) ) { 144 continue; 145 } 146 147 $error = maybe_unserialize( array_shift( $meta_values ) ); 148 149 $paused_extensions[ substr( $meta_key, strlen( $this->meta_prefix ) ) ] = $error; 150 } 151 152 return $paused_extensions; 153 } 154 155 return (array) get_option( $this->option_name, array() ); 156 } 157 158 /** 159 * Gets the site meta query clause for querying sites with paused extensions. 160 * 161 * @since 5.1.0 162 * 163 * @param string $extension Optional. Plugin or theme directory name. If not provided, the clause 164 * returned will query sites where any extension of the same name is paused. 165 * @return array A single clause to add to a meta query. 166 */ 167 public function get_site_meta_query_clause( $extension = '' ) { 168 if ( ! empty( $extension ) ) { 169 return array( 170 'key' => $this->meta_prefix . $extension, 171 'compare_key' => '=', 172 ); 173 } 174 175 return array( 176 'key' => $this->meta_prefix . '%', 177 'compare_key' => 'LIKE', 178 ); 179 } 180 181 /** 182 * Checks whether the underlying API to store paused extensions is loaded. 183 * 184 * @since 5.1.0 185 * 186 * @return bool True if the API is loaded, false otherwise. 187 */ 188 protected function is_api_loaded() { 189 if ( is_multisite() ) { 190 return function_exists( 'is_site_meta_supported' ) && function_exists( 'get_site_meta' ); 191 } 192 193 return function_exists( 'get_option' ); 194 } 195 } -
new file src/wp-includes/error-protection.php
diff --git a/src/wp-includes/error-protection.php b/src/wp-includes/error-protection.php new file mode 100644 index 0000000000..27a27d0594
- + 1 <?php 2 /** 3 * Error Protection API: Functions 4 * 5 * @package WordPress 6 * @since 5.1.0 7 */ 8 9 /** 10 * Gets the instance for storing paused plugins. 11 * 12 * @since 5.1.0 13 * 14 * @return WP_Paused_Extensions_Storage Paused plugins storage. 15 */ 16 function wp_paused_plugins() { 17 static $wp_paused_plugins_storage = null; 18 19 if ( null === $wp_paused_plugins_storage ) { 20 $wp_paused_plugins_storage = new WP_Paused_Extensions_Storage( 'paused_plugins', 'paused_plugin_' ); 21 } 22 23 return $wp_paused_plugins_storage; 24 } 25 26 /** 27 * Gets the instance for storing paused themes. 28 * 29 * @since 5.1.0 30 * 31 * @return WP_Paused_Extensions_Storage Paused themes storage. 32 */ 33 function wp_paused_themes() { 34 static $wp_paused_themes_storage = null; 35 36 if ( null === $wp_paused_themes_storage ) { 37 $wp_paused_themes_storage = new WP_Paused_Extensions_Storage( 'paused_themes', 'paused_theme_' ); 38 } 39 40 return $wp_paused_themes_storage; 41 } 42 43 /** 44 * Records the extension error as a database option. 45 * 46 * @since 5.1.0 47 * 48 * @global array $wp_theme_directories 49 * 50 * @param array $error Error that was triggered. 51 * @return bool Whether the error was correctly recorded. 52 */ 53 function wp_record_extension_error( $error ) { 54 global $wp_theme_directories; 55 56 $error_file = wp_normalize_path( $error['file'] ); 57 $wp_plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); 58 59 if ( 0 === strpos( $error_file, $wp_plugin_dir ) ) { 60 $callback = 'wp_paused_plugins'; 61 $path = str_replace( $wp_plugin_dir . '/', '', $error_file ); 62 } else { 63 foreach ( $wp_theme_directories as $theme_directory ) { 64 $theme_directory = wp_normalize_path( $theme_directory ); 65 if ( 0 === strpos( $error_file, $theme_directory ) ) { 66 $callback = 'wp_paused_themes'; 67 $path = str_replace( $theme_directory . '/', '', $error_file ); 68 } 69 } 70 } 71 72 if ( empty( $callback ) || empty( $path ) ) { 73 return false; 74 } 75 76 $parts = explode( '/', $path ); 77 $extension = array_shift( $parts ); 78 79 return call_user_func( $callback )->set( $extension, $error ); 80 } 81 82 /** 83 * Forgets a previously recorded extension error again. 84 * 85 * @since 5.1.0 86 * 87 * @param string $type Type of the extension. 88 * @param string $extension Relative path of the extension. 89 * @param bool $network_wide Optional. Whether to resume the plugin for the entire 90 * network. Default false. 91 * @return bool Whether the extension error was successfully forgotten. 92 */ 93 function wp_forget_extension_error( $type, $extension, $network_wide = false ) { 94 switch ( $type ) { 95 case 'plugins': 96 $callback = 'wp_paused_plugins'; 97 list( $extension ) = explode( '/', $extension ); 98 break; 99 case 'themes': 100 $callback = 'wp_paused_themes'; 101 list( $extension ) = explode( '/', $extension ); 102 break; 103 } 104 105 if ( empty( $callback ) || empty( $extension ) ) { 106 return false; 107 } 108 109 // Handle manually since the regular APIs do not expose this functionality. 110 if ( $network_wide ) { 111 $site_meta_query_clause = call_user_func( $callback )->get_site_meta_query_clause( $extension ); 112 return delete_metadata( 'blog', 0, $site_meta_query_clause['key'], '', true ); 113 } 114 115 return call_user_func( $callback )->unset( $extension ); 116 } 117 118 /** 119 * Determines whether we are dealing with an error that WordPress should handle 120 * in order to protect the admin backend against WSODs. 121 * 122 * @param array $error Error information retrieved from error_get_last(). 123 * 124 * @return bool Whether WordPress should handle this error. 125 */ 126 function wp_should_handle_error( $error ) { 127 if ( ! isset( $error['type'] ) ) { 128 return false; 129 } 130 131 $error_types_to_handle = array( 132 E_ERROR, 133 E_PARSE, 134 E_USER_ERROR, 135 E_COMPILE_ERROR, 136 E_RECOVERABLE_ERROR, 137 ); 138 139 return in_array( $error['type'], $error_types_to_handle, true ); 140 } 141 142 /** 143 * Wraps the shutdown handler function so it can be made pluggable at a later 144 * stage. 145 * 146 * @since 5.1.0 147 * 148 * @return void 149 */ 150 function wp_shutdown_handler_wrapper() { 151 if ( defined( 'WP_EXECUTION_SUCCEEDED' ) && WP_EXECUTION_SUCCEEDED ) { 152 return; 153 } 154 155 // Load the pluggable shutdown handler in case we found one. 156 if ( function_exists( 'wp_handle_shutdown' ) ) { 157 $stop_propagation = false; 158 159 try { 160 $stop_propagation = (bool) wp_handle_shutdown(); 161 } catch ( Exception $exception ) { 162 // Catch exceptions and remain silent. 163 } 164 165 if ( $stop_propagation ) { 166 return; 167 } 168 } 169 170 $error = error_get_last(); 171 172 // No error, just skip the error handling code. 173 if ( null === $error ) { 174 return; 175 } 176 177 // Bail early if this error should not be handled. 178 if ( ! wp_should_handle_error( $error ) ) { 179 return; 180 } 181 182 try { 183 // Persist the detected error. 184 wp_record_extension_error( $error ); 185 186 /* 187 * If we happen to be on a protected endpoint, we try to redirect to 188 * catch multiple errors in one go. 189 */ 190 if ( is_protected_endpoint() ) { 191 /* 192 * Pluggable is usually loaded after plugins, so we manually 193 * include it here for redirection functionality. 194 */ 195 if ( ! function_exists( 'wp_redirect' ) ) { 196 include ABSPATH . WPINC . '/pluggable.php'; 197 } 198 199 $scheme = is_ssl() ? 'https://' : 'http://'; 200 201 $url = "{$scheme}{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; 202 wp_redirect( $url ); 203 exit; 204 } 205 206 // Load custom PHP error template, if present. 207 $php_error_pluggable = WP_CONTENT_DIR . '/php-error.php'; 208 if ( is_readable( $php_error_pluggable ) ) { 209 /* 210 * This drop-in should control the HTTP status code and print the 211 * HTML markup indicating that a PHP error occurred. Alternatively, 212 * `wp_die()` can be used. 213 */ 214 require_once $php_error_pluggable; 215 die(); 216 } 217 218 // Otherwise, fall back to a default wp_die() message. 219 $message = sprintf( 220 '<p>%s</p>', 221 __( 'The site is experiencing technical difficulties.' ) 222 ); 223 224 if ( function_exists( 'get_admin_url' ) ) { 225 $message .= sprintf( 226 '<hr><p><em>%s <a href="%s">%s</a></em></p>', 227 __( 'Are you the site owner?' ), 228 get_admin_url(), 229 __( 'Log into the admin backend to fix this.' ) 230 ); 231 } 232 233 if ( function_exists( 'apply_filters' ) ) { 234 /** 235 * Filters the message that the default PHP error page displays. 236 * 237 * @since 5.1.0 238 * 239 * @param string $message HTML error message to display. 240 */ 241 $message = apply_filters( 'wp_technical_issues_display', $message ); 242 } 243 244 wp_die( $message, '', 500 ); 245 246 } catch ( Exception $exception ) { 247 // Catch exceptions and remain silent. 248 } 249 } 250 251 /** 252 * Registers the WordPress premature shutdown handler. 253 * 254 * @since 5.1.0 255 * 256 * @return void 257 */ 258 function wp_register_premature_shutdown_handler() { 259 register_shutdown_function( 'wp_shutdown_handler_wrapper' ); 260 } -
src/wp-includes/load.php
diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 79e445021b..27b4f081c6 100644
a b function wp_get_active_and_valid_plugins() { 687 687 $plugins[] = WP_PLUGIN_DIR . '/' . $plugin; 688 688 } 689 689 } 690 691 /* 692 * Remove plugins from the list of active plugins when we're on an endpoint 693 * that should be protected against WSODs and the plugin is paused. 694 */ 695 if ( is_protected_endpoint() ) { 696 $plugins = wp_skip_paused_plugins( $plugins ); 697 } 698 699 return $plugins; 700 } 701 702 /** 703 * Filters a given list of plugins, removing any paused plugins from it. 704 * 705 * @since 5.1.0 706 * 707 * @param array $plugins List of absolute plugin main file paths. 708 * @return array Filtered value of $plugins, without any paused plugins. 709 */ 710 function wp_skip_paused_plugins( array $plugins ) { 711 $paused_plugins = wp_paused_plugins()->get_all(); 712 713 if ( empty( $paused_plugins ) ) { 714 return $plugins; 715 } 716 717 foreach ( $plugins as $index => $plugin ) { 718 list( $plugin ) = explode( '/', plugin_basename( $plugin ) ); 719 720 if ( array_key_exists( $plugin, $paused_plugins ) ) { 721 unset( $plugins[ $index ] ); 722 723 // Store list of paused plugins for displaying an admin notice. 724 $GLOBALS['_paused_plugins'][ $plugin ] = $paused_plugins[ $plugin ]; 725 } 726 } 727 690 728 return $plugins; 691 729 } 692 730 … … function wp_doing_ajax() { 1154 1192 return apply_filters( 'wp_doing_ajax', defined( 'DOING_AJAX' ) && DOING_AJAX ); 1155 1193 } 1156 1194 1195 /** 1196 * Determines whether we are currently on an endpoint that should be protected 1197 * against WSODs. 1198 * 1199 * @since 5.1.0 1200 * 1201 * @return bool True if the current endpoint should be protected. 1202 */ 1203 function is_protected_endpoint() { 1204 // Protect login pages. 1205 if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) { 1206 return true; 1207 } 1208 1209 // Protect the admin backend. 1210 if ( is_admin() && ! wp_doing_ajax() ) { 1211 return true; 1212 } 1213 1214 // Protect AJAX actions that could help resolve a fatal error should be available. 1215 if ( wp_doing_ajax() && is_protected_ajax_action() ) { 1216 return true; 1217 } 1218 1219 /** 1220 * Filters whether the current request is against a protected endpoint. 1221 * 1222 * This filter is only fired when an endpoint is requested which is not already protected by 1223 * WordPress core. As such, it exclusively allows providing further protected endpoints in 1224 * addition to the admin backend, login pages and protected AJAX actions. 1225 * 1226 * @since 5.1.0 1227 * 1228 * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected. 1229 * Default false. 1230 */ 1231 return (bool) apply_filters( 'is_protected_endpoint', false ); 1232 } 1233 1234 /** 1235 * Determines whether we are currently handling an AJAX action that should be 1236 * protected against WSODs. 1237 * 1238 * @since 5.1.0 1239 * 1240 * @return bool True if the current AJAX action should be protected. 1241 */ 1242 function is_protected_ajax_action() { 1243 $actions_to_protect = array( 1244 'edit-theme-plugin-file', // Saving changes in the core code editor. 1245 'heartbeat', // Keep the heart beating. 1246 'install-plugin', // Installing a new plugin. 1247 'install-theme', // Installing a new theme. 1248 'search-plugins', // Searching in the list of plugins. 1249 'search-install-plugins', // Searching for a plugin in the plugin install screen. 1250 'update-plugin', // Update an existing plugin. 1251 'update-theme', // Update an existing theme. 1252 ); 1253 1254 if ( ! wp_doing_ajax() ) { 1255 return false; 1256 } 1257 1258 if ( ! isset( $_REQUEST['action'] ) ) { 1259 return false; 1260 } 1261 1262 if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) { 1263 return false; 1264 } 1265 1266 return true; 1267 } 1268 1157 1269 /** 1158 1270 * Determines whether the current request is a WordPress cron request. 1159 1271 * -
src/wp-includes/ms-load.php
diff --git a/src/wp-includes/ms-load.php b/src/wp-includes/ms-load.php index 932ae76ca5..4f630cce27 100644
a b function wp_get_active_network_plugins() { 52 52 $plugins[] = WP_PLUGIN_DIR . '/' . $plugin; 53 53 } 54 54 } 55 56 /* 57 * Remove plugins from the list of active plugins when we're on an endpoint 58 * that should be protected against WSODs and the plugin is paused. 59 */ 60 if ( is_protected_endpoint() ) { 61 $plugins = wp_skip_paused_plugins( $plugins ); 62 } 63 55 64 return $plugins; 56 65 } 57 66 -
src/wp-settings.php
diff --git a/src/wp-settings.php b/src/wp-settings.php index eb632238f0..43a904f9b4 100644
a b 17 17 18 18 // Include files required for initialization. 19 19 require( ABSPATH . WPINC . '/load.php' ); 20 require( ABSPATH . WPINC . '/class-wp-paused-extensions-storage.php' ); 21 require( ABSPATH . WPINC . '/error-protection.php' ); 20 22 require( ABSPATH . WPINC . '/default-constants.php' ); 21 23 require_once( ABSPATH . WPINC . '/plugin.php' ); 22 24 25 // Make sure we register the premature shutdown handler as soon as possible. 26 wp_register_premature_shutdown_handler(); 27 23 28 /* 24 29 * These can't be directly globalized in version.php. When updating, 25 30 * we're including version.php from another installation and don't want … … 40 45 // Set initial default constants including WP_MEMORY_LIMIT, WP_MAX_MEMORY_LIMIT, WP_DEBUG, SCRIPT_DEBUG, WP_CONTENT_DIR and WP_CACHE. 41 46 wp_initial_constants(); 42 47 48 /* 49 * Allow an optional shutdown handler to be included through a pluggable file. 50 * This file should register a function `wp_handle_shutdown( $context )` that 51 * returns a boolean value. If the return value evaluates to false, the default 52 * shutdown handler will not be executed. 53 */ 54 if ( is_readable( WP_CONTENT_DIR . '/shutdown-handler.php' ) ) { 55 include WP_CONTENT_DIR . '/shutdown-handler.php'; 56 } 57 43 58 // Check for the required PHP version and for the MySQL extension or a database drop-in. 44 59 wp_check_php_mysql_versions(); 45 60 … … 482 497 * @since 3.0.0 483 498 */ 484 499 do_action( 'wp_loaded' ); 500 501 /* 502 * Store the fact that we could successfully execute the entire WordPress 503 * lifecycle. This is used to skip the premature shutdown handler, as it cannot 504 * be unregistered. 505 */ 506 if ( ! defined( 'WP_EXECUTION_SUCCEEDED' ) ) { 507 define( 'WP_EXECUTION_SUCCEEDED', true ); 508 }