balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-recovery-mode-email-service.php (10660B)


      1 <?php
      2 /**
      3  * Error Protection API: WP_Recovery_Mode_Email_Link class
      4  *
      5  * @package WordPress
      6  * @since 5.2.0
      7  */
      8 
      9 /**
     10  * Core class used to send an email with a link to begin Recovery Mode.
     11  *
     12  * @since 5.2.0
     13  */
     14 final class WP_Recovery_Mode_Email_Service {
     15 
     16 	const RATE_LIMIT_OPTION = 'recovery_mode_email_last_sent';
     17 
     18 	/**
     19 	 * Service to generate recovery mode URLs.
     20 	 *
     21 	 * @since 5.2.0
     22 	 * @var WP_Recovery_Mode_Link_Service
     23 	 */
     24 	private $link_service;
     25 
     26 	/**
     27 	 * WP_Recovery_Mode_Email_Service constructor.
     28 	 *
     29 	 * @since 5.2.0
     30 	 *
     31 	 * @param WP_Recovery_Mode_Link_Service $link_service
     32 	 */
     33 	public function __construct( WP_Recovery_Mode_Link_Service $link_service ) {
     34 		$this->link_service = $link_service;
     35 	}
     36 
     37 	/**
     38 	 * Sends the recovery mode email if the rate limit has not been sent.
     39 	 *
     40 	 * @since 5.2.0
     41 	 *
     42 	 * @param int   $rate_limit Number of seconds before another email can be sent.
     43 	 * @param array $error      Error details from {@see error_get_last()}
     44 	 * @param array $extension {
     45 	 *     The extension that caused the error.
     46 	 *
     47 	 *     @type string $slug The extension slug. The plugin or theme's directory.
     48 	 *     @type string $type The extension type. Either 'plugin' or 'theme'.
     49 	 * }
     50 	 * @return true|WP_Error True if email sent, WP_Error otherwise.
     51 	 */
     52 	public function maybe_send_recovery_mode_email( $rate_limit, $error, $extension ) {
     53 
     54 		$last_sent = get_option( self::RATE_LIMIT_OPTION );
     55 
     56 		if ( ! $last_sent || time() > $last_sent + $rate_limit ) {
     57 			if ( ! update_option( self::RATE_LIMIT_OPTION, time() ) ) {
     58 				return new WP_Error( 'storage_error', __( 'Could not update the email last sent time.' ) );
     59 			}
     60 
     61 			$sent = $this->send_recovery_mode_email( $rate_limit, $error, $extension );
     62 
     63 			if ( $sent ) {
     64 				return true;
     65 			}
     66 
     67 			return new WP_Error(
     68 				'email_failed',
     69 				sprintf(
     70 					/* translators: %s: mail() */
     71 					__( 'The email could not be sent. Possible reason: your host may have disabled the %s function.' ),
     72 					'mail()'
     73 				)
     74 			);
     75 		}
     76 
     77 		$err_message = sprintf(
     78 			/* translators: 1: Last sent as a human time diff, 2: Wait time as a human time diff. */
     79 			__( 'A recovery link was already sent %1$s ago. Please wait another %2$s before requesting a new email.' ),
     80 			human_time_diff( $last_sent ),
     81 			human_time_diff( $last_sent + $rate_limit )
     82 		);
     83 
     84 		return new WP_Error( 'email_sent_already', $err_message );
     85 	}
     86 
     87 	/**
     88 	 * Clears the rate limit, allowing a new recovery mode email to be sent immediately.
     89 	 *
     90 	 * @since 5.2.0
     91 	 *
     92 	 * @return bool True on success, false on failure.
     93 	 */
     94 	public function clear_rate_limit() {
     95 		return delete_option( self::RATE_LIMIT_OPTION );
     96 	}
     97 
     98 	/**
     99 	 * Sends the Recovery Mode email to the site admin email address.
    100 	 *
    101 	 * @since 5.2.0
    102 	 *
    103 	 * @param int   $rate_limit Number of seconds before another email can be sent.
    104 	 * @param array $error      Error details from {@see error_get_last()}
    105 	 * @param array $extension  Extension that caused the error.
    106 	 * @return bool Whether the email was sent successfully.
    107 	 */
    108 	private function send_recovery_mode_email( $rate_limit, $error, $extension ) {
    109 
    110 		$url      = $this->link_service->generate_url();
    111 		$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
    112 
    113 		$switched_locale = false;
    114 
    115 		// The switch_to_locale() function is loaded before it can actually be used.
    116 		if ( function_exists( 'switch_to_locale' ) && isset( $GLOBALS['wp_locale_switcher'] ) ) {
    117 			$switched_locale = switch_to_locale( get_locale() );
    118 		}
    119 
    120 		if ( $extension ) {
    121 			$cause   = $this->get_cause( $extension );
    122 			$details = wp_strip_all_tags( wp_get_extension_error_description( $error ) );
    123 
    124 			if ( $details ) {
    125 				$header  = __( 'Error Details' );
    126 				$details = "\n\n" . $header . "\n" . str_pad( '', strlen( $header ), '=' ) . "\n" . $details;
    127 			}
    128 		} else {
    129 			$cause   = '';
    130 			$details = '';
    131 		}
    132 
    133 		/**
    134 		 * Filters the support message sent with the the fatal error protection email.
    135 		 *
    136 		 * @since 5.2.0
    137 		 *
    138 		 * @param string $message The Message to include in the email.
    139 		 */
    140 		$support = apply_filters( 'recovery_email_support_info', __( 'Please contact your host for assistance with investigating this issue further.' ) );
    141 
    142 		/**
    143 		 * Filters the debug information included in the fatal error protection email.
    144 		 *
    145 		 * @since 5.3.0
    146 		 *
    147 		 * @param array $message An associative array of debug information.
    148 		 */
    149 		$debug = apply_filters( 'recovery_email_debug_info', $this->get_debug( $extension ) );
    150 
    151 		/* translators: Do not translate LINK, EXPIRES, CAUSE, DETAILS, SITEURL, PAGEURL, SUPPORT. DEBUG: those are placeholders. */
    152 		$message = __(
    153 			'Howdy!
    154 
    155 Since WordPress 5.2 there is a built-in feature that detects when a plugin or theme causes a fatal error on your site, and notifies you with this automated email.
    156 ###CAUSE###
    157 First, visit your website (###SITEURL###) and check for any visible issues. Next, visit the page where the error was caught (###PAGEURL###) and check for any visible issues.
    158 
    159 ###SUPPORT###
    160 
    161 If your site appears broken and you can\'t access your dashboard normally, WordPress now has a special "recovery mode". This lets you safely login to your dashboard and investigate further.
    162 
    163 ###LINK###
    164 
    165 To keep your site safe, this link will expire in ###EXPIRES###. Don\'t worry about that, though: a new link will be emailed to you if the error occurs again after it expires.
    166 
    167 When seeking help with this issue, you may be asked for some of the following information:
    168 ###DEBUG###
    169 
    170 ###DETAILS###'
    171 		);
    172 		$message = str_replace(
    173 			array(
    174 				'###LINK###',
    175 				'###EXPIRES###',
    176 				'###CAUSE###',
    177 				'###DETAILS###',
    178 				'###SITEURL###',
    179 				'###PAGEURL###',
    180 				'###SUPPORT###',
    181 				'###DEBUG###',
    182 			),
    183 			array(
    184 				$url,
    185 				human_time_diff( time() + $rate_limit ),
    186 				$cause ? "\n{$cause}\n" : "\n",
    187 				$details,
    188 				home_url( '/' ),
    189 				home_url( $_SERVER['REQUEST_URI'] ),
    190 				$support,
    191 				implode( "\r\n", $debug ),
    192 			),
    193 			$message
    194 		);
    195 
    196 		$email = array(
    197 			'to'          => $this->get_recovery_mode_email_address(),
    198 			/* translators: %s: Site title. */
    199 			'subject'     => __( '[%s] Your Site is Experiencing a Technical Issue' ),
    200 			'message'     => $message,
    201 			'headers'     => '',
    202 			'attachments' => '',
    203 		);
    204 
    205 		/**
    206 		 * Filters the contents of the Recovery Mode email.
    207 		 *
    208 		 * @since 5.2.0
    209 		 * @since 5.6.0 The `$email` argument includes the `attachments` key.
    210 		 *
    211 		 * @param array  $email {
    212 		 *     Used to build a call to wp_mail().
    213 		 *
    214 		 *     @type string|array $to          Array or comma-separated list of email addresses to send message.
    215 		 *     @type string       $subject     Email subject
    216 		 *     @type string       $message     Message contents
    217 		 *     @type string|array $headers     Optional. Additional headers.
    218 		 *     @type string|array $attachments Optional. Files to attach.
    219 		 * }
    220 		 * @param string $url   URL to enter recovery mode.
    221 		 */
    222 		$email = apply_filters( 'recovery_mode_email', $email, $url );
    223 
    224 		$sent = wp_mail(
    225 			$email['to'],
    226 			wp_specialchars_decode( sprintf( $email['subject'], $blogname ) ),
    227 			$email['message'],
    228 			$email['headers'],
    229 			$email['attachments']
    230 		);
    231 
    232 		if ( $switched_locale ) {
    233 			restore_previous_locale();
    234 		}
    235 
    236 		return $sent;
    237 	}
    238 
    239 	/**
    240 	 * Gets the email address to send the recovery mode link to.
    241 	 *
    242 	 * @since 5.2.0
    243 	 *
    244 	 * @return string Email address to send recovery mode link to.
    245 	 */
    246 	private function get_recovery_mode_email_address() {
    247 		if ( defined( 'RECOVERY_MODE_EMAIL' ) && is_email( RECOVERY_MODE_EMAIL ) ) {
    248 			return RECOVERY_MODE_EMAIL;
    249 		}
    250 
    251 		return get_option( 'admin_email' );
    252 	}
    253 
    254 	/**
    255 	 * Gets the description indicating the possible cause for the error.
    256 	 *
    257 	 * @since 5.2.0
    258 	 *
    259 	 * @param array $extension The extension that caused the error.
    260 	 * @return string Message about which extension caused the error.
    261 	 */
    262 	private function get_cause( $extension ) {
    263 
    264 		if ( 'plugin' === $extension['type'] ) {
    265 			$plugin = $this->get_plugin( $extension );
    266 
    267 			if ( false === $plugin ) {
    268 				$name = $extension['slug'];
    269 			} else {
    270 				$name = $plugin['Name'];
    271 			}
    272 
    273 			/* translators: %s: Plugin name. */
    274 			$cause = sprintf( __( 'In this case, WordPress caught an error with one of your plugins, %s.' ), $name );
    275 		} else {
    276 			$theme = wp_get_theme( $extension['slug'] );
    277 			$name  = $theme->exists() ? $theme->display( 'Name' ) : $extension['slug'];
    278 
    279 			/* translators: %s: Theme name. */
    280 			$cause = sprintf( __( 'In this case, WordPress caught an error with your theme, %s.' ), $name );
    281 		}
    282 
    283 		return $cause;
    284 	}
    285 
    286 	/**
    287 	 * Return the details for a single plugin based on the extension data from an error.
    288 	 *
    289 	 * @since 5.3.0
    290 	 *
    291 	 * @param array $extension The extension that caused the error.
    292 	 * @return array|false A plugin array {@see get_plugins()} or `false` if no plugin was found.
    293 	 */
    294 	private function get_plugin( $extension ) {
    295 		if ( ! function_exists( 'get_plugins' ) ) {
    296 			require_once ABSPATH . 'wp-admin/includes/plugin.php';
    297 		}
    298 
    299 		$plugins = get_plugins();
    300 
    301 		// Assume plugin main file name first since it is a common convention.
    302 		if ( isset( $plugins[ "{$extension['slug']}/{$extension['slug']}.php" ] ) ) {
    303 			return $plugins[ "{$extension['slug']}/{$extension['slug']}.php" ];
    304 		} else {
    305 			foreach ( $plugins as $file => $plugin_data ) {
    306 				if ( 0 === strpos( $file, "{$extension['slug']}/" ) || $file === $extension['slug'] ) {
    307 					return $plugin_data;
    308 				}
    309 			}
    310 		}
    311 
    312 		return false;
    313 	}
    314 
    315 	/**
    316 	 * Return debug information in an easy to manipulate format.
    317 	 *
    318 	 * @since 5.3.0
    319 	 *
    320 	 * @param array $extension The extension that caused the error.
    321 	 * @return array An associative array of debug information.
    322 	 */
    323 	private function get_debug( $extension ) {
    324 		$theme      = wp_get_theme();
    325 		$wp_version = get_bloginfo( 'version' );
    326 
    327 		if ( $extension ) {
    328 			$plugin = $this->get_plugin( $extension );
    329 		} else {
    330 			$plugin = null;
    331 		}
    332 
    333 		$debug = array(
    334 			'wp'    => sprintf(
    335 				/* translators: %s: Current WordPress version number. */
    336 				__( 'WordPress version %s' ),
    337 				$wp_version
    338 			),
    339 			'theme' => sprintf(
    340 				/* translators: 1: Current active theme name. 2: Current active theme version. */
    341 				__( 'Current theme: %1$s (version %2$s)' ),
    342 				$theme->get( 'Name' ),
    343 				$theme->get( 'Version' )
    344 			),
    345 		);
    346 
    347 		if ( null !== $plugin ) {
    348 			$debug['plugin'] = sprintf(
    349 				/* translators: 1: The failing plugins name. 2: The failing plugins version. */
    350 				__( 'Current plugin: %1$s (version %2$s)' ),
    351 				$plugin['Name'],
    352 				$plugin['Version']
    353 			);
    354 		}
    355 
    356 		$debug['php'] = sprintf(
    357 			/* translators: %s: The currently used PHP version. */
    358 			__( 'PHP version %s' ),
    359 			PHP_VERSION
    360 		);
    361 
    362 		return $debug;
    363 	}
    364 }