Make WordPress Core


Ignore:
Timestamp:
03/21/2019 09:52:07 PM (6 years ago)
Author:
flixos90
Message:

Bootstrap/Load: Introduce a recovery mode for fixing fatal errors.

Using the new fatal handler introduced in [44962], an email is sent to the admin when a fatal error occurs. This email includes a secret link to enter recovery mode. When clicked, the link will be validated and on success a cookie will be placed on the client, enabling recovery mode for that user. This functionality is executed early before plugins and themes are loaded, in order to be unaffected by potential fatal errors these might be causing.

When in recovery mode, broken plugins and themes will be paused for that client, so that they are able to access the admin backend despite of these errors. They are notified about the broken extensions and the errors caused, and can then decide whether they would like to temporarily deactivate the extension or fix the problem and resume the extension.

A link in the admin bar allows the client to exit recovery mode.

Props timothyblynjacobs, afragen, flixos90, nerrad, miss_jwo, schlessera, spacedmonkey, swissspidy.
Fixes #46130, #44458.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/plugin.php

    r44717 r44973  
    469469function _get_dropins() {
    470470    $dropins = array(
    471         'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
    472         'db.php'             => array( __( 'Custom database class.' ), true ), // auto on load
    473         'db-error.php'       => array( __( 'Custom database error message.' ), true ), // auto on error
    474         'install.php'        => array( __( 'Custom installation script.' ), true ), // auto on installation
    475         'maintenance.php'    => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance
    476         'object-cache.php'   => array( __( 'External object cache.' ), true ), // auto on load
     471        'advanced-cache.php'      => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
     472        'db.php'                  => array( __( 'Custom database class.' ), true ), // auto on load
     473        'db-error.php'            => array( __( 'Custom database error message.' ), true ), // auto on error
     474        'install.php'             => array( __( 'Custom installation script.' ), true ), // auto on installation
     475        'maintenance.php'         => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance
     476        'object-cache.php'        => array( __( 'External object cache.' ), true ), // auto on load
     477        'php-error.php'           => array( __( 'Custom PHP error message.' ), true ), // auto on error
     478        'fatal-error-handler.php' => array( __( 'Custom PHP fatal error handler.' ), true ), // auto on error
    477479    );
    478480
     
    21022104    WP_Privacy_Policy_Content::add( $plugin_name, $policy_text );
    21032105}
     2106
     2107/**
     2108 * Determines whether a plugin is technically active but was paused while
     2109 * loading.
     2110 *
     2111 * For more information on this and similar theme functions, check out
     2112 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
     2113 * Conditional Tags} article in the Theme Developer Handbook.
     2114 *
     2115 * @since 5.2.0
     2116 *
     2117 * @param string $plugin Path to the plugin file relative to the plugins directory.
     2118 * @return bool True, if in the list of paused plugins. False, not in the list.
     2119 */
     2120function is_plugin_paused( $plugin ) {
     2121    if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
     2122        return false;
     2123    }
     2124
     2125    if ( ! is_plugin_active( $plugin ) ) {
     2126        return false;
     2127    }
     2128
     2129    list( $plugin ) = explode( '/', $plugin );
     2130
     2131    return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] );
     2132}
     2133
     2134/**
     2135 * Gets the error that was recorded for a paused plugin.
     2136 *
     2137 * @since 5.2.0
     2138 *
     2139 * @param string $plugin Path to the plugin file relative to the plugins
     2140 *                       directory.
     2141 * @return array|false Array of error information as it was returned by
     2142 *                     `error_get_last()`, or false if none was recorded.
     2143 */
     2144function wp_get_plugin_error( $plugin ) {
     2145    if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
     2146        return false;
     2147    }
     2148
     2149    list( $plugin ) = explode( '/', $plugin );
     2150
     2151    if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) {
     2152        return false;
     2153    }
     2154
     2155    return $GLOBALS['_paused_plugins'][ $plugin ];
     2156}
     2157
     2158/**
     2159 * Tries to resume a single plugin.
     2160 *
     2161 * If a redirect was provided, we first ensure the plugin does not throw fatal
     2162 * errors anymore.
     2163 *
     2164 * The way it works is by setting the redirection to the error before trying to
     2165 * include the plugin file. If the plugin fails, then the redirection will not
     2166 * be overwritten with the success message and the plugin will not be resumed.
     2167 *
     2168 * @since 5.2.0
     2169 *
     2170 * @param string $plugin       Single plugin to resume.
     2171 * @param string $redirect     Optional. URL to redirect to. Default empty string.
     2172 * @return bool|WP_Error True on success, false if `$plugin` was not paused,
     2173 *                       `WP_Error` on failure.
     2174 */
     2175function resume_plugin( $plugin, $redirect = '' ) {
     2176    /*
     2177     * We'll override this later if the plugin could be resumed without
     2178     * creating a fatal error.
     2179     */
     2180    if ( ! empty( $redirect ) ) {
     2181        wp_redirect(
     2182            add_query_arg(
     2183                '_error_nonce',
     2184                wp_create_nonce( 'plugin-resume-error_' . $plugin ),
     2185                $redirect
     2186            )
     2187        );
     2188
     2189        // Load the plugin to test whether it throws a fatal error.
     2190        ob_start();
     2191        plugin_sandbox_scrape( $plugin );
     2192        ob_clean();
     2193    }
     2194
     2195    list( $extension ) = explode( '/', $plugin );
     2196
     2197    $result = wp_paused_plugins()->delete( $extension );
     2198
     2199    if ( ! $result ) {
     2200        return new WP_Error(
     2201            'could_not_resume_plugin',
     2202            __( 'Could not resume the plugin.' )
     2203        );
     2204    }
     2205
     2206    return true;
     2207}
     2208
     2209/**
     2210 * Renders an admin notice in case some plugins have been paused due to errors.
     2211 *
     2212 * @since 5.2.0
     2213 */
     2214function paused_plugins_notice() {
     2215    if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
     2216        return;
     2217    }
     2218
     2219    if ( ! current_user_can( 'resume_plugins' ) ) {
     2220        return;
     2221    }
     2222
     2223    if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) {
     2224        return;
     2225    }
     2226
     2227    printf(
     2228        '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p><a href="%s">%s</a></p></div>',
     2229        __( 'One or more plugins failed to load properly.' ),
     2230        __( 'You can find more details and make changes on the Plugins screen.' ),
     2231        esc_url( admin_url( 'plugins.php?plugin_status=paused' ) ),
     2232        __( 'Go to the Plugins screen' )
     2233    );
     2234}
Note: See TracChangeset for help on using the changeset viewer.