Bootstrap/Load: Introduce a recovery mode for fixing fatal errors.
Using the new fatal handler introduced in [44962], an email is sent to the admin when a fatal error occurs. This email includes a secret link to enter recovery mode. When clicked, the link will be validated and on success a cookie will be placed on the client, enabling recovery mode for that user. This functionality is executed early before plugins and themes are loaded, in order to be unaffected by potential fatal errors these might be causing.
When in recovery mode, broken plugins and themes will be paused for that client, so that they are able to access the admin backend despite of these errors. They are notified about the broken extensions and the errors caused, and can then decide whether they would like to temporarily deactivate the extension or fix the problem and resume the extension.
A link in the admin bar allows the client to exit recovery mode.
Props timothyblynjacobs, afragen, flixos90, nerrad, miss_jwo, schlessera, spacedmonkey, swissspidy.
Fixes #46130, #44458.
Built from https://develop.svn.wordpress.org/trunk@44973
git-svn-id: http://core.svn.wordpress.org/trunk@44804 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2019-03-21 17:53:51 -04:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Error Protection API: WP_Paused_Extensions_Storage class
|
|
|
|
*
|
|
|
|
* @package WordPress
|
2020-01-28 19:45:18 -05:00
|
|
|
* @since 5.2.0
|
Bootstrap/Load: Introduce a recovery mode for fixing fatal errors.
Using the new fatal handler introduced in [44962], an email is sent to the admin when a fatal error occurs. This email includes a secret link to enter recovery mode. When clicked, the link will be validated and on success a cookie will be placed on the client, enabling recovery mode for that user. This functionality is executed early before plugins and themes are loaded, in order to be unaffected by potential fatal errors these might be causing.
When in recovery mode, broken plugins and themes will be paused for that client, so that they are able to access the admin backend despite of these errors. They are notified about the broken extensions and the errors caused, and can then decide whether they would like to temporarily deactivate the extension or fix the problem and resume the extension.
A link in the admin bar allows the client to exit recovery mode.
Props timothyblynjacobs, afragen, flixos90, nerrad, miss_jwo, schlessera, spacedmonkey, swissspidy.
Fixes #46130, #44458.
Built from https://develop.svn.wordpress.org/trunk@44973
git-svn-id: http://core.svn.wordpress.org/trunk@44804 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2019-03-21 17:53:51 -04:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Core class used for storing paused extensions.
|
|
|
|
*
|
|
|
|
* @since 5.2.0
|
|
|
|
*/
|
|
|
|
class WP_Paused_Extensions_Storage {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Type of extension. Used to key extension storage.
|
|
|
|
*
|
|
|
|
* @since 5.2.0
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $type;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
*
|
|
|
|
* @since 5.2.0
|
|
|
|
*
|
|
|
|
* @param string $extension_type Extension type. Either 'plugin' or 'theme'.
|
|
|
|
*/
|
|
|
|
public function __construct( $extension_type ) {
|
|
|
|
$this->type = $extension_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.2.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 set( $extension, $error ) {
|
|
|
|
if ( ! $this->is_api_loaded() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$option_name = $this->get_option_name();
|
|
|
|
|
|
|
|
if ( ! $option_name ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$paused_extensions = (array) get_option( $option_name, array() );
|
|
|
|
|
|
|
|
// Do not update if the error is already stored.
|
|
|
|
if ( isset( $paused_extensions[ $this->type ][ $extension ] ) && $paused_extensions[ $this->type ][ $extension ] === $error ) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$paused_extensions[ $this->type ][ $extension ] = $error;
|
|
|
|
|
|
|
|
return update_option( $option_name, $paused_extensions );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forgets a previously recorded extension error.
|
|
|
|
*
|
|
|
|
* @since 5.2.0
|
|
|
|
*
|
|
|
|
* @param string $extension Plugin or theme directory name.
|
|
|
|
*
|
|
|
|
* @return bool True on success, false on failure.
|
|
|
|
*/
|
|
|
|
public function delete( $extension ) {
|
|
|
|
if ( ! $this->is_api_loaded() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$option_name = $this->get_option_name();
|
|
|
|
|
|
|
|
if ( ! $option_name ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$paused_extensions = (array) get_option( $option_name, array() );
|
|
|
|
|
|
|
|
// Do not delete if no error is stored.
|
|
|
|
if ( ! isset( $paused_extensions[ $this->type ][ $extension ] ) ) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
unset( $paused_extensions[ $this->type ][ $extension ] );
|
|
|
|
|
|
|
|
if ( empty( $paused_extensions[ $this->type ] ) ) {
|
|
|
|
unset( $paused_extensions[ $this->type ] );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up the entire option if we're removing the only error.
|
|
|
|
if ( ! $paused_extensions ) {
|
|
|
|
return delete_option( $option_name );
|
|
|
|
}
|
|
|
|
|
|
|
|
return update_option( $option_name, $paused_extensions );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the error for an extension, if paused.
|
|
|
|
*
|
|
|
|
* @since 5.2.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
$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.2.0
|
|
|
|
*
|
|
|
|
* @return array Associative array of extension slugs to the error recorded.
|
|
|
|
*/
|
|
|
|
public function get_all() {
|
|
|
|
if ( ! $this->is_api_loaded() ) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
$option_name = $this->get_option_name();
|
|
|
|
|
|
|
|
if ( ! $option_name ) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
$paused_extensions = (array) get_option( $option_name, array() );
|
|
|
|
|
|
|
|
return isset( $paused_extensions[ $this->type ] ) ? $paused_extensions[ $this->type ] : array();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove all paused extensions.
|
|
|
|
*
|
|
|
|
* @since 5.2.0
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function delete_all() {
|
|
|
|
if ( ! $this->is_api_loaded() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$option_name = $this->get_option_name();
|
|
|
|
|
|
|
|
if ( ! $option_name ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$paused_extensions = (array) get_option( $option_name, array() );
|
|
|
|
|
|
|
|
unset( $paused_extensions[ $this->type ] );
|
|
|
|
|
|
|
|
if ( ! $paused_extensions ) {
|
|
|
|
return delete_option( $option_name );
|
|
|
|
}
|
|
|
|
|
|
|
|
return update_option( $option_name, $paused_extensions );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether the underlying API to store paused extensions is loaded.
|
|
|
|
*
|
|
|
|
* @since 5.2.0
|
|
|
|
*
|
|
|
|
* @return bool True if the API is loaded, false otherwise.
|
|
|
|
*/
|
|
|
|
protected function is_api_loaded() {
|
|
|
|
return function_exists( 'get_option' );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the option name for storing paused extensions.
|
|
|
|
*
|
|
|
|
* @since 5.2.0
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function get_option_name() {
|
|
|
|
if ( ! wp_recovery_mode()->is_active() ) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
$session_id = wp_recovery_mode()->get_session_id();
|
|
|
|
if ( empty( $session_id ) ) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
return "{$session_id}_paused_extensions";
|
|
|
|
}
|
|
|
|
}
|