Make WordPress Core

Changeset 44524


Ignore:
Timestamp:
01/09/2019 08:04:55 PM (6 years ago)
Author:
flixos90
Message:

Bootstrap/Load: Introduce fatal error recovery mechanism allowing users to still log in to their admin dashboard.

This changeset introduces a WP_Shutdown_Handler class that detects fatal errors and which extension (plugin or theme) causes them. Such an error is then recorded, and an error message is displayed. Subsequently, in certain protected areas, for example the admin, the broken extension will be paused, ensuring that the website is still usable in the respective area. The major benefit is that this mechanism allows site owners to still log in to their website, to fix the problem by either disabling the extension or solving the bug and then resuming the extension.

Extensions are only paused in certain designated areas. The frontend for example stays unaffected, as it is impossible to know what pausing the extension would cause to be missing, so it might be preferrable to clearly see that the website is temporarily not accessible instead.

The fatal error recovery is especially important in scope of encouraging the switch to a maintained PHP version, as not necessarily every WordPress extension is compatible with all PHP versions. If problems occur now, non-technical site owners that do not have immediate access to the codebase are not locked out of their site and can at least temporarily solve the problem quickly.

Websites that have custom requirements in that regard can implement their own shutdown handler by adding a shutdown-handler.php drop-in that returns the handler instance to use, which must be based on a class that inherits WP_Shutdown_Handler. That handler will then be used in place of the default one.

Websites that would like to modify specifically the error template displayed in the frontend can add a php-error.php drop-in that works similarly to the existing db-error.php drop-in.

Props afragen, bradleyt, flixos90, ocean90, schlessera, SergeyBiryukov, spacedmonkey.
Fixes #44458.

Location:
trunk/src
Files:
3 added
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/list-tables.css

    r44352 r44524  
    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;
  • trunk/src/wp-admin/includes/admin-filters.php

    r44291 r44524  
    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
  • trunk/src/wp-admin/includes/class-wp-plugins-list-table.php

    r43571 r44524  
    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        }
     
    100100            'mustuse'            => array(),
    101101            'dropins'            => array(),
     102            'paused'             => array(),
    102103        );
    103104
     
    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
     
    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 ] ) ) {
     
    438445                case 'dropins':
    439446                    $text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count );
     447                    break;
     448                case 'paused':
     449                    /* translators: %s: plugin count */
     450                    $text = _n( 'Paused <span class="count">(%s)</span>', 'Paused <span class="count">(%s)</span>', $count );
    440451                    break;
    441452                case 'upgrade':
     
    626637                        $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>';
    627638                    }
     639                    if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) {
     640                        /* translators: %s: plugin name */
     641                        $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( 'Network Resume %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Resume' ) . '</a>';
     642                    }
    628643                } else {
    629644                    if ( current_user_can( 'manage_network_plugins' ) ) {
    630645                        /* translators: %s: plugin name */
    631646                        $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>';
     647                    }
     648                    if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) {
     649                        /* translators: %s: plugin name */
     650                        $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( 'Network Resume %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Network Resume' ) . '</a>';
    632651                    }
    633652                    if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) {
     
    641660                        'network_active' => __( 'Network Active' ),
    642661                    );
     662                    if ( ! $restrict_network_only && current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) {
     663                        /* translators: %s: plugin name */
     664                        $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 %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume' ) . '</a>';
     665                    }
    643666                } elseif ( $restrict_network_only ) {
    644667                    $actions = array(
     
    649672                        /* translators: %s: plugin name */
    650673                        $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>';
     674                    }
     675                    if ( current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) {
     676                        /* translators: %s: plugin name */
     677                        $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 %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume' ) . '</a>';
    651678                    }
    652679                } else {
     
    754781        if ( ! empty( $totals['upgrade'] ) && ! empty( $plugin_data['update'] ) ) {
    755782            $class .= ' update';
     783        }
     784
     785        $paused                        = is_plugin_paused( $plugin_file );
     786        $paused_on_network_sites_count = $screen->in_admin( 'network' ) ? count_paused_plugin_sites_for_network( $plugin_file ) : 0;
     787        if ( $paused || $paused_on_network_sites_count ) {
     788            $class .= ' paused';
    756789        }
    757790
     
    834867                     * @param string   $status      Status of the plugin. Defaults are 'All', 'Active',
    835868                     *                              'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    836                      *                              'Drop-ins', 'Search'.
     869                     *                              'Drop-ins', 'Search', 'Paused'.
    837870                     */
    838871                    $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );
    839872                    echo implode( ' | ', $plugin_meta );
    840873
    841                     echo '</div></td>';
     874                    echo '</div>';
     875
     876                    if ( $paused || $paused_on_network_sites_count ) {
     877                        $notice_text = __( 'This plugin failed to load properly and was paused within the admin backend.' );
     878                        if ( $screen->in_admin( 'network' ) && $paused_on_network_sites_count ) {
     879                            $notice_text = sprintf(
     880                                /* translators: %s: number of sites */
     881                                _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 ),
     882                                number_format_i18n( $paused_on_network_sites_count )
     883                            );
     884                        }
     885
     886                        printf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text );
     887
     888                        $error = wp_get_plugin_error( $plugin_file );
     889
     890                        if ( false !== $error ) {
     891                            $constants = get_defined_constants( true );
     892                            $constants = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal'];
     893
     894                            foreach ( $constants as $constant => $value ) {
     895                                if ( 0 === strpos( $constant, 'E_' ) ) {
     896                                    $core_errors[ $value ] = $constant;
     897                                }
     898                            }
     899
     900                            $error['type'] = $core_errors[ $error['type'] ];
     901
     902                            printf(
     903                                '<div class="error-display"><p>%s</p></div>',
     904                                sprintf(
     905                                    /* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */
     906                                    __( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ),
     907                                    "<code>{$error['type']}</code>",
     908                                    "<code>{$error['line']}</code>",
     909                                    "<code>{$error['file']}</code>",
     910                                    "<code>{$error['message']}</code>"
     911                                )
     912                            );
     913                        }
     914                    }
     915
     916                    echo '</td>';
    842917                    break;
    843918                default:
     
    872947         * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    873948         *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    874          *                            'Drop-ins', 'Search'.
     949         *                            'Drop-ins', 'Search', 'Paused'.
    875950         */
    876951        do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
     
    888963         * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
    889964         *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
    890          *                            'Drop-ins', 'Search'.
     965         *                            'Drop-ins', 'Search', 'Paused'.
    891966         */
    892967        do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
  • trunk/src/wp-admin/includes/plugin.php

    r43361 r44524  
    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
     
    495497function is_plugin_inactive( $plugin ) {
    496498    return ! is_plugin_active( $plugin );
     499}
     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 */
     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 );
    497577}
    498578
     
    692772        if ( ! is_plugin_active( $plugin ) ) {
    693773            continue;
     774        }
     775
     776        // Clean up the database before deactivating the plugin.
     777        if ( is_plugin_paused( $plugin ) ) {
     778            resume_plugin( $plugin );
    694779        }
    695780
     
    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.
     
    9551045
    9561046        return new WP_Error( 'could_not_remove_plugin', sprintf( $message, implode( ', ', $errors ) ) );
     1047    }
     1048
     1049    return true;
     1050}
     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 */
     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 the plugin.' )
     1097        );
    9571098    }
    9581099
     
    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}
  • trunk/src/wp-admin/includes/theme.php

    r43571 r44524  
    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 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}
  • trunk/src/wp-admin/plugins.php

    r43667 r44524  
    390390            break;
    391391
     392        case 'resume':
     393            if ( ! current_user_can( 'resume_plugin', $plugin ) ) {
     394                wp_die( __( 'Sorry, you are not allowed to resume 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'] ) ) {
     
    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>.' );
     
    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( 'Plugin <strong>resumed</strong>.' ); ?></p></div>
    544569<?php endif; ?>
    545570
  • trunk/src/wp-admin/themes.php

    r43571 r44524  
    3333        switch_theme( $theme->get_stylesheet() );
    3434        wp_redirect( admin_url( 'themes.php?activated=true' ) );
     35        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 theme.' ) . '</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' ) );
    3555        exit;
    3656    } elseif ( 'delete' == $_GET['action'] ) {
     
    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();
     
    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' );
     
    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>
     
    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();
  • trunk/src/wp-includes/capabilities.php

    r44146 r44524  
    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':
  • trunk/src/wp-includes/class-wp-theme.php

    r44151 r44524  
    370370            // Set the parent. Pass the current instance so we can do the crazy checks above and assess errors.
    371371            $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this );
     372        }
     373
     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.' ) );
    372376        }
    373377
  • trunk/src/wp-includes/load.php

    r44453 r44524  
    697697        }
    698698    }
     699
     700    /*
     701     * Remove plugins from the list of active plugins when we're on an endpoint
     702     * that should be protected against WSODs and the plugin is paused.
     703     */
     704    if ( is_protected_endpoint() ) {
     705        $plugins = wp_skip_paused_plugins( $plugins );
     706    }
     707
    699708    return $plugins;
     709}
     710
     711/**
     712 * Filters a given list of plugins, removing any paused plugins from it.
     713 *
     714 * @since 5.1.0
     715 *
     716 * @param array $plugins List of absolute plugin main file paths.
     717 * @return array Filtered value of $plugins, without any paused plugins.
     718 */
     719function wp_skip_paused_plugins( array $plugins ) {
     720    $paused_plugins = wp_paused_plugins()->get_all();
     721
     722    if ( empty( $paused_plugins ) ) {
     723        return $plugins;
     724    }
     725
     726    foreach ( $plugins as $index => $plugin ) {
     727        list( $plugin ) = explode( '/', plugin_basename( $plugin ) );
     728
     729        if ( array_key_exists( $plugin, $paused_plugins ) ) {
     730            unset( $plugins[ $index ] );
     731
     732            // Store list of paused plugins for displaying an admin notice.
     733            $GLOBALS['_paused_plugins'][ $plugin ] = $paused_plugins[ $plugin ];
     734        }
     735    }
     736
     737    return $plugins;
     738}
     739
     740/**
     741 * Retrieves an array of active and valid themes.
     742 *
     743 * While upgrading or installing WordPress, no themes are returned.
     744 *
     745 * @since 5.1.0
     746 * @access private
     747 *
     748 * @return array Array of paths to theme directories.
     749 */
     750function wp_get_active_and_valid_themes() {
     751    global $pagenow;
     752
     753    $themes = array();
     754
     755    if ( wp_installing() && 'wp-activate.php' !== $pagenow ) {
     756        return $themes;
     757    }
     758
     759    if ( TEMPLATEPATH !== STYLESHEETPATH ) {
     760        $themes[] = STYLESHEETPATH;
     761    }
     762
     763    $themes[] = TEMPLATEPATH;
     764
     765    /*
     766     * Remove themes from the list of active themes when we're on an endpoint
     767     * that should be protected against WSODs and the theme is paused.
     768     */
     769    if ( is_protected_endpoint() ) {
     770        $themes = wp_skip_paused_themes( $themes );
     771
     772        // If no active and valid themes exist, skip loading themes.
     773        if ( empty( $themes ) ) {
     774            add_filter( 'wp_using_themes', '__return_false' );
     775        }
     776    }
     777
     778    return $themes;
     779}
     780
     781/**
     782 * Filters a given list of themes, removing any paused themes from it.
     783 *
     784 * @since 5.1.0
     785 *
     786 * @param array $themes List of absolute theme directory paths.
     787 * @return array Filtered value of $themes, without any paused themes.
     788 */
     789function wp_skip_paused_themes( array $themes ) {
     790    $paused_themes = wp_paused_themes()->get_all();
     791
     792    if ( empty( $paused_themes ) ) {
     793        return $themes;
     794    }
     795
     796    foreach ( $themes as $index => $theme ) {
     797        $theme = basename( $theme );
     798
     799        if ( array_key_exists( $theme, $paused_themes ) ) {
     800            unset( $themes[ $index ] );
     801
     802            // Store list of paused themes for displaying an admin notice.
     803            $GLOBALS['_paused_themes'][ $theme ] = $paused_themes[ $theme ];
     804        }
     805    }
     806
     807    return $themes;
    700808}
    701809
     
    11651273
    11661274/**
     1275 * Determines whether the current request should use themes.
     1276 *
     1277 * @since 5.1.0
     1278 *
     1279 * @return bool True if themes should be used, false otherwise.
     1280 */
     1281function wp_using_themes() {
     1282    /**
     1283     * Filters whether the current request should use themes.
     1284     *
     1285     * @since 5.1.0
     1286     *
     1287     * @param bool $wp_using_themes Whether the current request should use themes.
     1288     */
     1289    return apply_filters( 'wp_using_themes', defined( 'WP_USE_THEMES' ) && WP_USE_THEMES );
     1290}
     1291
     1292/**
     1293 * Determines whether we are currently on an endpoint that should be protected against WSODs.
     1294 *
     1295 * @since 5.1.0
     1296 *
     1297 * @return bool True if the current endpoint should be protected.
     1298 */
     1299function is_protected_endpoint() {
     1300    // Protect login pages.
     1301    if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) {
     1302        return true;
     1303    }
     1304
     1305    // Protect the admin backend.
     1306    if ( is_admin() && ! wp_doing_ajax() ) {
     1307        return true;
     1308    }
     1309
     1310    // Protect AJAX actions that could help resolve a fatal error should be available.
     1311    if ( is_protected_ajax_action() ) {
     1312        return true;
     1313    }
     1314
     1315    /**
     1316     * Filters whether the current request is against a protected endpoint.
     1317     *
     1318     * This filter is only fired when an endpoint is requested which is not already protected by
     1319     * WordPress core. As such, it exclusively allows providing further protected endpoints in
     1320     * addition to the admin backend, login pages and protected AJAX actions.
     1321     *
     1322     * @since 5.1.0
     1323     *
     1324     * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected. Default false.
     1325     */
     1326    return (bool) apply_filters( 'is_protected_endpoint', false );
     1327}
     1328
     1329/**
     1330 * Determines whether we are currently handling an AJAX action that should be protected against WSODs.
     1331 *
     1332 * @since 5.1.0
     1333 *
     1334 * @return bool True if the current AJAX action should be protected.
     1335 */
     1336function is_protected_ajax_action() {
     1337    if ( ! wp_doing_ajax() ) {
     1338        return false;
     1339    }
     1340
     1341    if ( ! isset( $_REQUEST['action'] ) ) {
     1342        return false;
     1343    }
     1344
     1345    $actions_to_protect = array(
     1346        'edit-theme-plugin-file', // Saving changes in the core code editor.
     1347        'heartbeat',              // Keep the heart beating.
     1348        'install-plugin',         // Installing a new plugin.
     1349        'install-theme',          // Installing a new theme.
     1350        'search-plugins',         // Searching in the list of plugins.
     1351        'search-install-plugins', // Searching for a plugin in the plugin install screen.
     1352        'update-plugin',          // Update an existing plugin.
     1353        'update-theme',           // Update an existing theme.
     1354    );
     1355
     1356    /**
     1357     * Filters the array of protected AJAX actions.
     1358     *
     1359     * This filter is only fired when doing AJAX and the AJAX request has an 'action' property.
     1360     *
     1361     * @since 5.1.0
     1362     *
     1363     * @param array $actions_to_protect Array of strings with AJAX actions to protect.
     1364     */
     1365    $actions_to_protect = (array) apply_filters( 'wp_protected_ajax_actions', $actions_to_protect );
     1366
     1367    if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) {
     1368        return false;
     1369    }
     1370
     1371    return true;
     1372}
     1373
     1374/**
    11671375 * Determines whether the current request is a WordPress cron request.
    11681376 *
  • trunk/src/wp-includes/ms-load.php

    r42698 r44524  
    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}
  • trunk/src/wp-includes/template-loader.php

    r42343 r44524  
    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.
     
    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() ) :
  • trunk/src/wp-settings.php

    r44344 r44524  
    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' );
     25
     26// Make sure we register the premature shutdown handler as soon as possible.
     27wp_register_premature_shutdown_handler();
    2228
    2329/*
     
    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' );
     483foreach ( wp_get_active_and_valid_themes() as $theme ) {
     484    if ( file_exists( $theme . '/functions.php' ) ) {
     485        include $theme . '/functions.php';
    480486    }
    481     if ( file_exists( TEMPLATEPATH . '/functions.php' ) ) {
    482         include( TEMPLATEPATH . '/functions.php' );
    483     }
    484 }
     487}
     488unset( $theme );
    485489
    486490/**
     
    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}
Note: See TracChangeset for help on using the changeset viewer.