WordPress/wp-includes/class-wp-paused-extensions-storage.php
Felix Arntz 1e4088fc98 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.

Built from https://develop.svn.wordpress.org/trunk@44524


git-svn-id: http://core.svn.wordpress.org/trunk@44355 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2019-01-09 20:05:49 +00:00

222 lines
5.4 KiB
PHP

<?php
/**
* Error Protection API: WP_Paused_Extensions_Storage class
*
* @package WordPress
* @since 5.1.0
*/
/**
* Core class used for storing paused extensions.
*
* @since 5.1.0
*/
class WP_Paused_Extensions_Storage {
/**
* Option name for storing paused extensions.
*
* @since 5.1.0
* @var string
*/
protected $option_name;
/**
* Prefix for paused extensions stored as site metadata.
*
* @since 5.1.0
* @var string
*/
protected $meta_prefix;
/**
* Constructor.
*
* @since 5.1.0
*
* @param string $option_name Option name for storing paused extensions.
* @param string $meta_prefix Prefix for paused extensions stored as site metadata.
*/
public function __construct( $option_name, $meta_prefix ) {
$this->option_name = $option_name;
$this->meta_prefix = $meta_prefix;
}
/**
* Records an extension error.
*
* Only one error is stored per extension, with subsequent errors for the same extension overriding the
* previously stored error.
*
* @since 5.1.0
*
* @param string $extension Plugin or theme directory name.
* @param array $error {
* Error that was triggered.
*
* @type string $type The error type.
* @type string $file The name of the file in which the error occurred.
* @type string $line The line number in which the error occurred.
* @type string $message The error message.
* }
* @return bool True on success, false on failure.
*/
public function record( $extension, $error ) {
if ( ! $this->is_api_loaded() ) {
return false;
}
if ( is_multisite() && is_site_meta_supported() ) {
// Do not update if the error is already stored.
if ( get_site_meta( get_current_blog_id(), $this->meta_prefix . $extension, true ) === $error ) {
return true;
}
return (bool) update_site_meta( get_current_blog_id(), $this->meta_prefix . $extension, $error );
}
$paused_extensions = $this->get_all();
// Do not update if the error is already stored.
if ( isset( $paused_extensions[ $extension ] ) && $paused_extensions[ $extension ] === $error ) {
return true;
}
$paused_extensions[ $extension ] = $error;
return update_option( $this->option_name, $paused_extensions );
}
/**
* Forgets a previously recorded extension error.
*
* @since 5.1.0
*
* @param string $extension Plugin or theme directory name.
* @return bool True on success, false on failure.
*/
public function forget( $extension ) {
if ( ! $this->is_api_loaded() ) {
return false;
}
if ( is_multisite() && is_site_meta_supported() ) {
// Do not delete if no error is stored.
if ( get_site_meta( get_current_blog_id(), $this->meta_prefix . $extension ) === array() ) {
return true;
}
return (bool) delete_site_meta( get_current_blog_id(), $this->meta_prefix . $extension );
}
$paused_extensions = $this->get_all();
// Do not delete if no error is stored.
if ( ! isset( $paused_extensions[ $extension ] ) ) {
return true;
}
// Clean up the entire option if we're removing the only error.
if ( count( $paused_extensions ) === 1 ) {
return delete_option( $this->option_name );
}
unset( $paused_extensions[ $extension ] );
return update_option( $this->option_name, $paused_extensions );
}
/**
* Gets the error for an extension, if paused.
*
* @since 5.1.0
*
* @param string $extension Plugin or theme directory name.
* @return array|null Error that is stored, or null if the extension is not paused.
*/
public function get( $extension ) {
if ( ! $this->is_api_loaded() ) {
return null;
}
if ( is_multisite() && is_site_meta_supported() ) {
$error = get_site_meta( get_current_blog_id(), $this->meta_prefix . $extension, true );
if ( ! $error ) {
return null;
}
return $error;
}
$paused_extensions = $this->get_all();
if ( ! isset( $paused_extensions[ $extension ] ) ) {
return null;
}
return $paused_extensions[ $extension ];
}
/**
* Gets the paused extensions with their errors.
*
* @since 5.1.0
*
* @return array Associative array of $extension => $error pairs.
*/
public function get_all() {
if ( ! $this->is_api_loaded() ) {
return array();
}
if ( is_multisite() && is_site_meta_supported() ) {
$site_metadata = get_site_meta( get_current_blog_id() );
$paused_extensions = array();
foreach ( $site_metadata as $meta_key => $meta_values ) {
if ( 0 !== strpos( $meta_key, $this->meta_prefix ) ) {
continue;
}
$error = maybe_unserialize( array_shift( $meta_values ) );
$paused_extensions[ substr( $meta_key, strlen( $this->meta_prefix ) ) ] = $error;
}
return $paused_extensions;
}
return (array) get_option( $this->option_name, array() );
}
/**
* Gets the site meta query clause for querying sites with paused extensions.
*
* @since 5.1.0
*
* @param string $extension Plugin or theme directory name.
* @return array A single clause to add to a meta query.
*/
public function get_site_meta_query_clause( $extension ) {
return array(
'key' => $this->meta_prefix . $extension,
'compare_key' => '=',
);
}
/**
* Checks whether the underlying API to store paused extensions is loaded.
*
* @since 5.1.0
*
* @return bool True if the API is loaded, false otherwise.
*/
protected function is_api_loaded() {
if ( is_multisite() ) {
return function_exists( 'is_site_meta_supported' ) && function_exists( 'get_site_meta' );
}
return function_exists( 'get_option' );
}
}