Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
2.55% |
5 / 196 |
|
7.69% |
1 / 13 |
CRAP | |
0.00% |
0 / 1 |
| WPCOMSH_Recovery_Mode_Sync | |
2.56% |
5 / 195 |
|
7.69% |
1 / 13 |
6455.55 | |
0.00% |
0 / 1 |
| init | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| capture_email_last_sent | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| capture_session_start | |
14.29% |
2 / 14 |
|
0.00% |
0 / 1 |
4.52 | |||
| capture_session_end | |
15.38% |
2 / 13 |
|
0.00% |
0 / 1 |
4.42 | |||
| send | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
182 | |||
| snapshot | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| current_session_errors | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
| capture_current_fatal | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
110 | |||
| resolve_extension_for_file | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
240 | |||
| extract_errors | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
306 | |||
| resolve_extension_version | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
110 | |||
| is_paused_extensions_option | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| trace | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Report WordPress recovery-mode state to wpcom via a dedicated endpoint so |
| 4 | * wpcom-side consumers can surface "needs recovery" / "in recovery" states |
| 5 | * for the site. |
| 6 | * |
| 7 | * Three timestamps + a per-extension error list are POSTed to |
| 8 | * `/sites/{blog_id}/recovery-mode-status`: |
| 9 | * |
| 10 | * - `recovery_mode_email_last_sent` — written by WP core each time a fatal |
| 11 | * triggers a recovery email (rate-limited to ~1/day). |
| 12 | * - `recovery_session_entered_at` — wpcomsh-managed; updated on any write |
| 13 | * to `{session_id}_paused_extensions`, i.e. when the admin enters a |
| 14 | * recovery session by clicking the email link. |
| 15 | * - `recovery_session_exited_at` — wpcomsh-managed; updated on deletion |
| 16 | * of `{session_id}_paused_extensions`, i.e. when the admin exits recovery. |
| 17 | * - `recovery_session_errors` — a list of `{kind, slug, version, |
| 18 | * errno, message, file, line}` records, so wpcom can surface what fataled |
| 19 | * rather than just that something fataled. Sourced from the live |
| 20 | * `*_paused_extensions` option once the admin enters recovery; on the |
| 21 | * fatal request itself (before the admin clicks the email link) the list |
| 22 | * is populated from `error_get_last()` so the very first POST already |
| 23 | * carries the error info. Empty once the admin exits recovery. |
| 24 | * |
| 25 | * The POST runs from a PHP shutdown function so the signal reaches wpcom even |
| 26 | * on fatal-error requests, matching the pattern used by migrate-guru-canary. |
| 27 | * |
| 28 | * @package wpcomsh |
| 29 | */ |
| 30 | |
| 31 | use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client; |
| 32 | |
| 33 | /** |
| 34 | * Captures recovery-mode option writes and forwards a state snapshot to wpcom |
| 35 | * on PHP shutdown. |
| 36 | */ |
| 37 | class WPCOMSH_Recovery_Mode_Sync { |
| 38 | |
| 39 | private const EMAIL_LAST_SENT_OPTION = 'recovery_mode_email_last_sent'; |
| 40 | private const ENTERED_AT_OPTION = 'wpcomsh_recovery_session_entered_at'; |
| 41 | private const EXITED_AT_OPTION = 'wpcomsh_recovery_session_exited_at'; |
| 42 | private const PAUSED_EXTENSIONS_OPTION_SUFFIX = '_paused_extensions'; |
| 43 | |
| 44 | /** |
| 45 | * Pending state snapshot. Empty until the first capture this request — |
| 46 | * its non-emptiness doubles as the "send needed" flag. |
| 47 | * |
| 48 | * @var array<string,mixed> |
| 49 | */ |
| 50 | private static $payload = array(); |
| 51 | |
| 52 | /** |
| 53 | * Error record captured from `error_get_last()` at email-send time so the |
| 54 | * fatal-request POST carries error info before the admin enters recovery |
| 55 | * and `*_paused_extensions` exists. Null when no fatal has been captured |
| 56 | * (or when the fatal didn't come from a known plugin/theme). |
| 57 | * |
| 58 | * @var array<string,mixed>|null |
| 59 | */ |
| 60 | private static $transient_fatal = null; |
| 61 | |
| 62 | /** |
| 63 | * Register option-change listeners. |
| 64 | */ |
| 65 | public static function init() { |
| 66 | add_action( 'add_option_' . self::EMAIL_LAST_SENT_OPTION, array( __CLASS__, 'capture_email_last_sent' ) ); |
| 67 | add_action( 'update_option_' . self::EMAIL_LAST_SENT_OPTION, array( __CLASS__, 'capture_email_last_sent' ) ); |
| 68 | |
| 69 | // Only `added_option` signals a new recovery session. `updated_option` |
| 70 | // fires for in-session extension additions and — crucially — during |
| 71 | // exit unwinding when `WP_Paused_Extensions_Storage::delete_all()` of |
| 72 | // one type rewrites the session option with remaining entries of the |
| 73 | // other type; treating that as a new entry would clobber entered_at. |
| 74 | add_action( 'added_option', array( __CLASS__, 'capture_session_start' ), 10, 1 ); |
| 75 | add_action( 'deleted_option', array( __CLASS__, 'capture_session_end' ), 10, 1 ); |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Listener for the recovery-mode email timestamp. |
| 80 | */ |
| 81 | public static function capture_email_last_sent() { |
| 82 | // We're called inside WP's fatal-handler shutdown stack, so |
| 83 | // `error_get_last()` still holds the original fatal that caused WP to |
| 84 | // send the email. Snapshot it now so the snapshot below picks it up. |
| 85 | self::capture_current_fatal(); |
| 86 | self::snapshot(); |
| 87 | self::trace( |
| 88 | 'captured email_last_sent', |
| 89 | array( 'value' => self::$payload['recovery_mode_email_last_sent'] ) |
| 90 | ); |
| 91 | self::send(); |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Listener for option writes that may represent entering a recovery session. |
| 96 | * |
| 97 | * @param string $option Option name. |
| 98 | */ |
| 99 | public static function capture_session_start( $option ) { |
| 100 | if ( ! self::is_paused_extensions_option( $option ) ) { |
| 101 | return; |
| 102 | } |
| 103 | $now = time(); |
| 104 | update_option( self::ENTERED_AT_OPTION, $now, false ); |
| 105 | self::snapshot(); |
| 106 | self::trace( |
| 107 | 'captured session_start', |
| 108 | array( |
| 109 | 'option' => $option, |
| 110 | 'entered_at' => $now, |
| 111 | 'errors' => self::$payload['recovery_session_errors'] ?? array(), |
| 112 | ) |
| 113 | ); |
| 114 | self::send(); |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Listener for option deletions that represent exiting a recovery session. |
| 119 | * |
| 120 | * @param string $option Option name. |
| 121 | */ |
| 122 | public static function capture_session_end( $option ) { |
| 123 | if ( ! self::is_paused_extensions_option( $option ) ) { |
| 124 | return; |
| 125 | } |
| 126 | $now = time(); |
| 127 | update_option( self::EXITED_AT_OPTION, $now, false ); |
| 128 | self::snapshot(); |
| 129 | self::trace( |
| 130 | 'captured session_end', |
| 131 | array( |
| 132 | 'option' => $option, |
| 133 | 'exited_at' => $now, |
| 134 | ) |
| 135 | ); |
| 136 | self::send(); |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * POST the current state snapshot to wpcom. Called synchronously from each |
| 141 | * capture listener — we deliberately do *not* defer to a PHP shutdown |
| 142 | * function because option writes that trigger a capture often happen from |
| 143 | * inside WP's own fatal-handler shutdown callback, and shutdown callbacks |
| 144 | * registered at that point (or even earlier) are not reliably invoked on |
| 145 | * the dying request. |
| 146 | */ |
| 147 | public static function send() { |
| 148 | if ( empty( self::$payload ) ) { |
| 149 | return; |
| 150 | } |
| 151 | if ( ! class_exists( Jetpack_Connection_Client::class ) ) { |
| 152 | self::trace( 'send() aborting: Jetpack Connection Client class missing' ); |
| 153 | return; |
| 154 | } |
| 155 | if ( ! function_exists( '_wpcom_get_current_blog_id' ) ) { |
| 156 | self::trace( 'send() aborting: _wpcom_get_current_blog_id() not defined' ); |
| 157 | return; |
| 158 | } |
| 159 | |
| 160 | // The Connection Client signs requests with wp_rand() / wp_generate_password(), |
| 161 | // both defined in pluggable.php. That file is loaded late in WP's bootstrap and |
| 162 | // is often not yet available when we're called from inside WP's fatal-handler |
| 163 | // shutdown path (the exact case this feature exists to handle). Load it here. |
| 164 | if ( ! function_exists( 'wp_rand' ) && defined( 'ABSPATH' ) ) { |
| 165 | $pluggable_path = ABSPATH . 'wp-includes/pluggable.php'; |
| 166 | if ( file_exists( $pluggable_path ) ) { |
| 167 | require_once $pluggable_path; |
| 168 | } |
| 169 | } |
| 170 | if ( ! function_exists( 'wp_rand' ) ) { |
| 171 | self::trace( 'send() aborting: wp_rand() unavailable' ); |
| 172 | return; |
| 173 | } |
| 174 | |
| 175 | try { |
| 176 | $wpcom_blog_id = _wpcom_get_current_blog_id(); |
| 177 | if ( ! $wpcom_blog_id ) { |
| 178 | self::trace( 'send() aborting: blog_id is falsy', array( 'value' => $wpcom_blog_id ) ); |
| 179 | return; |
| 180 | } |
| 181 | |
| 182 | self::trace( |
| 183 | 'posting state', |
| 184 | array( |
| 185 | 'blog_id' => $wpcom_blog_id, |
| 186 | 'payload' => self::$payload, |
| 187 | ) |
| 188 | ); |
| 189 | |
| 190 | $response = Jetpack_Connection_Client::wpcom_json_api_request_as_blog( |
| 191 | sprintf( '/sites/%s/recovery-mode-status', $wpcom_blog_id ), |
| 192 | 'v2', |
| 193 | array( 'method' => 'POST' ), |
| 194 | self::$payload, |
| 195 | 'wpcom' |
| 196 | ); |
| 197 | |
| 198 | if ( is_wp_error( $response ) ) { |
| 199 | self::trace( |
| 200 | 'post returned WP_Error', |
| 201 | array( 'error' => $response->get_error_message() ) |
| 202 | ); |
| 203 | } else { |
| 204 | $code = (int) wp_remote_retrieve_response_code( $response ); |
| 205 | if ( $code < 200 || $code >= 300 ) { |
| 206 | self::trace( |
| 207 | 'post returned non-2xx', |
| 208 | array( 'code' => $code ) |
| 209 | ); |
| 210 | } |
| 211 | } |
| 212 | } catch ( \Throwable $e ) { |
| 213 | self::trace( |
| 214 | 'post threw', |
| 215 | array( 'exception' => $e->getMessage() ) |
| 216 | ); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * Populate the in-memory state snapshot once per request. Errors are read |
| 222 | * from the live `*_paused_extensions` option so every capture path (email, |
| 223 | * start, end) emits a complete state — no persistence of our own needed. |
| 224 | */ |
| 225 | private static function snapshot() { |
| 226 | if ( ! empty( self::$payload ) ) { |
| 227 | return; |
| 228 | } |
| 229 | self::$payload = array( |
| 230 | 'recovery_mode_email_last_sent' => (int) get_option( self::EMAIL_LAST_SENT_OPTION, 0 ), |
| 231 | 'recovery_session_entered_at' => (int) get_option( self::ENTERED_AT_OPTION, 0 ), |
| 232 | 'recovery_session_exited_at' => (int) get_option( self::EXITED_AT_OPTION, 0 ), |
| 233 | 'recovery_session_errors' => self::current_session_errors(), |
| 234 | ); |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * Read the active recovery session's `*_paused_extensions` option and |
| 239 | * normalize it into transportable error records. Returns an empty array |
| 240 | * when the session is gone (e.g. after admin exits) so wpcom sees the |
| 241 | * state cleared rather than stale errors. |
| 242 | * |
| 243 | * @return array<int,array<string,mixed>> |
| 244 | */ |
| 245 | private static function current_session_errors() { |
| 246 | if ( function_exists( 'wp_recovery_mode' ) ) { |
| 247 | $session_id = wp_recovery_mode()->get_session_id(); |
| 248 | if ( ! empty( $session_id ) ) { |
| 249 | $errors = self::extract_errors( |
| 250 | get_option( $session_id . self::PAUSED_EXTENSIONS_OPTION_SUFFIX ) |
| 251 | ); |
| 252 | if ( ! empty( $errors ) ) { |
| 253 | return $errors; |
| 254 | } |
| 255 | } |
| 256 | } |
| 257 | // No active session yet (fatal request, before admin clicks the email |
| 258 | // link) — fall back to the fatal we captured from `error_get_last()`. |
| 259 | return null !== self::$transient_fatal ? array( self::$transient_fatal ) : array(); |
| 260 | } |
| 261 | |
| 262 | /** |
| 263 | * Snapshot the currently-pending PHP fatal (if any) into a transportable |
| 264 | * record. Called from `capture_email_last_sent()` so we run inside WP's |
| 265 | * fatal-handler shutdown, when `error_get_last()` still holds the fatal |
| 266 | * that triggered the recovery email. No-op when there's no fatal pending |
| 267 | * or the fatal didn't originate from a known plugin/theme path. |
| 268 | */ |
| 269 | private static function capture_current_fatal() { |
| 270 | if ( null !== self::$transient_fatal ) { |
| 271 | return; |
| 272 | } |
| 273 | $err = error_get_last(); |
| 274 | if ( ! is_array( $err ) ) { |
| 275 | return; |
| 276 | } |
| 277 | $fatal_mask = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR; |
| 278 | if ( ! ( (int) ( $err['type'] ?? 0 ) & $fatal_mask ) ) { |
| 279 | return; |
| 280 | } |
| 281 | $file = (string) ( $err['file'] ?? '' ); |
| 282 | $resolved = self::resolve_extension_for_file( $file ); |
| 283 | if ( null === $resolved ) { |
| 284 | return; |
| 285 | } |
| 286 | list( $kind, $slug ) = $resolved; |
| 287 | |
| 288 | if ( ! function_exists( 'get_plugins' ) && defined( 'ABSPATH' ) ) { |
| 289 | $plugin_admin = ABSPATH . 'wp-admin/includes/plugin.php'; |
| 290 | if ( file_exists( $plugin_admin ) ) { |
| 291 | require_once $plugin_admin; |
| 292 | } |
| 293 | } |
| 294 | $plugins = function_exists( 'get_plugins' ) ? get_plugins() : array(); |
| 295 | |
| 296 | self::$transient_fatal = array( |
| 297 | 'kind' => $kind, |
| 298 | 'slug' => $slug, |
| 299 | 'version' => self::resolve_extension_version( $kind, $slug, $plugins ), |
| 300 | 'errno' => (int) $err['type'], |
| 301 | 'message' => (string) ( $err['message'] ?? '' ), |
| 302 | 'file' => '' !== $file ? basename( $file ) : '', |
| 303 | 'line' => (int) ( $err['line'] ?? 0 ), |
| 304 | ); |
| 305 | } |
| 306 | |
| 307 | /** |
| 308 | * Identify the plugin or theme a fatal originated from by matching the |
| 309 | * file path against the plugin and active theme directories. Returns |
| 310 | * `[kind, slug]` matching the shape WP's own recovery storage uses — the |
| 311 | * plugin slug is the first path segment under `WP_PLUGIN_DIR` (mirroring |
| 312 | * core's `WP_Recovery_Mode::get_extension_for_error()`), and the theme |
| 313 | * slug is the stylesheet/template directory name. Returns null when the |
| 314 | * file doesn't live under a known extension (core, mu-plugins, drop-ins). |
| 315 | * |
| 316 | * @param string $file Absolute path to the file that fataled. |
| 317 | * @return array{0:string,1:string}|null |
| 318 | */ |
| 319 | private static function resolve_extension_for_file( $file ) { |
| 320 | if ( '' === $file || ! function_exists( 'wp_normalize_path' ) ) { |
| 321 | return null; |
| 322 | } |
| 323 | $normalized = wp_normalize_path( $file ); |
| 324 | |
| 325 | if ( defined( 'WP_PLUGIN_DIR' ) ) { |
| 326 | $plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ) . '/'; |
| 327 | if ( str_starts_with( $normalized, $plugin_dir ) ) { |
| 328 | $rel = substr( $normalized, strlen( $plugin_dir ) ); |
| 329 | $parts = explode( '/', $rel ); |
| 330 | if ( '' !== $parts[0] ) { |
| 331 | return array( 'plugin', $parts[0] ); |
| 332 | } |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | if ( function_exists( 'get_stylesheet' ) && function_exists( 'get_stylesheet_directory' ) ) { |
| 337 | $candidates = array(); |
| 338 | $stylesheet = (string) get_stylesheet(); |
| 339 | if ( '' !== $stylesheet ) { |
| 340 | $candidates[ $stylesheet ] = wp_normalize_path( get_stylesheet_directory() ) . '/'; |
| 341 | } |
| 342 | if ( function_exists( 'get_template' ) && function_exists( 'get_template_directory' ) ) { |
| 343 | $template = (string) get_template(); |
| 344 | if ( '' !== $template && $template !== $stylesheet ) { |
| 345 | $candidates[ $template ] = wp_normalize_path( get_template_directory() ) . '/'; |
| 346 | } |
| 347 | } |
| 348 | foreach ( $candidates as $slug => $dir ) { |
| 349 | if ( str_starts_with( $normalized, $dir ) ) { |
| 350 | return array( 'theme', $slug ); |
| 351 | } |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | return null; |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * Normalize WP's `*_paused_extensions` option into a flat list of records |
| 360 | * suitable for transport. The option is shaped as |
| 361 | * `[ 'plugin' => [ slug => {type,file,line,message} ], 'theme' => [ ... ] ]`. |
| 362 | * |
| 363 | * Each output record carries the kind/slug/version + the captured error |
| 364 | * (file is reduced to its basename so server paths don't leak). |
| 365 | * |
| 366 | * @param mixed $paused_extensions Raw option value. |
| 367 | * @return array<int,array<string,mixed>> |
| 368 | */ |
| 369 | private static function extract_errors( $paused_extensions ) { |
| 370 | if ( ! is_array( $paused_extensions ) ) { |
| 371 | return array(); |
| 372 | } |
| 373 | |
| 374 | // `get_plugins()` lives in wp-admin/includes/plugin.php — not loaded on |
| 375 | // front-end requests, but recovery emails fire there. |
| 376 | if ( ! function_exists( 'get_plugins' ) && defined( 'ABSPATH' ) ) { |
| 377 | $plugin_admin = ABSPATH . 'wp-admin/includes/plugin.php'; |
| 378 | if ( file_exists( $plugin_admin ) ) { |
| 379 | require_once $plugin_admin; |
| 380 | } |
| 381 | } |
| 382 | $plugins = function_exists( 'get_plugins' ) ? get_plugins() : array(); |
| 383 | |
| 384 | $out = array(); |
| 385 | foreach ( array( 'plugin', 'theme' ) as $kind ) { |
| 386 | if ( empty( $paused_extensions[ $kind ] ) || ! is_array( $paused_extensions[ $kind ] ) ) { |
| 387 | continue; |
| 388 | } |
| 389 | foreach ( $paused_extensions[ $kind ] as $slug => $error ) { |
| 390 | if ( ! is_string( $slug ) || '' === $slug || ! is_array( $error ) ) { |
| 391 | continue; |
| 392 | } |
| 393 | $out[] = array( |
| 394 | 'kind' => $kind, |
| 395 | 'slug' => $slug, |
| 396 | 'version' => self::resolve_extension_version( $kind, $slug, $plugins ), |
| 397 | 'errno' => isset( $error['type'] ) ? (int) $error['type'] : 0, |
| 398 | 'message' => isset( $error['message'] ) ? (string) $error['message'] : '', |
| 399 | 'file' => isset( $error['file'] ) ? basename( (string) $error['file'] ) : '', |
| 400 | 'line' => isset( $error['line'] ) ? (int) $error['line'] : 0, |
| 401 | ); |
| 402 | } |
| 403 | } |
| 404 | return $out; |
| 405 | } |
| 406 | |
| 407 | /** |
| 408 | * Resolve the installed version string for a paused extension. |
| 409 | * |
| 410 | * For plugins, WP keys the paused entry by the plugin's main-file path (or |
| 411 | * its dirname); we match against `get_plugins()` output. For themes, |
| 412 | * `wp_get_theme()` looks up by stylesheet directory name. |
| 413 | * |
| 414 | * @param string $kind `plugin` or `theme`. |
| 415 | * @param string $slug Extension slug as recorded by WP recovery mode. |
| 416 | * @param array $plugins Cached `get_plugins()` output (plugins only). |
| 417 | * @return string Version string, or empty when not resolvable. |
| 418 | */ |
| 419 | private static function resolve_extension_version( $kind, $slug, $plugins ) { |
| 420 | if ( 'plugin' === $kind ) { |
| 421 | foreach ( $plugins as $file => $data ) { |
| 422 | if ( $slug === $file || $slug === dirname( $file ) ) { |
| 423 | return isset( $data['Version'] ) ? (string) $data['Version'] : ''; |
| 424 | } |
| 425 | } |
| 426 | return ''; |
| 427 | } |
| 428 | if ( 'theme' === $kind && function_exists( 'wp_get_theme' ) ) { |
| 429 | $theme = wp_get_theme( $slug ); |
| 430 | if ( $theme && $theme->exists() ) { |
| 431 | return (string) $theme->get( 'Version' ); |
| 432 | } |
| 433 | } |
| 434 | return ''; |
| 435 | } |
| 436 | |
| 437 | /** |
| 438 | * Whether the given option name is a recovery-session paused-extensions |
| 439 | * option (session-scoped, dynamically named). |
| 440 | * |
| 441 | * @param string $option Option name. |
| 442 | * @return bool |
| 443 | */ |
| 444 | private static function is_paused_extensions_option( $option ) { |
| 445 | return is_string( $option ) && str_ends_with( $option, self::PAUSED_EXTENSIONS_OPTION_SUFFIX ); |
| 446 | } |
| 447 | |
| 448 | /** |
| 449 | * Emit a trace log to error_log when opted in via filter. Default is off. |
| 450 | * |
| 451 | * Enable on a specific site with: |
| 452 | * add_filter( 'wpcomsh_recovery_mode_sync_logging_enabled', '__return_true' ); |
| 453 | * |
| 454 | * @param string $message Trace message. |
| 455 | * @param array $extra Optional structured context. |
| 456 | */ |
| 457 | private static function trace( $message, $extra = array() ) { |
| 458 | /** |
| 459 | * Whether to emit recovery-mode-sync trace logs to error_log. |
| 460 | * |
| 461 | * @param bool $enabled Defaults to false. |
| 462 | */ |
| 463 | if ( ! apply_filters( 'wpcomsh_recovery_mode_sync_logging_enabled', false ) ) { |
| 464 | return; |
| 465 | } |
| 466 | static $request_id = null; |
| 467 | if ( $request_id === null ) { |
| 468 | $request_id = substr( md5( uniqid( '', true ) ), 0, 8 ); |
| 469 | } |
| 470 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
| 471 | error_log( 'wpcomsh_recovery_mode_sync[' . $request_id . ']: ' . $message . ' ' . wp_json_encode( $extra, JSON_UNESCAPED_SLASHES ) ); |
| 472 | } |
| 473 | } |
| 474 | |
| 475 | WPCOMSH_Recovery_Mode_Sync::init(); |