Make WordPress Core


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.

File:
1 edited

Legend:

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