[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-includes/ -> class-wp-recovery-mode.php (source)

   1  <?php
   2  /**
   3   * Error Protection API: WP_Recovery_Mode class
   4   *
   5   * @package WordPress
   6   * @since 5.2.0
   7   */
   8  
   9  /**
  10   * Core class used to implement Recovery Mode.
  11   *
  12   * @since 5.2.0
  13   */
  14  class WP_Recovery_Mode {
  15  
  16      const EXIT_ACTION = 'exit_recovery_mode';
  17  
  18      /**
  19       * Service to handle cookies.
  20       *
  21       * @since 5.2.0
  22       * @var WP_Recovery_Mode_Cookie_Service
  23       */
  24      private $cookie_service;
  25  
  26      /**
  27       * Service to generate a recovery mode key.
  28       *
  29       * @since 5.2.0
  30       * @var WP_Recovery_Mode_Key_Service
  31       */
  32      private $key_service;
  33  
  34      /**
  35       * Service to generate and validate recovery mode links.
  36       *
  37       * @since 5.2.0
  38       * @var WP_Recovery_Mode_Link_Service
  39       */
  40      private $link_service;
  41  
  42      /**
  43       * Service to handle sending an email with a recovery mode link.
  44       *
  45       * @since 5.2.0
  46       * @var WP_Recovery_Mode_Email_Service
  47       */
  48      private $email_service;
  49  
  50      /**
  51       * Is recovery mode initialized.
  52       *
  53       * @since 5.2.0
  54       * @var bool
  55       */
  56      private $is_initialized = false;
  57  
  58      /**
  59       * Is recovery mode active in this session.
  60       *
  61       * @since 5.2.0
  62       * @var bool
  63       */
  64      private $is_active = false;
  65  
  66      /**
  67       * Get an ID representing the current recovery mode session.
  68       *
  69       * @since 5.2.0
  70       * @var string
  71       */
  72      private $session_id = '';
  73  
  74      /**
  75       * WP_Recovery_Mode constructor.
  76       *
  77       * @since 5.2.0
  78       */
  79  	public function __construct() {
  80          $this->cookie_service = new WP_Recovery_Mode_Cookie_Service();
  81          $this->key_service    = new WP_Recovery_Mode_Key_Service();
  82          $this->link_service   = new WP_Recovery_Mode_Link_Service( $this->cookie_service, $this->key_service );
  83          $this->email_service  = new WP_Recovery_Mode_Email_Service( $this->link_service );
  84      }
  85  
  86      /**
  87       * Initialize recovery mode for the current request.
  88       *
  89       * @since 5.2.0
  90       */
  91  	public function initialize() {
  92          $this->is_initialized = true;
  93  
  94          add_action( 'wp_logout', array( $this, 'exit_recovery_mode' ) );
  95          add_action( 'login_form_' . self::EXIT_ACTION, array( $this, 'handle_exit_recovery_mode' ) );
  96          add_action( 'recovery_mode_clean_expired_keys', array( $this, 'clean_expired_keys' ) );
  97  
  98          if ( ! wp_next_scheduled( 'recovery_mode_clean_expired_keys' ) && ! wp_installing() ) {
  99              wp_schedule_event( time(), 'daily', 'recovery_mode_clean_expired_keys' );
 100          }
 101  
 102          if ( defined( 'WP_RECOVERY_MODE_SESSION_ID' ) ) {
 103              $this->is_active  = true;
 104              $this->session_id = WP_RECOVERY_MODE_SESSION_ID;
 105  
 106              return;
 107          }
 108  
 109          if ( $this->cookie_service->is_cookie_set() ) {
 110              $this->handle_cookie();
 111  
 112              return;
 113          }
 114  
 115          $this->link_service->handle_begin_link( $this->get_link_ttl() );
 116      }
 117  
 118      /**
 119       * Checks whether recovery mode is active.
 120       *
 121       * This will not change after recovery mode has been initialized. {@see WP_Recovery_Mode::run()}.
 122       *
 123       * @since 5.2.0
 124       *
 125       * @return bool True if recovery mode is active, false otherwise.
 126       */
 127  	public function is_active() {
 128          return $this->is_active;
 129      }
 130  
 131      /**
 132       * Gets the recovery mode session ID.
 133       *
 134       * @since 5.2.0
 135       *
 136       * @return string The session ID if recovery mode is active, empty string otherwise.
 137       */
 138  	public function get_session_id() {
 139          return $this->session_id;
 140      }
 141  
 142      /**
 143       * Checks whether recovery mode has been initialized.
 144       *
 145       * Recovery mode should not be used until this point. Initialization happens immediately before loading plugins.
 146       *
 147       * @since 5.2.0
 148       *
 149       * @return bool
 150       */
 151  	public function is_initialized() {
 152          return $this->is_initialized;
 153      }
 154  
 155      /**
 156       * Handles a fatal error occurring.
 157       *
 158       * The calling API should immediately die() after calling this function.
 159       *
 160       * @since 5.2.0
 161       *
 162       * @param array $error Error details from `error_get_last()`.
 163       * @return true|WP_Error True if the error was handled and headers have already been sent.
 164       *                       Or the request will exit to try and catch multiple errors at once.
 165       *                       WP_Error if an error occurred preventing it from being handled.
 166       */
 167  	public function handle_error( array $error ) {
 168  
 169          $extension = $this->get_extension_for_error( $error );
 170  
 171          if ( ! $extension || $this->is_network_plugin( $extension ) ) {
 172              return new WP_Error( 'invalid_source', __( 'Error not caused by a plugin or theme.' ) );
 173          }
 174  
 175          if ( ! $this->is_active() ) {
 176              if ( ! is_protected_endpoint() ) {
 177                  return new WP_Error( 'non_protected_endpoint', __( 'Error occurred on a non-protected endpoint.' ) );
 178              }
 179  
 180              if ( ! function_exists( 'wp_generate_password' ) ) {
 181                  require_once  ABSPATH . WPINC . '/pluggable.php';
 182              }
 183  
 184              return $this->email_service->maybe_send_recovery_mode_email( $this->get_email_rate_limit(), $error, $extension );
 185          }
 186  
 187          if ( ! $this->store_error( $error ) ) {
 188              return new WP_Error( 'storage_error', __( 'Failed to store the error.' ) );
 189          }
 190  
 191          if ( headers_sent() ) {
 192              return true;
 193          }
 194  
 195          $this->redirect_protected();
 196      }
 197  
 198      /**
 199       * Ends the current recovery mode session.
 200       *
 201       * @since 5.2.0
 202       *
 203       * @return bool True on success, false on failure.
 204       */
 205  	public function exit_recovery_mode() {
 206          if ( ! $this->is_active() ) {
 207              return false;
 208          }
 209  
 210          $this->email_service->clear_rate_limit();
 211          $this->cookie_service->clear_cookie();
 212  
 213          wp_paused_plugins()->delete_all();
 214          wp_paused_themes()->delete_all();
 215  
 216          return true;
 217      }
 218  
 219      /**
 220       * Handles a request to exit Recovery Mode.
 221       *
 222       * @since 5.2.0
 223       */
 224  	public function handle_exit_recovery_mode() {
 225          $redirect_to = wp_get_referer();
 226  
 227          // Safety check in case referrer returns false.
 228          if ( ! $redirect_to ) {
 229              $redirect_to = is_user_logged_in() ? admin_url() : home_url();
 230          }
 231  
 232          if ( ! $this->is_active() ) {
 233              wp_safe_redirect( $redirect_to );
 234              die;
 235          }
 236  
 237          if ( ! isset( $_GET['action'] ) || self::EXIT_ACTION !== $_GET['action'] ) {
 238              return;
 239          }
 240  
 241          if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], self::EXIT_ACTION ) ) {
 242              wp_die( __( 'Exit recovery mode link expired.' ), 403 );
 243          }
 244  
 245          if ( ! $this->exit_recovery_mode() ) {
 246              wp_die( __( 'Failed to exit recovery mode. Please try again later.' ) );
 247          }
 248  
 249          wp_safe_redirect( $redirect_to );
 250          die;
 251      }
 252  
 253      /**
 254       * Cleans any recovery mode keys that have expired according to the link TTL.
 255       *
 256       * Executes on a daily cron schedule.
 257       *
 258       * @since 5.2.0
 259       */
 260  	public function clean_expired_keys() {
 261          $this->key_service->clean_expired_keys( $this->get_link_ttl() );
 262      }
 263  
 264      /**
 265       * Handles checking for the recovery mode cookie and validating it.
 266       *
 267       * @since 5.2.0
 268       */
 269  	protected function handle_cookie() {
 270          $validated = $this->cookie_service->validate_cookie();
 271  
 272          if ( is_wp_error( $validated ) ) {
 273              $this->cookie_service->clear_cookie();
 274  
 275              $validated->add_data( array( 'status' => 403 ) );
 276              wp_die( $validated );
 277          }
 278  
 279          $session_id = $this->cookie_service->get_session_id_from_cookie();
 280          if ( is_wp_error( $session_id ) ) {
 281              $this->cookie_service->clear_cookie();
 282  
 283              $session_id->add_data( array( 'status' => 403 ) );
 284              wp_die( $session_id );
 285          }
 286  
 287          $this->is_active  = true;
 288          $this->session_id = $session_id;
 289      }
 290  
 291      /**
 292       * Gets the rate limit between sending new recovery mode email links.
 293       *
 294       * @since 5.2.0
 295       *
 296       * @return int Rate limit in seconds.
 297       */
 298  	protected function get_email_rate_limit() {
 299          /**
 300           * Filters the rate limit between sending new recovery mode email links.
 301           *
 302           * @since 5.2.0
 303           *
 304           * @param int $rate_limit Time to wait in seconds. Defaults to 1 day.
 305           */
 306          return apply_filters( 'recovery_mode_email_rate_limit', DAY_IN_SECONDS );
 307      }
 308  
 309      /**
 310       * Gets the number of seconds the recovery mode link is valid for.
 311       *
 312       * @since 5.2.0
 313       *
 314       * @return int Interval in seconds.
 315       */
 316  	protected function get_link_ttl() {
 317  
 318          $rate_limit = $this->get_email_rate_limit();
 319          $valid_for  = $rate_limit;
 320  
 321          /**
 322           * Filters the amount of time the recovery mode email link is valid for.
 323           *
 324           * The ttl must be at least as long as the email rate limit.
 325           *
 326           * @since 5.2.0
 327           *
 328           * @param int $valid_for The number of seconds the link is valid for.
 329           */
 330          $valid_for = apply_filters( 'recovery_mode_email_link_ttl', $valid_for );
 331  
 332          return max( $valid_for, $rate_limit );
 333      }
 334  
 335      /**
 336       * Gets the extension that the error occurred in.
 337       *
 338       * @since 5.2.0
 339       *
 340       * @global array $wp_theme_directories
 341       *
 342       * @param array $error Error details from `error_get_last()`.
 343       * @return array|false {
 344       *     Extension details.
 345       *
 346       *     @type string $slug The extension slug. This is the plugin or theme's directory.
 347       *     @type string $type The extension type. Either 'plugin' or 'theme'.
 348       * }
 349       */
 350  	protected function get_extension_for_error( $error ) {
 351          global $wp_theme_directories;
 352  
 353          if ( ! isset( $error['file'] ) ) {
 354              return false;
 355          }
 356  
 357          if ( ! defined( 'WP_PLUGIN_DIR' ) ) {
 358              return false;
 359          }
 360  
 361          $error_file    = wp_normalize_path( $error['file'] );
 362          $wp_plugin_dir = wp_normalize_path( WP_PLUGIN_DIR );
 363  
 364          if ( 0 === strpos( $error_file, $wp_plugin_dir ) ) {
 365              $path  = str_replace( $wp_plugin_dir . '/', '', $error_file );
 366              $parts = explode( '/', $path );
 367  
 368              return array(
 369                  'type' => 'plugin',
 370                  'slug' => $parts[0],
 371              );
 372          }
 373  
 374          if ( empty( $wp_theme_directories ) ) {
 375              return false;
 376          }
 377  
 378          foreach ( $wp_theme_directories as $theme_directory ) {
 379              $theme_directory = wp_normalize_path( $theme_directory );
 380  
 381              if ( 0 === strpos( $error_file, $theme_directory ) ) {
 382                  $path  = str_replace( $theme_directory . '/', '', $error_file );
 383                  $parts = explode( '/', $path );
 384  
 385                  return array(
 386                      'type' => 'theme',
 387                      'slug' => $parts[0],
 388                  );
 389              }
 390          }
 391  
 392          return false;
 393      }
 394  
 395      /**
 396       * Checks whether the given extension a network activated plugin.
 397       *
 398       * @since 5.2.0
 399       *
 400       * @param array $extension Extension data.
 401       * @return bool True if network plugin, false otherwise.
 402       */
 403  	protected function is_network_plugin( $extension ) {
 404          if ( 'plugin' !== $extension['type'] ) {
 405              return false;
 406          }
 407  
 408          if ( ! is_multisite() ) {
 409              return false;
 410          }
 411  
 412          $network_plugins = wp_get_active_network_plugins();
 413  
 414          foreach ( $network_plugins as $plugin ) {
 415              if ( 0 === strpos( $plugin, $extension['slug'] . '/' ) ) {
 416                  return true;
 417              }
 418          }
 419  
 420          return false;
 421      }
 422  
 423      /**
 424       * Stores the given error so that the extension causing it is paused.
 425       *
 426       * @since 5.2.0
 427       *
 428       * @param array $error Error details from `error_get_last()`.
 429       * @return bool True if the error was stored successfully, false otherwise.
 430       */
 431  	protected function store_error( $error ) {
 432          $extension = $this->get_extension_for_error( $error );
 433  
 434          if ( ! $extension ) {
 435              return false;
 436          }
 437  
 438          switch ( $extension['type'] ) {
 439              case 'plugin':
 440                  return wp_paused_plugins()->set( $extension['slug'], $error );
 441              case 'theme':
 442                  return wp_paused_themes()->set( $extension['slug'], $error );
 443              default:
 444                  return false;
 445          }
 446      }
 447  
 448      /**
 449       * Redirects the current request to allow recovering multiple errors in one go.
 450       *
 451       * The redirection will only happen when on a protected endpoint.
 452       *
 453       * It must be ensured that this method is only called when an error actually occurred and will not occur on the
 454       * next request again. Otherwise it will create a redirect loop.
 455       *
 456       * @since 5.2.0
 457       */
 458  	protected function redirect_protected() {
 459          // Pluggable is usually loaded after plugins, so we manually include it here for redirection functionality.
 460          if ( ! function_exists( 'wp_safe_redirect' ) ) {
 461              require_once  ABSPATH . WPINC . '/pluggable.php';
 462          }
 463  
 464          $scheme = is_ssl() ? 'https://' : 'http://';
 465  
 466          $url = "{$scheme}{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
 467          wp_safe_redirect( $url );
 468          exit;
 469      }
 470  }


Generated: Tue Dec 3 01:00:02 2024 Cross-referenced by PHPXref 0.7.1