[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

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


Generated : Sat Dec 21 08:20:01 2024 Cross-referenced by PHPXref