diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index 893326ba38ad6..0e8891ec0e0f3 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -1301,6 +1301,31 @@ ul.cat-checklist { text-decoration: underline; } +.plugins tr.paused th.check-column { + border-left: 4px solid #d54e21; +} + +.plugins tr.paused th, +.plugins tr.paused td { + background-color: #fef7f1; +} + +.plugins tr.paused .plugin-title, +.plugins .paused .dashicons-warning { + color: #dc3232; +} + +.plugins .paused .error-display p, +.plugins .paused .error-display code { + font-size: 90%; + font-style: italic; + color: rgb( 0, 0, 0, 0.7 ); +} + +.plugins .resume-link { + color: #dc3232; +} + .plugin-card .update-now:before { color: #f56e28; content: "\f463"; diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 0816b2420b370..bc2f769b644ee 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -121,6 +121,7 @@ add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called. add_action( 'admin_notices', 'update_nag', 3 ); +add_action( 'admin_notices', 'paused_plugins_notice', 5 ); add_action( 'admin_notices', 'maintenance_nag', 10 ); add_filter( 'update_footer', 'core_update_footer' ); diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index 6e130f94b45c1..a7766117691a8 100644 --- a/src/wp-admin/includes/class-wp-plugins-list-table.php +++ b/src/wp-admin/includes/class-wp-plugins-list-table.php @@ -40,7 +40,7 @@ public function __construct( $args = array() ) { ); $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 @@ public function prepare_items() { 'upgrade' => array(), 'mustuse' => array(), 'dropins' => array(), + 'paused' => array(), ); $screen = $this->screen; @@ -209,6 +210,9 @@ public function prepare_items() { 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 @@ public function prepare_items() { // 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 @@ -436,6 +443,9 @@ protected function get_views() { case 'dropins': $text = _n( 'Drop-ins (%s)', 'Drop-ins (%s)', $count ); break; + case 'paused': + $text = _n( 'Paused (%s)', 'Paused (%s)', $count ); + break; case 'upgrade': $text = _n( 'Update Available (%s)', 'Update Available (%s)', $count ); break; @@ -647,6 +657,10 @@ public function single_row( $item ) { /* translators: %s: plugin name */ $actions['deactivate'] = '' . __( 'Deactivate' ) . ''; } + if ( current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) { + /* translators: %s: plugin name */ + $actions['resume'] = '' . __( 'Resume execution' ) . ''; + } } else { if ( current_user_can( 'activate_plugin', $plugin_file ) ) { /* translators: %s: plugin name */ @@ -753,6 +767,11 @@ public function single_row( $item ) { $class .= ' update'; } + $paused = is_plugin_paused( $plugin_file ); + if ( $paused ) { + $class .= ' paused'; + } + $plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_name ); printf( '
%s
', + __( 'This plugin failed to load properly and was paused within the admin backend.' ) + ); + + $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'] ]; + + echo sprintf( + '%s
{$error['type']}",
+ "{$error['line']}",
+ "{$error['file']}",
+ "{$error['message']}"
+ )
+ );
+ }
+ }
+
+
+ echo '';
break;
default:
$classes = "$column_name column-$column_name $class";
@@ -869,7 +924,7 @@ public function single_row( $item ) {
* @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 );
@@ -885,7 +940,7 @@ public function single_row( $item ) {
* @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 );
}
diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php
index c898fc516936a..ba9a5d2c99170 100644
--- a/src/wp-admin/includes/plugin.php
+++ b/src/wp-admin/includes/plugin.php
@@ -444,6 +444,8 @@ function _get_dropins() {
'install.php' => array( __( 'Custom installation script.' ), true ), // auto on installation
'maintenance.php' => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance
'object-cache.php' => array( __( 'External object cache.' ), true ), // auto on load
+ 'php-error.php' => array( __( 'Custom PHP error message.' ), true ), // auto on error
+ 'shutdown-handler' => array( __( 'Custom PHP shutdown handler.' ), true ), // auto on error
);
if ( is_multisite() ) {
@@ -496,6 +498,57 @@ function is_plugin_inactive( $plugin ) {
return ! is_plugin_active( $plugin );
}
+/**
+ * Determines whether a plugin is technically active but was paused while
+ * loading.
+ *
+ * For more information on this and similar theme functions, check out
+ * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
+ * Conditional Tags} article in the Theme Developer Handbook.
+ *
+ * @since 5.0.0
+ *
+ * @param string $plugin Path to the plugin file relative to the plugins directory.
+ * @return bool True, if in the active plugins list. False, not in the list.
+ */
+function is_plugin_paused( $plugin ) {
+ if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
+ return false;
+ }
+
+ if ( ! is_plugin_active( $plugin ) || is_plugin_active_for_network( $plugin ) ) {
+ return false;
+ }
+
+ list( $plugin ) = explode( '/', $plugin );
+
+ return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] );
+}
+
+/**
+ * Gets the error that was recorded for a paused plugin.
+ *
+ * @since 5.0.0
+ *
+ * @param string $plugin Path to the plugin file relative to the plugins
+ * directory.
+ * @return array|false Array of error information as it was returned by
+ * `error_get_last()`, or false if none was recorded.
+ */
+function wp_get_plugin_error( $plugin ) {
+ if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
+ return false;
+ }
+
+ list( $plugin ) = explode( '/', $plugin );
+
+ if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) {
+ return false;
+ }
+
+ return $GLOBALS['_paused_plugins'][ $plugin ];
+}
+
/**
* Determines whether the plugin is active for the entire network.
*
@@ -693,6 +746,11 @@ function deactivate_plugins( $plugins, $silent = false, $network_wide = null ) {
continue;
}
+ // Clean up the database before deactivating the plugin.
+ if ( is_plugin_paused( $plugin ) ) {
+ resume_plugin( $plugin );
+ }
+
$network_deactivating = false !== $network_wide && is_plugin_active_for_network( $plugin );
if ( ! $silent ) {
@@ -887,6 +945,11 @@ function delete_plugins( $plugins, $deprecated = '' ) {
uninstall_plugin( $plugin_file );
}
+ // Clean up the database before removing the plugin.
+ if ( is_plugin_paused( $plugin_file ) ) {
+ resume_plugin( $plugin_file );
+ }
+
/**
* Fires immediately before a plugin deletion attempt.
*
@@ -959,6 +1022,28 @@ function delete_plugins( $plugins, $deprecated = '' ) {
return true;
}
+/**
+ * Resumes a single plugin.
+ *
+ * Resuming the plugin basically means removing its entry from the
+ * `pause_on_admin` database option.
+ *
+ * @since 5.0.0
+ *
+ * @param string $plugin Single plugin to resume.
+ *
+ * @return bool|WP_Error True on success, false if `$plugin` was not paused, `WP_Error` on failure.
+ */
+function resume_plugin( $plugin ) {
+ $result = wp_forget_extension_error( 'plugins', $plugin );
+
+ if ( ! $result ) {
+ return new WP_Error( 'could_not_resume_plugin', __( 'Could not resume execution of the plugin.' ) );
+ }
+
+ return true;
+}
+
/**
* Validate active plugins
*
@@ -2066,3 +2151,35 @@ function wp_add_privacy_policy_content( $plugin_name, $policy_text ) {
WP_Privacy_Policy_Content::add( $plugin_name, $policy_text );
}
+
+/**
+ * Renders an admin notice in case some plugins have been paused due to errors.
+ *
+ * @since 5.0.0
+ *
+ * @return void
+ */
+function paused_plugins_notice() {
+ if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'deactivate_plugins' ) ) {
+ return;
+ }
+
+ if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) {
+ return;
+ }
+
+ echo sprintf(
+ '%s
%s
%s
deactivated.' ); ?>
resumed.' ); ?>
%s
', + __( 'The site is experiencing technical difficulties.' ) + ); + + if ( function_exists( 'get_admin_url' ) ) { + $url = get_admin_url(); + $message .= sprintf( + '%s %s
', + __( 'Are you the site owner?' ), + $url, + __( 'Log into the admin backend to fix this.' ) + ); + } + + if ( function_exists( 'apply_filters' ) ) { + /** + * Filters the message that the default PHP error page displays. + * + * @since 5.0.0 + * + * @param string $message HTML error message to display. + */ + $message = apply_filters( 'wp_technical_issues_display', $message ); + } + + wp_die( $message ); + } catch ( Exception $exception ) { + // Catch exceptions and remain silent. + } +} + +/** + * Registers the WordPress premature shutdown handler. + * + * @since 5.0.0 + * + * @return void + */ +function wp_register_premature_shutdown_handler() { + register_shutdown_function( 'wp_shutdown_handler_wrapper' ); +} diff --git a/src/wp-settings.php b/src/wp-settings.php index eb632238f031c..c633f05b1d5f9 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -20,6 +20,9 @@ require( ABSPATH . WPINC . '/default-constants.php' ); require_once( ABSPATH . WPINC . '/plugin.php' ); +// Make sure we register the premature shutdown handler as soon as possible. +wp_register_premature_shutdown_handler(); + /* * These can't be directly globalized in version.php. When updating, * we're including version.php from another installation and don't want @@ -40,6 +43,16 @@ // Set initial default constants including WP_MEMORY_LIMIT, WP_MAX_MEMORY_LIMIT, WP_DEBUG, SCRIPT_DEBUG, WP_CONTENT_DIR and WP_CACHE. wp_initial_constants(); +/* + * Allow an optional shutdown handler to be included through a pluggable file. + * This file should register a function `wp_handle_shutdown( $context )` that + * returns a boolean value. If the return value evaluates to false, the default + * shutdown handler will not be executed. + */ +if ( is_readable( WP_CONTENT_DIR . '/shutdown-handler.php' ) ) { + include WP_CONTENT_DIR . '/shutdown-handler.php'; +} + // Check for the required PHP version and for the MySQL extension or a database drop-in. wp_check_php_mysql_versions(); @@ -482,3 +495,12 @@ * @since 3.0.0 */ do_action( 'wp_loaded' ); + +/* + * Store the fact that we could successfully execute the entire WordPress + * lifecycle. This is used to skip the premature shutdown handler, as it cannot + * be unregistered. + */ +if ( ! defined( 'WP_EXECUTION_SUCCEEDED' ) ) { + define( 'WP_EXECUTION_SUCCEEDED', true ); +}