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.


git-svn-id: https://develop.svn.wordpress.org/trunk@44524 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Felix Arntz
2019-01-09 20:04:55 +00:00
parent 493b90cde1
commit fc37b1746e
16 changed files with 1310 additions and 30 deletions
@@ -40,7 +40,7 @@ class WP_Plugins_List_Table extends WP_List_Table {
);
$status = 'all';
if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search' ) ) ) {
if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused' ) ) ) {
$status = $_REQUEST['plugin_status'];
}
@@ -99,6 +99,7 @@ class WP_Plugins_List_Table extends WP_List_Table {
'upgrade' => array(),
'mustuse' => array(),
'dropins' => array(),
'paused' => array(),
);
$screen = $this->screen;
@@ -209,6 +210,9 @@ class WP_Plugins_List_Table extends WP_List_Table {
if ( $show_network_active ) {
// On the non-network screen, show network-active plugins if allowed
$plugins['active'][ $plugin_file ] = $plugin_data;
if ( is_plugin_paused( $plugin_file ) ) {
$plugins['paused'][ $plugin_file ] = $plugin_data;
}
} else {
// On the non-network screen, filter out network-active plugins
unset( $plugins['all'][ $plugin_file ] );
@@ -218,6 +222,9 @@ class WP_Plugins_List_Table extends WP_List_Table {
// On the non-network screen, populate the active list with plugins that are individually activated
// On the network-admin screen, populate the active list with plugins that are network activated
$plugins['active'][ $plugin_file ] = $plugin_data;
if ( is_plugin_paused( $plugin_file ) ) {
$plugins['paused'][ $plugin_file ] = $plugin_data;
}
} else {
if ( isset( $recently_activated[ $plugin_file ] ) ) {
// Populate the recently activated list with plugins that have been recently activated
@@ -438,6 +445,10 @@ class WP_Plugins_List_Table extends WP_List_Table {
case 'dropins':
$text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count );
break;
case 'paused':
/* translators: %s: plugin count */
$text = _n( 'Paused <span class="count">(%s)</span>', 'Paused <span class="count">(%s)</span>', $count );
break;
case 'upgrade':
$text = _n( 'Update Available <span class="count">(%s)</span>', 'Update Available <span class="count">(%s)</span>', $count );
break;
@@ -625,11 +636,19 @@ class WP_Plugins_List_Table extends WP_List_Table {
/* translators: %s: plugin name */
$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>';
}
if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) {
/* translators: %s: plugin name */
$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>';
}
} else {
if ( current_user_can( 'manage_network_plugins' ) ) {
/* translators: %s: plugin name */
$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>';
}
if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) {
/* translators: %s: plugin name */
$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>';
}
if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) {
/* translators: %s: plugin name */
$actions['delete'] = '<a href="' . wp_nonce_url( 'plugins.php?action=delete-selected&amp;checked[]=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'bulk-plugins' ) . '" class="delete" aria-label="' . esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Delete' ) . '</a>';
@@ -640,6 +659,10 @@ class WP_Plugins_List_Table extends WP_List_Table {
$actions = array(
'network_active' => __( 'Network Active' ),
);
if ( ! $restrict_network_only && current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) {
/* translators: %s: plugin name */
$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>';
}
} elseif ( $restrict_network_only ) {
$actions = array(
'network_only' => __( 'Network Only' ),
@@ -649,6 +672,10 @@ class WP_Plugins_List_Table extends WP_List_Table {
/* translators: %s: plugin name */
$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>';
}
if ( current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) {
/* translators: %s: plugin name */
$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>';
}
} else {
if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
/* translators: %s: plugin name */
@@ -755,6 +782,12 @@ class WP_Plugins_List_Table extends WP_List_Table {
$class .= ' update';
}
$paused = is_plugin_paused( $plugin_file );
$paused_on_network_sites_count = $screen->in_admin( 'network' ) ? count_paused_plugin_sites_for_network( $plugin_file ) : 0;
if ( $paused || $paused_on_network_sites_count ) {
$class .= ' paused';
}
$plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_name );
printf(
'<tr class="%s" data-slug="%s" data-plugin="%s">',
@@ -833,12 +866,54 @@ class WP_Plugins_List_Table extends WP_List_Table {
* @param array $plugin_data An array of plugin data.
* @param string $status Status of the plugin. Defaults are 'All', 'Active',
* 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
* 'Drop-ins', 'Search'.
* 'Drop-ins', 'Search', 'Paused'.
*/
$plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );
echo implode( ' | ', $plugin_meta );
echo '</div></td>';
echo '</div>';
if ( $paused || $paused_on_network_sites_count ) {
$notice_text = __( 'This plugin failed to load properly and was paused within the admin backend.' );
if ( $screen->in_admin( 'network' ) && $paused_on_network_sites_count ) {
$notice_text = sprintf(
/* translators: %s: number of sites */
_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 ),
number_format_i18n( $paused_on_network_sites_count )
);
}
printf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text );
$error = wp_get_plugin_error( $plugin_file );
if ( false !== $error ) {
$constants = get_defined_constants( true );
$constants = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal'];
foreach ( $constants as $constant => $value ) {
if ( 0 === strpos( $constant, 'E_' ) ) {
$core_errors[ $value ] = $constant;
}
}
$error['type'] = $core_errors[ $error['type'] ];
printf(
'<div class="error-display"><p>%s</p></div>',
sprintf(
/* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */
__( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ),
"<code>{$error['type']}</code>",
"<code>{$error['line']}</code>",
"<code>{$error['file']}</code>",
"<code>{$error['message']}</code>"
)
);
}
}
echo '</td>';
break;
default:
$classes = "$column_name column-$column_name $class";
@@ -871,7 +946,7 @@ class WP_Plugins_List_Table extends WP_List_Table {
* @param array $plugin_data An array of plugin data.
* @param string $status Status of the plugin. Defaults are 'All', 'Active',
* 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
* 'Drop-ins', 'Search'.
* 'Drop-ins', 'Search', 'Paused'.
*/
do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
@@ -887,7 +962,7 @@ class WP_Plugins_List_Table extends WP_List_Table {
* @param array $plugin_data An array of plugin data.
* @param string $status Status of the plugin. Defaults are 'All', 'Active',
* 'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
* 'Drop-ins', 'Search'.
* 'Drop-ins', 'Search', 'Paused'.
*/
do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
}