WordPress.org

Make WordPress Core


Ignore:
Timestamp:
01/09/2019 08:04:55 PM (9 months 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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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 *
Note: See TracChangeset for help on using the changeset viewer.