diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index 992f69c2f8..6dcac9d56f 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 a1efcc62f4..a25eda7f42 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -123,6 +123,8 @@ add_action( 'load-plugins.php', 'wp_plugin_update_rows', 20 ); // After wp_updat 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', 'paused_themes_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 1f540c9ed2..a9063223f9 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 @@ 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 (%s)', 'Drop-ins (%s)', $count ); break; + case 'paused': + /* translators: %s: plugin count */ + $text = _n( 'Paused (%s)', 'Paused (%s)', $count ); + break; case 'upgrade': $text = _n( 'Update Available (%s)', 'Update Available (%s)', $count ); break; @@ -625,11 +636,19 @@ class WP_Plugins_List_Table extends WP_List_Table { /* translators: %s: plugin name */ $actions['deactivate'] = '' . __( 'Network Deactivate' ) . ''; } + if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) { + /* translators: %s: plugin name */ + $actions['resume'] = '' . __( 'Network Resume' ) . ''; + } } else { if ( current_user_can( 'manage_network_plugins' ) ) { /* translators: %s: plugin name */ $actions['activate'] = '' . __( 'Network Activate' ) . ''; } + if ( current_user_can( 'manage_network_plugins' ) && count_paused_plugin_sites_for_network( $plugin_file ) ) { + /* translators: %s: plugin name */ + $actions['resume'] = '' . __( 'Network Resume' ) . ''; + } if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) { /* translators: %s: plugin name */ $actions['delete'] = '' . __( 'Delete' ) . ''; @@ -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'] = '' . __( 'Resume' ) . ''; + } } 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'] = '' . __( 'Deactivate' ) . ''; } + if ( current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) { + /* translators: %s: plugin name */ + $actions['resume'] = '' . __( 'Resume' ) . ''; + } } 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( '
%s
', $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( + '%s
{$error['type']}",
+ "{$error['line']}",
+ "{$error['file']}",
+ "{$error['message']}"
+ )
+ );
+ }
+ }
+
+ echo '';
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 );
}
diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php
index c898fc5169..412c5330e4 100644
--- a/src/wp-admin/includes/plugin.php
+++ b/src/wp-admin/includes/plugin.php
@@ -438,12 +438,14 @@ function get_dropins() {
*/
function _get_dropins() {
$dropins = array(
- 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
- 'db.php' => array( __( 'Custom database class.' ), true ), // auto on load
- 'db-error.php' => array( __( 'Custom database error message.' ), true ), // auto on error
- '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
+ 'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
+ 'db.php' => array( __( 'Custom database class.' ), true ), // auto on load
+ 'db-error.php' => array( __( 'Custom database error message.' ), true ), // auto on error
+ '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.php' => array( __( 'Custom PHP shutdown handler.' ), true ), // auto on error
);
if ( is_multisite() ) {
@@ -496,6 +498,84 @@ 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.1.0
+ *
+ * @param string $plugin Path to the plugin file relative to the plugins directory.
+ * @return bool True, if in the list of paused plugins. 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.1.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 ];
+}
+
+/**
+ * Gets the number of sites on which a specific plugin is paused.
+ *
+ * @since 5.1.0
+ *
+ * @param string $plugin Path to the plugin file relative to the plugins directory.
+ * @return int Site count.
+ */
+function count_paused_plugin_sites_for_network( $plugin ) {
+ if ( ! is_multisite() ) {
+ return is_plugin_paused( $plugin ) ? 1 : 0;
+ }
+
+ list( $plugin ) = explode( '/', $plugin );
+
+ $query_args = array(
+ 'count' => true,
+ 'number' => 0,
+ 'network_id' => get_current_network_id(),
+ 'meta_query' => array(
+ wp_paused_plugins()->get_site_meta_query_clause( $plugin ),
+ ),
+ );
+
+ return get_sites( $query_args );
+}
+
/**
* Determines whether the plugin is active for the entire network.
*
@@ -693,6 +773,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 +972,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 +1049,57 @@ function delete_plugins( $plugins, $deprecated = '' ) {
return true;
}
+/**
+ * Tries to resume a single plugin.
+ *
+ * If a redirect was provided, we first ensure the plugin does not throw fatal
+ * errors anymore.
+ *
+ * The way it works is by setting the redirection to the error before trying to
+ * include the plugin file. If the plugin fails, then the redirection will not
+ * be overwritten with the success message and the plugin will not be resumed.
+ *
+ * @since 5.1.0
+ *
+ * @param string $plugin Single plugin to resume.
+ * @param string $redirect Optional. URL to redirect to. Default empty string.
+ * @param bool $network_wide Optional. Whether to resume the plugin for the entire
+ * network. Default false.
+ * @return bool|WP_Error True on success, false if `$plugin` was not paused,
+ * `WP_Error` on failure.
+ */
+function resume_plugin( $plugin, $redirect = '', $network_wide = false ) {
+ /*
+ * We'll override this later if the plugin could be included without
+ * creating a fatal error.
+ */
+ if ( ! empty( $redirect ) ) {
+ wp_redirect(
+ add_query_arg(
+ '_error_nonce',
+ wp_create_nonce( 'plugin-resume-error_' . $plugin ),
+ $redirect
+ )
+ );
+
+ // Load the plugin to test whether it throws a fatal error.
+ ob_start();
+ plugin_sandbox_scrape( $plugin );
+ ob_clean();
+ }
+
+ $result = wp_forget_extension_error( 'plugins', $plugin, $network_wide );
+
+ if ( ! $result ) {
+ return new WP_Error(
+ 'could_not_resume_plugin',
+ __( 'Could not resume the plugin.' )
+ );
+ }
+
+ return true;
+}
+
/**
* Validate active plugins
*
@@ -2066,3 +2207,33 @@ 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.1.0
+ */
+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;
+ }
+
+ printf(
+ '%s
%s
%s
%s
%s
%s
deactivated.' ); ?>
resumed.' ); ?>
' . __( 'Sorry, you are not allowed to resume this theme.' ) . '
', + 403 + ); + } + + $result = resume_theme( $theme->get_stylesheet() ); + + if ( is_wp_error( $result ) ) { + wp_die( $result ); + } + + wp_redirect( admin_url( 'themes.php?resumed=true' ) ); + exit; } elseif ( 'delete' == $_GET['action'] ) { check_admin_referer( 'delete-theme_' . $_GET['stylesheet'] ); $theme = wp_get_theme( $_GET['stylesheet'] ); @@ -173,25 +193,33 @@ require_once( ABSPATH . 'wp-admin/admin-header.php' );' && substr( $message, -4 ) === '
' ) { + $message = substr( $message, 3, -4 ); + } + + wp_die( $message, '', 500 ); + } + + /** + * Returns the error message markup to display in the default error template. + * + * @since 5.1.0 + * + * @return string Error message HTML output. + */ + protected function get_error_message_markup() { + if ( ! function_exists( '__' ) ) { + function __( $text ) { + return $text; + } + } + + $message = sprintf( + '%s
', + __( 'The site is experiencing technical difficulties.' ) + ); + + if ( function_exists( 'admin_url' ) ) { + $message .= sprintf( + '%s %s
', + __( 'Are you the site owner?' ), + admin_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.1.0 + * + * @param string $message HTML error message to display. + */ + $message = apply_filters( 'wp_technical_issues_display', $message ); + } + + return $message; + } +} diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php index a9ddb2251a..8b9c2c8815 100644 --- a/src/wp-includes/class-wp-theme.php +++ b/src/wp-includes/class-wp-theme.php @@ -371,6 +371,10 @@ final class WP_Theme implements ArrayAccess { $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this ); } + if ( wp_paused_themes()->get( $this->stylesheet ) && ( ! is_wp_error( $this->errors ) || ! isset( $this->errors->errors['theme_paused'] ) ) ) { + $this->errors = new WP_Error( 'theme_paused', __( 'This theme failed to load properly and was paused within the admin backend.' ) ); + } + // We're good. If we didn't retrieve from cache, set it. if ( ! is_array( $cache ) ) { $cache = array( diff --git a/src/wp-includes/error-protection.php b/src/wp-includes/error-protection.php new file mode 100644 index 0000000000..f12bd2409f --- /dev/null +++ b/src/wp-includes/error-protection.php @@ -0,0 +1,166 @@ +record( $extension, $error ); +} + +/** + * Forgets a previously recorded extension error again. + * + * @since 5.1.0 + * + * @param string $type Type of the extension. + * @param string $extension Relative path of the extension. + * @param bool $network_wide Optional. Whether to resume the plugin for the entire + * network. Default false. + * @return bool Whether the extension error was successfully forgotten. + */ +function wp_forget_extension_error( $type, $extension, $network_wide = false ) { + switch ( $type ) { + case 'plugins': + $callback = 'wp_paused_plugins'; + list( $extension ) = explode( '/', $extension ); + break; + case 'themes': + $callback = 'wp_paused_themes'; + list( $extension ) = explode( '/', $extension ); + break; + } + + if ( empty( $callback ) || empty( $extension ) ) { + return false; + } + + // Handle manually since the regular APIs do not expose this functionality. + if ( $network_wide && is_site_meta_supported() ) { + $site_meta_query_clause = call_user_func( $callback )->get_site_meta_query_clause( $extension ); + return delete_metadata( 'blog', 0, $site_meta_query_clause['key'], '', true ); + } + + return call_user_func( $callback )->forget( $extension ); +} + +/** + * Determines whether we are dealing with an error that WordPress should handle + * in order to protect the admin backend against WSODs. + * + * @param array $error Error information retrieved from error_get_last(). + * + * @return bool Whether WordPress should handle this error. + */ +function wp_should_handle_error( $error ) { + if ( ! isset( $error['type'] ) ) { + return false; + } + + $error_types_to_handle = array( + E_ERROR, + E_PARSE, + E_USER_ERROR, + E_COMPILE_ERROR, + E_RECOVERABLE_ERROR, + ); + + return in_array( $error['type'], $error_types_to_handle, true ); +} + +/** + * Registers the WordPress premature shutdown handler. + * + * @since 5.1.0 + */ +function wp_register_premature_shutdown_handler() { + $handler = null; + if ( defined( 'WP_CONTENT_DIR' ) && is_readable( WP_CONTENT_DIR . '/shutdown-handler.php' ) ) { + $handler = include WP_CONTENT_DIR . '/shutdown-handler.php'; + } + + if ( ! is_object( $handler ) || ! is_callable( array( $handler, 'handle' ) ) ) { + $handler = new WP_Shutdown_Handler(); + } + + register_shutdown_function( array( $handler, 'handle' ) ); +} diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index e959d79db8..e404a68f9f 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -696,9 +696,117 @@ function wp_get_active_and_valid_plugins() { $plugins[] = WP_PLUGIN_DIR . '/' . $plugin; } } + + /* + * Remove plugins from the list of active plugins when we're on an endpoint + * that should be protected against WSODs and the plugin is paused. + */ + if ( is_protected_endpoint() ) { + $plugins = wp_skip_paused_plugins( $plugins ); + } + return $plugins; } +/** + * Filters a given list of plugins, removing any paused plugins from it. + * + * @since 5.1.0 + * + * @param array $plugins List of absolute plugin main file paths. + * @return array Filtered value of $plugins, without any paused plugins. + */ +function wp_skip_paused_plugins( array $plugins ) { + $paused_plugins = wp_paused_plugins()->get_all(); + + if ( empty( $paused_plugins ) ) { + return $plugins; + } + + foreach ( $plugins as $index => $plugin ) { + list( $plugin ) = explode( '/', plugin_basename( $plugin ) ); + + if ( array_key_exists( $plugin, $paused_plugins ) ) { + unset( $plugins[ $index ] ); + + // Store list of paused plugins for displaying an admin notice. + $GLOBALS['_paused_plugins'][ $plugin ] = $paused_plugins[ $plugin ]; + } + } + + return $plugins; +} + +/** + * Retrieves an array of active and valid themes. + * + * While upgrading or installing WordPress, no themes are returned. + * + * @since 5.1.0 + * @access private + * + * @return array Array of paths to theme directories. + */ +function wp_get_active_and_valid_themes() { + global $pagenow; + + $themes = array(); + + if ( wp_installing() && 'wp-activate.php' !== $pagenow ) { + return $themes; + } + + if ( TEMPLATEPATH !== STYLESHEETPATH ) { + $themes[] = STYLESHEETPATH; + } + + $themes[] = TEMPLATEPATH; + + /* + * Remove themes from the list of active themes when we're on an endpoint + * that should be protected against WSODs and the theme is paused. + */ + if ( is_protected_endpoint() ) { + $themes = wp_skip_paused_themes( $themes ); + + // If no active and valid themes exist, skip loading themes. + if ( empty( $themes ) ) { + add_filter( 'wp_using_themes', '__return_false' ); + } + } + + return $themes; +} + +/** + * Filters a given list of themes, removing any paused themes from it. + * + * @since 5.1.0 + * + * @param array $themes List of absolute theme directory paths. + * @return array Filtered value of $themes, without any paused themes. + */ +function wp_skip_paused_themes( array $themes ) { + $paused_themes = wp_paused_themes()->get_all(); + + if ( empty( $paused_themes ) ) { + return $themes; + } + + foreach ( $themes as $index => $theme ) { + $theme = basename( $theme ); + + if ( array_key_exists( $theme, $paused_themes ) ) { + unset( $themes[ $index ] ); + + // Store list of paused themes for displaying an admin notice. + $GLOBALS['_paused_themes'][ $theme ] = $paused_themes[ $theme ]; + } + } + + return $themes; +} + /** * Set internal encoding. * @@ -1163,6 +1271,106 @@ function wp_doing_ajax() { return apply_filters( 'wp_doing_ajax', defined( 'DOING_AJAX' ) && DOING_AJAX ); } +/** + * Determines whether the current request should use themes. + * + * @since 5.1.0 + * + * @return bool True if themes should be used, false otherwise. + */ +function wp_using_themes() { + /** + * Filters whether the current request should use themes. + * + * @since 5.1.0 + * + * @param bool $wp_using_themes Whether the current request should use themes. + */ + return apply_filters( 'wp_using_themes', defined( 'WP_USE_THEMES' ) && WP_USE_THEMES ); +} + +/** + * Determines whether we are currently on an endpoint that should be protected against WSODs. + * + * @since 5.1.0 + * + * @return bool True if the current endpoint should be protected. + */ +function is_protected_endpoint() { + // Protect login pages. + if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) { + return true; + } + + // Protect the admin backend. + if ( is_admin() && ! wp_doing_ajax() ) { + return true; + } + + // Protect AJAX actions that could help resolve a fatal error should be available. + if ( is_protected_ajax_action() ) { + return true; + } + + /** + * Filters whether the current request is against a protected endpoint. + * + * This filter is only fired when an endpoint is requested which is not already protected by + * WordPress core. As such, it exclusively allows providing further protected endpoints in + * addition to the admin backend, login pages and protected AJAX actions. + * + * @since 5.1.0 + * + * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected. Default false. + */ + return (bool) apply_filters( 'is_protected_endpoint', false ); +} + +/** + * Determines whether we are currently handling an AJAX action that should be protected against WSODs. + * + * @since 5.1.0 + * + * @return bool True if the current AJAX action should be protected. + */ +function is_protected_ajax_action() { + if ( ! wp_doing_ajax() ) { + return false; + } + + if ( ! isset( $_REQUEST['action'] ) ) { + return false; + } + + $actions_to_protect = array( + 'edit-theme-plugin-file', // Saving changes in the core code editor. + 'heartbeat', // Keep the heart beating. + 'install-plugin', // Installing a new plugin. + 'install-theme', // Installing a new theme. + 'search-plugins', // Searching in the list of plugins. + 'search-install-plugins', // Searching for a plugin in the plugin install screen. + 'update-plugin', // Update an existing plugin. + 'update-theme', // Update an existing theme. + ); + + /** + * Filters the array of protected AJAX actions. + * + * This filter is only fired when doing AJAX and the AJAX request has an 'action' property. + * + * @since 5.1.0 + * + * @param array $actions_to_protect Array of strings with AJAX actions to protect. + */ + $actions_to_protect = (array) apply_filters( 'wp_protected_ajax_actions', $actions_to_protect ); + + if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) { + return false; + } + + return true; +} + /** * Determines whether the current request is a WordPress cron request. * diff --git a/src/wp-includes/ms-load.php b/src/wp-includes/ms-load.php index 932ae76ca5..4f630cce27 100644 --- a/src/wp-includes/ms-load.php +++ b/src/wp-includes/ms-load.php @@ -52,6 +52,15 @@ function wp_get_active_network_plugins() { $plugins[] = WP_PLUGIN_DIR . '/' . $plugin; } } + + /* + * Remove plugins from the list of active plugins when we're on an endpoint + * that should be protected against WSODs and the plugin is paused. + */ + if ( is_protected_endpoint() ) { + $plugins = wp_skip_paused_plugins( $plugins ); + } + return $plugins; } diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php index 0d2bafae44..0879798f55 100644 --- a/src/wp-includes/template-loader.php +++ b/src/wp-includes/template-loader.php @@ -4,7 +4,7 @@ * * @package WordPress */ -if ( defined( 'WP_USE_THEMES' ) && WP_USE_THEMES ) { +if ( wp_using_themes() ) { /** * Fires before determining which template to load. * @@ -44,7 +44,7 @@ elseif ( is_trackback() ) : return; endif; -if ( defined( 'WP_USE_THEMES' ) && WP_USE_THEMES ) : +if ( wp_using_themes() ) : $template = false; if ( is_embed() && $template = get_embed_template() ) : elseif ( is_404() && $template = get_404_template() ) : diff --git a/src/wp-settings.php b/src/wp-settings.php index ca7d0b1a14..781d8e6ca1 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -17,9 +17,15 @@ define( 'WPINC', 'wp-includes' ); // Include files required for initialization. require( ABSPATH . WPINC . '/load.php' ); +require( ABSPATH . WPINC . '/class-wp-paused-extensions-storage.php' ); +require( ABSPATH . WPINC . '/class-wp-shutdown-handler.php' ); +require( ABSPATH . WPINC . '/error-protection.php' ); 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 @@ -474,14 +480,12 @@ $GLOBALS['wp_locale_switcher'] = new WP_Locale_Switcher(); $GLOBALS['wp_locale_switcher']->init(); // Load the functions for the active theme, for both parent and child theme if applicable. -if ( ! wp_installing() || 'wp-activate.php' === $pagenow ) { - if ( TEMPLATEPATH !== STYLESHEETPATH && file_exists( STYLESHEETPATH . '/functions.php' ) ) { - include( STYLESHEETPATH . '/functions.php' ); - } - if ( file_exists( TEMPLATEPATH . '/functions.php' ) ) { - include( TEMPLATEPATH . '/functions.php' ); +foreach ( wp_get_active_and_valid_themes() as $theme ) { + if ( file_exists( $theme . '/functions.php' ) ) { + include $theme . '/functions.php'; } } +unset( $theme ); /** * Fires after the theme is loaded. @@ -526,3 +530,12 @@ if ( is_multisite() ) { * @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 ); +}