store; } /** * Returns the level a site has asked for. */ public function threshold(): string { $level = Settings::text( 'log_level' ); return array_key_exists( $level, LogLevel::ranks() ) ? $level : LogLevel::OFF; } /** * Determines whether a level would be written. * * Worth asking before assembling an expensive context. * * @param string $level One of the {@see LogLevel} constants. * * @return bool True when a line at this level is kept. */ public function is_enabled( string $level ): bool { return LogLevel::allows( $level, $this->threshold() ); } /** * Writes a line. * * @param string $level One of the {@see LogLevel} constants. * @param string $channel One of the channel constants. * @param string $message What happened, in one sentence. * @param array $context Optional. Identifiers and reasons, never answers. * * @return bool True when the line was written. */ public function log( string $level, string $channel, string $message, array $context = array() ): bool { if ( ! $this->is_enabled( $level ) ) { return false; } /** * Filters a line before it is written. * * Return an empty message to drop it. * * @since 1.0.0 * * @param string $message What happened. * @param string $level Severity. * @param string $channel Channel. * @param array $context Extra detail. */ $message = (string) apply_filters( 'formpilot_log_message', $message, $level, $channel, $context ); if ( '' === trim( $message ) ) { return false; } $line = sprintf( '[%1$s] %2$s %3$s: %4$s%5$s', gmdate( 'Y-m-d H:i:s' ), strtoupper( $level ), $channel, $this->clean( $message ), $this->encode( $context ) ); return $this->store->write( $line ); } /** * Writes a line at the error level. * * @param string $channel One of the channel constants. * @param string $message What happened. * @param array $context Optional. Extra detail. * * @return bool True when the line was written. */ public function error( string $channel, string $message, array $context = array() ): bool { return $this->log( LogLevel::ERROR, $channel, $message, $context ); } /** * Writes a line at the warning level. * * @param string $channel One of the channel constants. * @param string $message What happened. * @param array $context Optional. Extra detail. * * @return bool True when the line was written. */ public function warning( string $channel, string $message, array $context = array() ): bool { return $this->log( LogLevel::WARNING, $channel, $message, $context ); } /** * Writes a line at the info level. * * @param string $channel One of the channel constants. * @param string $message What happened. * @param array $context Optional. Extra detail. * * @return bool True when the line was written. */ public function info( string $channel, string $message, array $context = array() ): bool { return $this->log( LogLevel::INFO, $channel, $message, $context ); } /** * Writes a line at the debug level. * * @param string $channel One of the channel constants. * @param string $message What happened. * @param array $context Optional. Extra detail. * * @return bool True when the line was written. */ public function debug( string $channel, string $message, array $context = array() ): bool { return $this->log( LogLevel::DEBUG, $channel, $message, $context ); } /** * Writes a WP_Error, code and message together. * * @param string $channel One of the channel constants. * @param WP_Error $error Error to record. * @param array $context Optional. Extra detail. * * @return bool True when the line was written. */ public function wp_error( string $channel, WP_Error $error, array $context = array() ): bool { return $this->error( $channel, $error->get_error_message(), array_merge( array( 'code' => $error->get_error_code() ), $context ) ); } /** * Writes a caught exception. * * The file and line are included; the stack trace is not. A trace carries the * arguments each frame was called with, which is how a submitted password * ends up in a log nobody thought was sensitive. * * @param string $channel One of the channel constants. * @param Throwable $exception Exception to record. * @param array $context Optional. Extra detail. * * @return bool True when the line was written. */ public function exception( string $channel, Throwable $exception, array $context = array() ): bool { return $this->error( $channel, $exception->getMessage(), array_merge( array( 'type' => $exception::class, 'file' => basename( $exception->getFile() ) . ':' . $exception->getLine(), ), $context ) ); } /** * Flattens a message onto one line. * * A line break inside a message would look like the start of a new entry to * anything reading the file back. * * @param string $message Raw message. */ private function clean( string $message ): string { return trim( (string) preg_replace( '/\s+/u', ' ', wp_strip_all_tags( $message ) ) ); } /** * Renders the context as compact JSON. * * @param array $context Extra detail. * * @return string The encoded context, prefixed with a space, or an empty string. */ private function encode( array $context ): string { if ( array() === $context ) { return ''; } $json = wp_json_encode( $context ); if ( ! is_string( $json ) ) { return ''; } $json = (string) preg_replace( '/\s+/u', ' ', $json ); if ( mb_strlen( $json ) > self::MAX_CONTEXT ) { $json = mb_substr( $json, 0, self::MAX_CONTEXT ) . '…'; } return ' ' . $json; } }