*/ private static $logged = array(); /** * Register a guarded action. * * @param string $hook Hook. * @param callable $callback Callback. * @param int $priority Priority. * @param int $args Accepted args. * @return void */ public static function action( $hook, $callback, $priority = 10, $args = 1 ) { add_action( $hook, static function ( ...$params ) use ( $callback, $hook ) { try { call_user_func_array( $callback, $params ); } catch ( \Throwable $e ) { self::report( $e, $hook ); } }, $priority, $args ); } /** * Register a guarded filter; on error the value passes through unchanged. * * @param string $hook Hook. * @param callable $callback Callback. * @param int $priority Priority. * @param int $args Accepted args. * @return void */ public static function filter( $hook, $callback, $priority = 10, $args = 1 ) { add_filter( $hook, static function ( ...$params ) use ( $callback, $hook ) { try { return call_user_func_array( $callback, $params ); } catch ( \Throwable $e ) { self::report( $e, $hook ); return $params ? $params[0] : null; } }, $priority, $args ); } /** * Run code, returning a fallback if it throws. * * @param callable $callback Code. * @param mixed $fallback Value on error. * @param string $where Label for the log. * @return mixed */ public static function run( $callback, $fallback = null, $where = '' ) { try { return call_user_func( $callback ); } catch ( \Throwable $e ) { self::report( $e, $where ); return $fallback; } } /** * Log an error once per request and remember the latest for the admin * notice (written at most every 10 minutes, so an error on every request * can't hammer the database). * * @param \Throwable $e Error. * @param string $where Hook or label. * @return void */ public static function report( \Throwable $e, $where = '' ) { $message = sprintf( '%s: %s in %s:%d', get_class( $e ), $e->getMessage(), str_replace( ABSPATH, '', $e->getFile() ), $e->getLine() ); if ( isset( self::$logged[ $message ] ) ) { return; } self::$logged[ $message ] = true; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- deliberate: the error is otherwise swallowed. error_log( 'ReturnKit caught an error' . ( $where ? ' in ' . $where : '' ) . ' and let the request continue: ' . $message ); try { $last = get_option( self::OPTION ); if ( ! is_array( $last ) || empty( $last['time'] ) || time() - (int) $last['time'] > 10 * MINUTE_IN_SECONDS || $last['message'] !== $message ) { update_option( self::OPTION, array( 'time' => time(), 'message' => substr( $message, 0, 500 ), 'where' => substr( (string) $where, 0, 100 ), 'version' => defined( 'RTKT_VERSION' ) ? RTKT_VERSION : '', ), false ); } } catch ( \Throwable $ignored ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Never let reporting itself break the request. } } /** * The latest caught error, if it happened in the last 24 hours. * * @return array|null */ public static function recent() { $last = get_option( self::OPTION ); if ( is_array( $last ) && ! empty( $last['time'] ) && time() - (int) $last['time'] < DAY_IN_SECONDS ) { return $last; } return null; } /** * Forget the latest error (after the store manager dismisses it). * * @return void */ public static function clear() { delete_option( self::OPTION ); } }