WordPress.org

Make WordPress Core

Ticket #44458: 44458.12.diff

File 44458.12.diff, 58.0 KB (added by flixos90, 20 months 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 992f69c2f8..6dcac9d56f 100644
    a b ul.cat-checklist { 
    13011301        text-decoration: underline;
    13021302}
    13031303
     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
    13041329.plugin-card .update-now:before {
    13051330        color: #f56e28;
    13061331        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 a1efcc62f4..a25eda7f42 100644
    a b  
    123123add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called.
    124124
    125125add_action( 'admin_notices', 'update_nag', 3 );
     126add_action( 'admin_notices', 'paused_plugins_notice', 5 );
     127add_action( 'admin_notices', 'paused_themes_notice', 5 );
    126128add_action( 'admin_notices', 'maintenance_nag', 10 );
    127129
    128130add_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..1da23f5f4e 100644
    a b public function __construct( $args = array() ) { 
    4040                );
    4141
    4242                $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' ) ) ) {
    4444                        $status = $_REQUEST['plugin_status'];
    4545                }
    4646
    public function prepare_items() { 
    9999                        'upgrade'            => array(),
    100100                        'mustuse'            => array(),
    101101                        'dropins'            => array(),
     102                        'paused'             => array(),
    102103                );
    103104
    104105                $screen = $this->screen;
    public function prepare_items() { 
    209210                                if ( $show_network_active ) {
    210211                                        // On the non-network screen, show network-active plugins if allowed
    211212                                        $plugins['active'][ $plugin_file ] = $plugin_data;
     213                                        if ( is_plugin_paused( $plugin_file ) ) {
     214                                                $plugins['paused'][ $plugin_file ] = $plugin_data;
     215                                        }
    212216                                } else {
    213217                                        // On the non-network screen, filter out network-active plugins
    214218                                        unset( $plugins['all'][ $plugin_file ] );
    public function prepare_items() { 
    218222                                // On the non-network screen, populate the active list with plugins that are individually activated
    219223                                // On the network-admin screen, populate the active list with plugins that are network activated
    220224                                $plugins['active'][ $plugin_file ] = $plugin_data;
     225                                if ( is_plugin_paused( $plugin_file ) ) {
     226                                        $plugins['paused'][ $plugin_file ] = $plugin_data;
     227                                }
    221228                        } else {
    222229                                if ( isset( $recently_activated[ $plugin_file ] ) ) {
    223230                                        // Populate the recently activated list with plugins that have been recently activated
    protected function get_views() { 
    421428
    422429                        switch ( $type ) {
    423430                                case 'all':
     431                                        /* translators: %s: plugin count */
    424432                                        $text = _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $count, 'plugins' );
    425433                                        break;
    426434                                case 'active':
     435                                        /* translators: %s: plugin count */
    427436                                        $text = _n( 'Active <span class="count">(%s)</span>', 'Active <span class="count">(%s)</span>', $count );
    428437                                        break;
    429438                                case 'recently_activated':
     439                                        /* translators: %s: plugin count */
    430440                                        $text = _n( 'Recently Active <span class="count">(%s)</span>', 'Recently Active <span class="count">(%s)</span>', $count );
    431441                                        break;
    432442                                case 'inactive':
     443                                        /* translators: %s: plugin count */
    433444                                        $text = _n( 'Inactive <span class="count">(%s)</span>', 'Inactive <span class="count">(%s)</span>', $count );
    434445                                        break;
    435446                                case 'mustuse':
     447                                        /* translators: %s: plugin count */
    436448                                        $text = _n( 'Must-Use <span class="count">(%s)</span>', 'Must-Use <span class="count">(%s)</span>', $count );
    437449                                        break;
    438450                                case 'dropins':
     451                                        /* translators: %s: plugin count */
    439452                                        $text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count );
    440453                                        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;
    441458                                case 'upgrade':
     459                                        /* translators: %s: plugin count */
    442460                                        $text = _n( 'Update Available <span class="count">(%s)</span>', 'Update Available <span class="count">(%s)</span>', $count );
    443461                                        break;
    444462                        }
    public function single_row( $item ) { 
    625643                                                /* translators: %s: plugin name */
    626644                                                $actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Deactivate' ) . '</a>';
    627645                                        }
     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&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;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                                        }
    628650                                } else {
    629651                                        if ( current_user_can( 'manage_network_plugins' ) ) {
    630652                                                /* translators: %s: plugin name */
    631653                                                $actions['activate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'activate-plugin_' . $plugin_file ) . '" class="edit" aria-label="' . esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Activate' ) . '</a>';
    632654                                        }
     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&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;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                                        }
    633659                                        if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) {
    634660                                                /* translators: %s: plugin name */
    635661                                                $actions['delete'] = '<a href="' . wp_nonce_url( 'plugins.php?action=delete-selected&amp;checked[]=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;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 ) { 
    640666                                        $actions = array(
    641667                                                'network_active' => __( 'Network Active' ),
    642668                                        );
     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&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume execution' ) . '</a>';
     672                                        }
    643673                                } elseif ( $restrict_network_only ) {
    644674                                        $actions = array(
    645675                                                'network_only' => __( 'Network Only' ),
    public function single_row( $item ) { 
    649679                                                /* translators: %s: plugin name */
    650680                                                $actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Deactivate' ) . '</a>';
    651681                                        }
     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&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume execution' ) . '</a>';
     685                                        }
    652686                                } else {
    653687                                        if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
    654688                                                /* translators: %s: plugin name */
    public function single_row( $item ) { 
    755789                        $class .= ' update';
    756790                }
    757791
     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
    758798                $plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_name );
    759799                printf(
    760800                        '<tr class="%s" data-slug="%s" data-plugin="%s">',
    public function single_row( $item ) { 
    833873                                         * @param array    $plugin_data An array of plugin data.
    834874                                         * @param string   $status      Status of the plugin. Defaults are 'All', 'Active',
    835875                                         *                              'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    836                                          *                              'Drop-ins', 'Search'.
     876                                         *                              'Drop-ins', 'Search', 'Paused'.
    837877                                         */
    838878                                        $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );
    839879                                        echo implode( ' | ', $plugin_meta );
    840880
    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                                                printf( '<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                                                        printf(
     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>';
    842924                                        break;
    843925                                default:
    844926                                        $classes = "$column_name column-$column_name $class";
    public function single_row( $item ) { 
    871953                 * @param array  $plugin_data An array of plugin data.
    872954                 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    873955                 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    874                  *                            'Drop-ins', 'Search'.
     956                 *                            'Drop-ins', 'Search', 'Paused'.
    875957                 */
    876958                do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
    877959
    public function single_row( $item ) { 
    887969                 * @param array  $plugin_data An array of plugin data.
    888970                 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    889971                 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    890                  *                            'Drop-ins', 'Search'.
     972                 *                            'Drop-ins', 'Search', 'Paused'.
    891973                 */
    892974                do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
    893975        }
  • src/wp-admin/includes/plugin.php

    diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php
    index c898fc5169..6041c74333 100644
    a b function get_dropins() { 
    438438 */
    439439function _get_dropins() {
    440440        $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
    447449        );
    448450
    449451        if ( is_multisite() ) {
    function is_plugin_inactive( $plugin ) { 
    496498        return ! is_plugin_active( $plugin );
    497499}
    498500
     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 */
     514function 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 */
     538function 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 */
     560function 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
    499579/**
    500580 * Determines whether the plugin is active for the entire network.
    501581 *
    function deactivate_plugins( $plugins, $silent = false, $network_wide = null ) { 
    693773                        continue;
    694774                }
    695775
     776                // Clean up the database before deactivating the plugin.
     777                if ( is_plugin_paused( $plugin ) ) {
     778                        resume_plugin( $plugin );
     779                }
     780
    696781                $network_deactivating = false !== $network_wide && is_plugin_active_for_network( $plugin );
    697782
    698783                if ( ! $silent ) {
    function delete_plugins( $plugins, $deprecated = '' ) { 
    887972                        uninstall_plugin( $plugin_file );
    888973                }
    889974
     975                // Clean up the database before removing the plugin.
     976                if ( is_plugin_paused( $plugin_file ) ) {
     977                        resume_plugin( $plugin_file );
     978                }
     979
    890980                /**
    891981                 * Fires immediately before a plugin deletion attempt.
    892982                 *
    function delete_plugins( $plugins, $deprecated = '' ) { 
    9591049        return true;
    9601050}
    9611051
     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 */
     1071function 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_sandbox_scrape( $plugin );
     1088                ob_clean();
     1089        }
     1090
     1091        $result = wp_forget_extension_error( 'plugins', $plugin, $network_wide );
     1092
     1093        if ( ! $result ) {
     1094                return new WP_Error(
     1095                        'could_not_resume_plugin',
     1096                        __( 'Could not resume execution of the plugin.' )
     1097                );
     1098        }
     1099
     1100        return true;
     1101}
     1102
    9621103/**
    9631104 * Validate active plugins
    9641105 *
    function wp_add_privacy_policy_content( $plugin_name, $policy_text ) { 
    20662207
    20672208        WP_Privacy_Policy_Content::add( $plugin_name, $policy_text );
    20682209}
     2210
     2211/**
     2212 * Renders an admin notice in case some plugins have been paused due to errors.
     2213 *
     2214 * @since 5.1.0
     2215 */
     2216function paused_plugins_notice() {
     2217        if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
     2218                return;
     2219        }
     2220
     2221        if ( ! current_user_can( 'deactivate_plugins' ) ) {
     2222                return;
     2223        }
     2224
     2225        if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) {
     2226                return;
     2227        }
     2228
     2229        printf(
     2230                '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p>%s</p></div>',
     2231                __( 'One or more plugins failed to load properly.' ),
     2232                __( 'You can find more details and make changes on the Plugins screen.' ),
     2233                sprintf(
     2234                        '<a href="%s">%s</a>',
     2235                        admin_url( 'plugins.php?plugin_status=paused' ),
     2236                        'Go to the Plugins screen'
     2237                )
     2238        );
     2239}
  • src/wp-admin/includes/theme.php

    diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php
    index f67886aaf1..c00a458891 100644
    a b function customize_themes_print_templates() { 
    763763        </script>
    764764        <?php
    765765}
     766
     767/**
     768 * Determines whether a theme is technically active but was paused while
     769 * loading.
     770 *
     771 * For more information on this and similar theme functions, check out
     772 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
     773 * Conditional Tags} article in the Theme Developer Handbook.
     774 *
     775 * @since 5.1.0
     776 *
     777 * @param string $theme Path to the theme directory relative to the themes directory.
     778 * @return bool True, if in the list of paused themes. False, not in the list.
     779 */
     780function is_theme_paused( $theme ) {
     781        if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
     782                return false;
     783        }
     784
     785        if ( $theme !== get_stylesheet() && $theme !== get_template() ) {
     786                return false;
     787        }
     788
     789        return array_key_exists( $theme, $GLOBALS['_paused_themes'] );
     790}
     791
     792/**
     793 * Gets the error that was recorded for a paused theme.
     794 *
     795 * @since 5.1.0
     796 *
     797 * @param string $theme Path to the theme directory relative to the themes
     798 *                      directory.
     799 * @return array|false Array of error information as it was returned by
     800 *                     `error_get_last()`, or false if none was recorded.
     801 */
     802function wp_get_theme_error( $theme ) {
     803        if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
     804                return false;
     805        }
     806
     807        if ( ! array_key_exists( $theme, $GLOBALS['_paused_themes'] ) ) {
     808                return false;
     809        }
     810
     811        return $GLOBALS['_paused_themes'][ $theme ];
     812}
     813
     814/**
     815 * Gets the number of sites on which a specific theme is paused.
     816 *
     817 * @since 5.1.0
     818 *
     819 * @param string $theme Path to the theme directory relative to the themes directory.
     820 * @return int Site count.
     821 */
     822function count_paused_theme_sites_for_network( $theme ) {
     823        if ( ! is_multisite() ) {
     824                return is_theme_paused( $theme ) ? 1 : 0;
     825        }
     826
     827        $query_args = array(
     828                'count'      => true,
     829                'number'     => 0,
     830                'network_id' => get_current_network_id(),
     831                'meta_query' => array(
     832                        wp_paused_themes()->get_site_meta_query_clause( $theme ),
     833                ),
     834        );
     835
     836        return get_sites( $query_args );
     837}
     838
     839/**
     840 * Tries to resume a single theme.
     841 *
     842 * @since 5.1.0
     843 *
     844 * @param string $theme Single theme to resume.
     845 * @return bool|WP_Error True on success, false if `$theme` was not paused,
     846 *                       `WP_Error` on failure.
     847 */
     848function resume_theme( $theme ) {
     849        $result = wp_forget_extension_error( 'themes', $theme );
     850
     851        if ( ! $result ) {
     852                return new WP_Error(
     853                        'could_not_resume_theme',
     854                        __( 'Could not resume execution of the theme.' )
     855                );
     856        }
     857
     858        return true;
     859}
     860
     861/**
     862 * Renders an admin notice in case some themes have been paused due to errors.
     863 *
     864 * @since 5.1.0
     865 */
     866function paused_themes_notice() {
     867        if ( 'themes.php' === $GLOBALS['pagenow'] ) {
     868                return;
     869        }
     870
     871        if ( ! current_user_can( 'switch_themes' ) ) {
     872                return;
     873        }
     874
     875        if ( ! isset( $GLOBALS['_paused_themes'] ) || empty( $GLOBALS['_paused_themes'] ) ) {
     876                return;
     877        }
     878
     879        printf(
     880                '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p>%s</p></div>',
     881                __( 'One or more themes failed to load properly.' ),
     882                __( 'You can find more details and make changes on the Themes screen.' ),
     883                sprintf(
     884                        '<a href="%s">%s</a>',
     885                        admin_url( 'themes.php' ),
     886                        'Go to the Themes screen'
     887                )
     888        );
     889}
  • 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  
    389389                        }
    390390                        break;
    391391
     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
    392413                default:
    393414                        if ( isset( $_POST['checked'] ) ) {
    394415                                check_admin_referer( 'bulk-plugins' );
     
    488509                        $_GET['charsout']
    489510                );
    490511                $errmsg .= ' ' . __( 'If you notice &#8220;headers already sent&#8221; 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>.' );
    491514        } else {
    492515                $errmsg = __( 'Plugin could not be activated because it triggered a <strong>fatal error</strong>.' );
    493516        }
     
    541564        <div id="message" class="updated notice is-dismissible"><p><?php _e( 'Selected plugins <strong>deactivated</strong>.' ); ?></p></div>
    542565<?php elseif ( 'update-selected' == $action ) : ?>
    543566        <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>
    544569<?php endif; ?>
    545570
    546571<div class="wrap">
  • src/wp-admin/themes.php

    diff --git a/src/wp-admin/themes.php b/src/wp-admin/themes.php
    index 82d95ea15d..9d03831b00 100644
    a b  
    3333                switch_theme( $theme->get_stylesheet() );
    3434                wp_redirect( admin_url( 'themes.php?activated=true' ) );
    3535                exit;
     36        } elseif ( 'resume' === $_GET['action'] ) {
     37                check_admin_referer( 'resume-theme_' . $_GET['stylesheet'] );
     38                $theme = wp_get_theme( $_GET['stylesheet'] );
     39
     40                if ( ! current_user_can( 'resume_themes' ) ) {
     41                        wp_die(
     42                                '<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' .
     43                                '<p>' . __( 'Sorry, you are not allowed to resume this item.' ) . '</p>',
     44                                403
     45                        );
     46                }
     47
     48                $result = resume_theme( $theme->get_stylesheet() );
     49
     50                if ( is_wp_error( $result ) ) {
     51                        wp_die( $result );
     52                }
     53
     54                wp_redirect( admin_url( 'themes.php?resumed=true' ) );
     55                exit;
    3656        } elseif ( 'delete' == $_GET['action'] ) {
    3757                check_admin_referer( 'delete-theme_' . $_GET['stylesheet'] );
    3858                $theme = wp_get_theme( $_GET['stylesheet'] );
     
    4060                if ( ! current_user_can( 'delete_themes' ) ) {
    4161                        wp_die(
    4262                                '<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' .
    43                                 '<p>' . __( 'Sorry, you are not allowed to delete this item.' ) . '</p>',
     63                                '<p>' . __( 'Sorry, you are not allowed to resume execution of this theme.' ) . '</p>',
    4464                                403
    4565                        );
    4666                }
     
    173193
    174194        <hr class="wp-header-end">
    175195<?php
    176 if ( ! validate_current_theme() || isset( $_GET['broken'] ) ) :
     196if ( ! validate_current_theme() || isset( $_GET['broken'] ) ) {
    177197        ?>
    178 <div id="message1" class="updated notice is-dismissible"><p><?php _e( 'The active theme is broken. Reverting to the default theme.' ); ?></p></div>
     198        <div id="message1" class="updated notice is-dismissible"><p><?php _e( 'The active theme is broken. Reverting to the default theme.' ); ?></p></div>
    179199        <?php
    180 elseif ( isset( $_GET['activated'] ) ) :
     200} elseif ( isset( $_GET['activated'] ) ) {
    181201        if ( isset( $_GET['previewed'] ) ) {
    182202                ?>
    183203                <div id="message2" class="updated notice is-dismissible"><p><?php _e( 'Settings saved and theme activated.' ); ?> <a href="<?php echo home_url( '/' ); ?>"><?php _e( 'Visit site' ); ?></a></p></div>
    184                 <?php } else { ?>
    185 <div id="message2" class="updated notice is-dismissible"><p><?php _e( 'New theme activated.' ); ?> <a href="<?php echo home_url( '/' ); ?>"><?php _e( 'Visit site' ); ?></a></p></div>
    186                                                                                                                                                 <?php
    187 }
    188         elseif ( isset( $_GET['deleted'] ) ) :
     204                <?php
     205        } else {
    189206                ?>
    190 <div id="message3" class="updated notice is-dismissible"><p><?php _e( 'Theme deleted.' ); ?></p></div>
    191 <?php elseif ( isset( $_GET['delete-active-child'] ) ) : ?>
     207                <div id="message2" class="updated notice is-dismissible"><p><?php _e( 'New theme activated.' ); ?> <a href="<?php echo home_url( '/' ); ?>"><?php _e( 'Visit site' ); ?></a></p></div>
     208                <?php
     209        }
     210} elseif ( isset( $_GET['deleted'] ) ) {
     211        ?>
     212        <div id="message3" class="updated notice is-dismissible"><p><?php _e( 'Theme deleted.' ); ?></p></div>
     213        <?php
     214} elseif ( isset( $_GET['delete-active-child'] ) ) {
     215        ?>
    192216        <div id="message4" class="error"><p><?php _e( 'You cannot delete a theme while it has an active child theme.' ); ?></p></div>
    193217        <?php
    194 endif;
     218} elseif ( isset( $_GET['resumed'] ) ) {
     219        ?>
     220        <div id="message5" class="updated notice is-dismissible"><p><?php _e( 'Theme resumed.' ); ?></p></div>
     221        <?php
     222}
    195223
    196224$ct = wp_get_theme();
    197225
     
    344372<p><?php _e( 'The following themes are installed but incomplete.' ); ?></p>
    345373
    346374        <?php
     375        $can_resume  = current_user_can( 'resume_themes' );
    347376        $can_delete  = current_user_can( 'delete_themes' );
    348377        $can_install = current_user_can( 'install_themes' );
    349378        ?>
     
    351380        <tr>
    352381                <th><?php _ex( 'Name', 'theme name' ); ?></th>
    353382                <th><?php _e( 'Description' ); ?></th>
     383                <?php if ( $can_resume ) { ?>
     384                        <td></td>
     385                <?php } ?>
    354386                <?php if ( $can_delete ) { ?>
    355387                        <td></td>
    356388                <?php } ?>
     
    363395                        <td><?php echo $broken_theme->get( 'Name' ) ? $broken_theme->display( 'Name' ) : $broken_theme->get_stylesheet(); ?></td>
    364396                        <td><?php echo $broken_theme->errors()->get_error_message(); ?></td>
    365397                        <?php
     398                        if ( $can_resume ) {
     399                                if ( 'theme_paused' === $broken_theme->errors()->get_error_code() ) {
     400                                        $stylesheet = $broken_theme->get_stylesheet();
     401                                        $resume_url = add_query_arg(
     402                                                array(
     403                                                        'action'     => 'resume',
     404                                                        'stylesheet' => urlencode( $stylesheet ),
     405                                                ),
     406                                                admin_url( 'themes.php' )
     407                                        );
     408                                        $resume_url = wp_nonce_url( $resume_url, 'resume-theme_' . $stylesheet );
     409                                        ?>
     410                                        <td><a href="<?php echo esc_url( $resume_url ); ?>" class="button resume-theme"><?php _e( 'Resume' ); ?></a></td>
     411                                        <?php
     412                                } else {
     413                                        ?>
     414                                        <td></td>
     415                                        <?php
     416                                }
     417                        }
     418
    366419                        if ( $can_delete ) {
    367420                                $stylesheet = $broken_theme->get_stylesheet();
    368421                                $delete_url = add_query_arg(
  • src/wp-includes/capabilities.php

    diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php
    index c192639608..afb6f59507 100644
    a b function map_meta_cap( $cap, $user_id ) { 
    464464                                }
    465465                        }
    466466                        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;
     471                case 'resume_themes':
     472                        // Even in a multisite, regular administrators should be able to resume a theme.
     473                        $caps[] = 'switch_themes';
     474                        break;
    467475                case 'delete_user':
    468476                case 'delete_users':
    469477                        // 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..ae5eb337f4
    - +  
     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 */
     14class 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 error for an extension, if paused.
     128         *
     129         * @since 5.1.0
     130         *
     131         * @param string $extension Plugin or theme directory name.
     132         * @return array|null Error that is set, or null if the extension is not paused.
     133         */
     134        public function get( $extension ) {
     135                if ( ! $this->is_api_loaded() ) {
     136                        return null;
     137                }
     138
     139                if ( is_multisite() && is_site_meta_supported() ) {
     140                        $error = get_site_meta( get_current_blog_id(), $this->meta_prefix . $extension, true );
     141                        if ( ! $error ) {
     142                                return null;
     143                        }
     144
     145                        return $error;
     146                }
     147
     148                $paused_extensions = $this->get_all();
     149
     150                if ( ! isset( $paused_extensions[ $extension ] ) ) {
     151                        return null;
     152                }
     153
     154                return $paused_extensions[ $extension ];
     155        }
     156
     157        /**
     158         * Gets the paused extensions with their errors.
     159         *
     160         * @since 5.1.0
     161         *
     162         * @return array Associative array of $extension => $error pairs.
     163         */
     164        public function get_all() {
     165                if ( ! $this->is_api_loaded() ) {
     166                        return array();
     167                }
     168
     169                if ( is_multisite() && is_site_meta_supported() ) {
     170                        $site_metadata = get_site_meta( get_current_blog_id() );
     171
     172                        $paused_extensions = array();
     173                        foreach ( $site_metadata as $meta_key => $meta_values ) {
     174                                if ( 0 !== strpos( $meta_key, $this->meta_prefix ) ) {
     175                                        continue;
     176                                }
     177
     178                                $error = maybe_unserialize( array_shift( $meta_values ) );
     179
     180                                $paused_extensions[ substr( $meta_key, strlen( $this->meta_prefix ) ) ] = $error;
     181                        }
     182
     183                        return $paused_extensions;
     184                }
     185
     186                return (array) get_option( $this->option_name, array() );
     187        }
     188
     189        /**
     190         * Gets the site meta query clause for querying sites with paused extensions.
     191         *
     192         * @since 5.1.0
     193         *
     194         * @param string $extension Plugin or theme directory name.
     195         * @return array A single clause to add to a meta query.
     196         */
     197        public function get_site_meta_query_clause( $extension = '' ) {
     198                return array(
     199                        'key'         => $this->meta_prefix . $extension,
     200                        'compare_key' => '=',
     201                );
     202        }
     203
     204        /**
     205         * Checks whether the underlying API to store paused extensions is loaded.
     206         *
     207         * @since 5.1.0
     208         *
     209         * @return bool True if the API is loaded, false otherwise.
     210         */
     211        protected function is_api_loaded() {
     212                if ( is_multisite() ) {
     213                        return function_exists( 'is_site_meta_supported' ) && function_exists( 'get_site_meta' );
     214                }
     215
     216                return function_exists( 'get_option' );
     217        }
     218}
  • new file src/wp-includes/class-wp-shutdown-handler.php

    diff --git a/src/wp-includes/class-wp-shutdown-handler.php b/src/wp-includes/class-wp-shutdown-handler.php
    new file mode 100644
    index 0000000000..660ffddd83
    - +  
     1<?php
     2/**
     3 * Error Protection API: WP_Shutdown_Handler class
     4 *
     5 * @package WordPress
     6 * @since 5.1.0
     7 */
     8
     9/**
     10 * Core class used as the default shutdown handler.
     11 *
     12 * A drop-in 'shutdown-handler.php' can be used to override the instance of this class and use a custom implementation
     13 * for the shutdown handler that WordPress registers. The custom class should extend this class and can override its
     14 * methods individually as necessary. The file must return the instance of the class that should be registered.
     15 *
     16 * @since 5.1.0
     17 */
     18class WP_Shutdown_Handler {
     19
     20        /**
     21         * Runs the shutdown handler.
     22         *
     23         * This method is registered via `register_shutdown_function()`.
     24         *
     25         * @since 5.1.0
     26         */
     27        public function handle() {
     28                // Bail if WordPress executed successfully.
     29                if ( defined( 'WP_EXECUTION_SUCCEEDED' ) && WP_EXECUTION_SUCCEEDED ) {
     30                        return;
     31                }
     32
     33                try {
     34                        // Bail if no error found or if it could not be stored.
     35                        if ( ! $this->detect_error() ) {
     36                                return;
     37                        }
     38
     39                        // Redirect the request to catch multiple errors in one go.
     40                        $this->redirect_protected();
     41
     42                        // Display the PHP error template.
     43                        $this->display_error_template();
     44                } catch ( Exception $e ) {
     45                        // Catch exceptions and remain silent.
     46                }
     47        }
     48
     49        /**
     50         * Detects the error causing the crash and stores it if one was found.
     51         *
     52         * @since 5.1.0
     53         *
     54         * @return bool True if an error was found and stored, false otherwise.
     55         */
     56        protected function detect_error() {
     57                $error = error_get_last();
     58
     59                // No error, just skip the error handling code.
     60                if ( null === $error ) {
     61                        return false;
     62                }
     63
     64                // Bail if this error should not be handled.
     65                if ( ! wp_should_handle_error( $error ) ) {
     66                        return false;
     67                }
     68
     69                // Try to store the error so that the respective extension is paused.
     70                return wp_record_extension_error( $error );
     71        }
     72
     73        /**
     74         * Redirects the current request to allow recovering multiple errors in one go.
     75         *
     76         * The redirection will only happen when on a protected endpoint.
     77         *
     78         * It must be ensured that this method is only called when an error actually occurred and will not occur on the
     79         * next request again. Otherwise it will create a redirect loop.
     80         *
     81         * @since 5.1.0
     82         */
     83        protected function redirect_protected() {
     84                // Do not redirect requests on non-protected endpoints.
     85                if ( ! is_protected_endpoint() ) {
     86                        return;
     87                }
     88
     89                // Pluggable is usually loaded after plugins, so we manually include it here for redirection functionality.
     90                if ( ! function_exists( 'wp_redirect' ) ) {
     91                        include ABSPATH . WPINC . '/pluggable.php';
     92                }
     93
     94                $scheme = is_ssl() ? 'https://' : 'http://';
     95
     96                $url = "{$scheme}{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
     97                wp_redirect( $url );
     98                exit;
     99        }
     100
     101        /**
     102         * Displays the PHP error template and sends the HTTP status code, typically 500.
     103         *
     104         * A drop-in 'php-error.php' can be used as a custom template. This drop-in should control the HTTP status code and
     105         * print the HTML markup indicating that a PHP error occurred. Alternatively, {@see wp_die()} can be used. Note
     106         * that this drop-in may potentially be executed very early in the WordPress bootstrap process, so any core
     107         * functions used that are not part of `wp-includes/load.php` should be checked for before being called.
     108         *
     109         * The default template also displays a link to the admin in order to fix the problem, however doing so is not
     110         * mandatory.
     111         *
     112         * @since 5.1.0
     113         */
     114        protected function display_error_template() {
     115                // Load custom PHP error template, if present.
     116                $php_error_pluggable = WP_CONTENT_DIR . '/php-error.php';
     117                if ( is_readable( $php_error_pluggable ) ) {
     118                        require_once $php_error_pluggable;
     119                        die();
     120                }
     121
     122                // Otherwise, fail with a `wp_die()` message.
     123                $message = $this->get_error_message_markup();
     124
     125                // `wp_die()` wraps the message in paragraph tags, so let's just try working around that.
     126                if ( substr( $message, 0, 3 ) === '<p>' && substr( $message, -4 ) === '</p>' ) {
     127                        $message = substr( $message, 3, -4 );
     128                }
     129
     130                wp_die( $message, '', 500 );
     131        }
     132
     133        /**
     134         * Returns the error message markup to display in the default error template.
     135         *
     136         * @since 5.1.0
     137         *
     138         * @return string Error message HTML output.
     139         */
     140        protected function get_error_message_markup() {
     141                // Retrieve messages separately to cover the case where `__()` is not available.
     142                $i18n = $this->get_i18n();
     143
     144                $message = sprintf(
     145                        '<p>%s</p>',
     146                        $i18n['The site is experiencing technical difficulties.']
     147                );
     148
     149                if ( function_exists( 'admin_url' ) ) {
     150                        $message .= sprintf(
     151                                '<hr><p><em>%s <a href="%s">%s</a></em></p>',
     152                                $i18n['Are you the site owner?'],
     153                                admin_url(),
     154                                $i18n['Log into the admin backend to fix this.']
     155                        );
     156                }
     157
     158                if ( function_exists( 'apply_filters' ) ) {
     159                        /**
     160                         * Filters the message that the default PHP error page displays.
     161                         *
     162                         * @since 5.1.0
     163                         *
     164                         * @param string $message HTML error message to display.
     165                         */
     166                        $message = apply_filters( 'wp_technical_issues_display', $message );
     167                }
     168
     169                return $message;
     170        }
     171
     172        /**
     173         * Gets translatable error message strings, or non-translated versions if `__()` is not available.
     174         *
     175         * Because the PHP error handler may potentially run very early, it is not guaranteed that {@see __()} is already
     176         * loaded. Therefore the translatable strings are centrally managed here, rather than where they are actually used.
     177         *
     178         * @since 5.1.0
     179         *
     180         * @return array Associative array of $message => $translated_message pairs.
     181         */
     182        private function get_i18n() {
     183                $keys = array(
     184                        'The site is experiencing technical difficulties.',
     185                        'Are you the site owner?',
     186                        'Log into the admin backend to fix this.',
     187                );
     188
     189                if ( function_exists( '__' ) ) {
     190                        return array_combine(
     191                                $keys,
     192                                array(
     193                                        __( 'The site is experiencing technical difficulties.' ),
     194                                        __( 'Are you the site owner?' ),
     195                                        __( 'Log into the admin backend to fix this.' ),
     196                                )
     197                        );
     198                }
     199
     200                return array_combine( $keys, $keys );
     201        }
     202}
  • src/wp-includes/class-wp-theme.php

    diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php
    index a9ddb2251a..8b9c2c8815 100644
    a b public function __construct( $theme_dir, $theme_root, $_child = null ) { 
    371371                        $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this );
    372372                }
    373373
     374                if ( wp_paused_themes()->get( $this->stylesheet ) && ( ! is_wp_error( $this->errors ) || ! isset( $this->errors->errors['theme_paused'] ) ) ) {
     375                        $this->errors = new WP_Error( 'theme_paused', __( 'This theme failed to load properly and was paused within the admin backend.' ) );
     376                }
     377
    374378                // We're good. If we didn't retrieve from cache, set it.
    375379                if ( ! is_array( $cache ) ) {
    376380                        $cache = array(
  • 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..37564dfef3
    - +  
     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 */
     16function 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 */
     33function 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 */
     53function 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 */
     93function 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 && is_site_meta_supported() ) {
     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 */
     126function 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 * Registers the WordPress premature shutdown handler.
     144 *
     145 * @since 5.1.0
     146 */
     147function wp_register_premature_shutdown_handler() {
     148        $handler = null;
     149        if ( is_readable( WP_CONTENT_DIR . '/shutdown-handler.php' ) ) {
     150                $handler = include WP_CONTENT_DIR . '/shutdown-handler.php';
     151        }
     152
     153        if ( ! is_object( $handler ) || ! is_callable( array( $handler, 'handle' ) ) ) {
     154                $handler = new WP_Shutdown_Handler();
     155        }
     156
     157        register_shutdown_function( array( $handler, 'handle' ) );
     158}
  • src/wp-includes/load.php

    diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php
    index 26fef7240a..ffb47ee8c1 100644
    a b function wp_get_active_and_valid_plugins() { 
    687687                        $plugins[] = WP_PLUGIN_DIR . '/' . $plugin;
    688688                }
    689689        }
     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 */
     710function 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
    690728        return $plugins;
    691729}
    692730
     731/**
     732 * Retrieves an array of active and valid themes.
     733 *
     734 * While upgrading or installing WordPress, no themes are returned.
     735 *
     736 * @since 5.1.0
     737 * @access private
     738 *
     739 * @return array Array of paths to theme directories.
     740 */
     741function wp_get_active_and_valid_themes() {
     742        global $pagenow;
     743
     744        $themes = array();
     745
     746        if ( wp_installing() && 'wp-activate.php' !== $pagenow ) {
     747                return $themes;
     748        }
     749
     750        if ( TEMPLATEPATH !== STYLESHEETPATH ) {
     751                $themes[] = STYLESHEETPATH;
     752        }
     753
     754        $themes[] = TEMPLATEPATH;
     755
     756        /*
     757         * Remove themes from the list of active themes when we're on an endpoint
     758         * that should be protected against WSODs and the theme is paused.
     759         */
     760        if ( is_protected_endpoint() ) {
     761                $themes = wp_skip_paused_themes( $themes );
     762
     763                // If no active and valid themes exist, skip loading themes.
     764                if ( empty( $themes ) ) {
     765                        add_filter( 'wp_using_themes', '__return_false' );
     766                }
     767        }
     768
     769        return $themes;
     770}
     771
     772/**
     773 * Filters a given list of themes, removing any paused themes from it.
     774 *
     775 * @since 5.1.0
     776 *
     777 * @param array $themes List of absolute theme directory paths.
     778 * @return array Filtered value of $themes, without any paused themes.
     779 */
     780function wp_skip_paused_themes( array $themes ) {
     781        $paused_themes = wp_paused_themes()->get_all();
     782
     783        if ( empty( $paused_themes ) ) {
     784                return $themes;
     785        }
     786
     787        foreach ( $themes as $index => $theme ) {
     788                $theme = basename( $theme );
     789
     790                if ( array_key_exists( $theme, $paused_themes ) ) {
     791                        unset( $themes[ $index ] );
     792
     793                        // Store list of paused themes for displaying an admin notice.
     794                        $GLOBALS['_paused_themes'][ $theme ] = $paused_themes[ $theme ];
     795                }
     796        }
     797
     798        return $themes;
     799}
     800
    693801/**
    694802 * Set internal encoding.
    695803 *
    function wp_doing_ajax() { 
    11541262        return apply_filters( 'wp_doing_ajax', defined( 'DOING_AJAX' ) && DOING_AJAX );
    11551263}
    11561264
     1265/**
     1266 * Determines whether the current request should use themes.
     1267 *
     1268 * @since 5.1.0
     1269 *
     1270 * @return bool True if themes should be used, false otherwise.
     1271 */
     1272function wp_using_themes() {
     1273        /**
     1274         * Filters whether the current request should use themes.
     1275         *
     1276         * @since 5.1.0
     1277         *
     1278         * @param bool $wp_using_themes Whether the current request should use themes.
     1279         */
     1280        return apply_filters( 'wp_using_themes', defined( 'WP_USE_THEMES' ) && WP_USE_THEMES );
     1281}
     1282
     1283/**
     1284 * Determines whether we are currently on an endpoint that should be protected
     1285 * against WSODs.
     1286 *
     1287 * @since 5.1.0
     1288 *
     1289 * @return bool True if the current endpoint should be protected.
     1290 */
     1291function is_protected_endpoint() {
     1292        // Protect login pages.
     1293        if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) {
     1294                return true;
     1295        }
     1296
     1297        // Protect the admin backend.
     1298        if ( is_admin() && ! wp_doing_ajax() ) {
     1299                return true;
     1300        }
     1301
     1302        // Protect AJAX actions that could help resolve a fatal error should be available.
     1303        if ( wp_doing_ajax() && is_protected_ajax_action() ) {
     1304                return true;
     1305        }
     1306
     1307        /**
     1308         * Filters whether the current request is against a protected endpoint.
     1309         *
     1310         * This filter is only fired when an endpoint is requested which is not already protected by
     1311         * WordPress core. As such, it exclusively allows providing further protected endpoints in
     1312         * addition to the admin backend, login pages and protected AJAX actions.
     1313         *
     1314         * @since 5.1.0
     1315         *
     1316         * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected.
     1317         *                                    Default false.
     1318         */
     1319        return (bool) apply_filters( 'is_protected_endpoint', false );
     1320}
     1321
     1322/**
     1323 * Determines whether we are currently handling an AJAX action that should be
     1324 * protected against WSODs.
     1325 *
     1326 * @since 5.1.0
     1327 *
     1328 * @return bool True if the current AJAX action should be protected.
     1329 */
     1330function is_protected_ajax_action() {
     1331        $actions_to_protect = array(
     1332                'edit-theme-plugin-file', // Saving changes in the core code editor.
     1333                'heartbeat',              // Keep the heart beating.
     1334                'install-plugin',         // Installing a new plugin.
     1335                'install-theme',          // Installing a new theme.
     1336                'search-plugins',         // Searching in the list of plugins.
     1337                'search-install-plugins', // Searching for a plugin in the plugin install screen.
     1338                'update-plugin',          // Update an existing plugin.
     1339                'update-theme',           // Update an existing theme.
     1340        );
     1341
     1342        if ( ! wp_doing_ajax() ) {
     1343                return false;
     1344        }
     1345
     1346        if ( ! isset( $_REQUEST['action'] ) ) {
     1347                return false;
     1348        }
     1349
     1350        if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) {
     1351                return false;
     1352        }
     1353
     1354        return true;
     1355}
     1356
    11571357/**
    11581358 * Determines whether the current request is a WordPress cron request.
    11591359 *
  • 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() { 
    5252                        $plugins[] = WP_PLUGIN_DIR . '/' . $plugin;
    5353                }
    5454        }
     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
    5564        return $plugins;
    5665}
    5766
  • src/wp-includes/template-loader.php

    diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php
    index 0d2bafae44..0879798f55 100644
    a b  
    44 *
    55 * @package WordPress
    66 */
    7 if ( defined( 'WP_USE_THEMES' ) && WP_USE_THEMES ) {
     7if ( wp_using_themes() ) {
    88        /**
    99         * Fires before determining which template to load.
    1010         *
     
    4444        return;
    4545endif;
    4646
    47 if ( defined( 'WP_USE_THEMES' ) && WP_USE_THEMES ) :
     47if ( wp_using_themes() ) :
    4848        $template = false;
    4949        if ( is_embed() && $template = get_embed_template() ) :
    5050        elseif ( is_404() && $template = get_404_template() ) :
  • src/wp-settings.php

    diff --git a/src/wp-settings.php b/src/wp-settings.php
    index ca7d0b1a14..781d8e6ca1 100644
    a b  
    1717
    1818// Include files required for initialization.
    1919require( ABSPATH . WPINC . '/load.php' );
     20require( ABSPATH . WPINC . '/class-wp-paused-extensions-storage.php' );
     21require( ABSPATH . WPINC . '/class-wp-shutdown-handler.php' );
     22require( ABSPATH . WPINC . '/error-protection.php' );
    2023require( ABSPATH . WPINC . '/default-constants.php' );
    2124require_once( ABSPATH . WPINC . '/plugin.php' );
    2225
     26// Make sure we register the premature shutdown handler as soon as possible.
     27wp_register_premature_shutdown_handler();
     28
    2329/*
    2430 * These can't be directly globalized in version.php. When updating,
    2531 * we're including version.php from another installation and don't want
     
    474480$GLOBALS['wp_locale_switcher']->init();
    475481
    476482// Load the functions for the active theme, for both parent and child theme if applicable.
    477 if ( ! wp_installing() || 'wp-activate.php' === $pagenow ) {
    478         if ( TEMPLATEPATH !== STYLESHEETPATH && file_exists( STYLESHEETPATH . '/functions.php' ) ) {
    479                 include( STYLESHEETPATH . '/functions.php' );
    480         }
    481         if ( file_exists( TEMPLATEPATH . '/functions.php' ) ) {
    482                 include( TEMPLATEPATH . '/functions.php' );
     483foreach ( wp_get_active_and_valid_themes() as $theme ) {
     484        if ( file_exists( $theme . '/functions.php' ) ) {
     485                include $theme . '/functions.php';
    483486        }
    484487}
     488unset( $theme );
    485489
    486490/**
    487491 * Fires after the theme is loaded.
     
    526530 * @since 3.0.0
    527531 */
    528532do_action( 'wp_loaded' );
     533
     534/*
     535 * Store the fact that we could successfully execute the entire WordPress
     536 * lifecycle. This is used to skip the premature shutdown handler, as it cannot
     537 * be unregistered.
     538 */
     539if ( ! defined( 'WP_EXECUTION_SUCCEEDED' ) ) {
     540        define( 'WP_EXECUTION_SUCCEEDED', true );
     541}