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-includes/load.php

    r44717 r44973  
    698698    }
    699699
     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 ( wp_is_recovery_mode() ) {
     705        $plugins = wp_skip_paused_plugins( $plugins );
     706    }
     707
     708    return $plugins;
     709}
     710
     711/**
     712 * Filters a given list of plugins, removing any paused plugins from it.
     713 *
     714 * @since 5.2.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
    700737    return $plugins;
    701738}
     
    726763    $themes[] = TEMPLATEPATH;
    727764
     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 ( wp_is_recovery_mode() ) {
     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
    728778    return $themes;
     779}
     780
     781/**
     782 * Filters a given list of themes, removing any paused themes from it.
     783 *
     784 * @since 5.2.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;
     808}
     809
     810/**
     811 * Is WordPress in Recovery Mode.
     812 *
     813 * In this mode, plugins or themes that cause WSODs will be paused.
     814 *
     815 * @since 5.2.0
     816 *
     817 * @return bool
     818 */
     819function wp_is_recovery_mode() {
     820    return wp_recovery_mode()->is_active();
     821}
     822
     823/**
     824 * Determines whether we are currently on an endpoint that should be protected against WSODs.
     825 *
     826 * @since 5.2.0
     827 *
     828 * @return bool True if the current endpoint should be protected.
     829 */
     830function is_protected_endpoint() {
     831    // Protect login pages.
     832    if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) {
     833        return true;
     834    }
     835
     836    // Protect the admin backend.
     837    if ( is_admin() && ! wp_doing_ajax() ) {
     838        return true;
     839    }
     840
     841    // Protect AJAX actions that could help resolve a fatal error should be available.
     842    if ( is_protected_ajax_action() ) {
     843        return true;
     844    }
     845
     846    /**
     847     * Filters whether the current request is against a protected endpoint.
     848     *
     849     * This filter is only fired when an endpoint is requested which is not already protected by
     850     * WordPress core. As such, it exclusively allows providing further protected endpoints in
     851     * addition to the admin backend, login pages and protected AJAX actions.
     852     *
     853     * @since 5.2.0
     854     *
     855     * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected. Default false.
     856     */
     857    return (bool) apply_filters( 'is_protected_endpoint', false );
     858}
     859
     860/**
     861 * Determines whether we are currently handling an AJAX action that should be protected against WSODs.
     862 *
     863 * @since 5.2.0
     864 *
     865 * @return bool True if the current AJAX action should be protected.
     866 */
     867function is_protected_ajax_action() {
     868    if ( ! wp_doing_ajax() ) {
     869        return false;
     870    }
     871
     872    if ( ! isset( $_REQUEST['action'] ) ) {
     873        return false;
     874    }
     875
     876    $actions_to_protect = array(
     877        'edit-theme-plugin-file', // Saving changes in the core code editor.
     878        'heartbeat',              // Keep the heart beating.
     879        'install-plugin',         // Installing a new plugin.
     880        'install-theme',          // Installing a new theme.
     881        'search-plugins',         // Searching in the list of plugins.
     882        'search-install-plugins', // Searching for a plugin in the plugin install screen.
     883        'update-plugin',          // Update an existing plugin.
     884        'update-theme',           // Update an existing theme.
     885    );
     886
     887    /**
     888     * Filters the array of protected AJAX actions.
     889     *
     890     * This filter is only fired when doing AJAX and the AJAX request has an 'action' property.
     891     *
     892     * @since 5.2.0
     893     *
     894     * @param array $actions_to_protect Array of strings with AJAX actions to protect.
     895     */
     896    $actions_to_protect = (array) apply_filters( 'wp_protected_ajax_actions', $actions_to_protect );
     897
     898    if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) {
     899        return false;
     900    }
     901
     902    return true;
    729903}
    730904
Note: See TracChangeset for help on using the changeset viewer.