<?php

/**
 * Class MC4WP_Debug_Log
 *
 * Simple logging class which writes to a file, loosely based on PSR-3.
 */
class MC4WP_Debug_Log {


	/**
	 * Detailed debug information
	 */
	const DEBUG = 100;

	/**
	 * Interesting events
	 *
	 * Examples: Visitor subscribed
	 */
	const INFO = 200;

	/**
	 * Exceptional occurrences that are not errors
	 *
	 * Examples: User already subscribed
	 */
	const WARNING = 300;

	/**
	 * Runtime errors
	 */
	const ERROR = 400;

	/**
	 * Logging levels from syslog protocol defined in RFC 5424
	 *
	 * @var array $levels Logging levels
	 */
	protected static $levels = array(
		self::DEBUG   => 'DEBUG',
		self::INFO    => 'INFO',
		self::WARNING => 'WARNING',
		self::ERROR   => 'ERROR',
	);

	/**
	 * @var string The file to which messages should be written.
	 */
	public $file;

	/**
	 * @var int Only write messages with this level or higher
	 */
	public $level;

	/**
	 * @var resource
	 */
	protected $stream;

	/**
	 * MC4WP_Debug_Log constructor.
	 *
	 * @param string $file
	 * @param mixed $level;
	 */
	public function __construct( $file, $level = self::DEBUG ) {
		$this->file  = $file;
		$this->level = self::to_level( $level );
	}

	/**
	 * @param mixed $level
	 * @param string $message
	 * @return boolean
	 */
	public function log( $level, $message ) {
		$level = self::to_level( $level );

		// only log if message level is higher than log level
		if ( $level < $this->level ) {
			return false;
		}

		// obfuscate email addresses in log message since log might be public.
		$message = mc4wp_obfuscate_email_addresses( (string) $message );

		// first, get rid of everything between "invisible" tags
		$message = preg_replace( '/<(?:style|script|head)>.+?<\/(?:style|script|head)>/is', '', $message );

		// then, strip tags (while retaining content of these tags)
		$message = strip_tags( $message );
		$message = trim( $message );

		/**
		 * Modifies the message that is written to the debug log.
		 * Return an empty string to skip logging this message altogether.
		 *
		 * @param string $message
		 */
		$message = apply_filters( 'mc4wp_debug_log_message', $message );
		if ( empty( $message ) ) {
			return false;
		}

		// generate line
		$level_name = self::get_level_name( $level );
		$datetime   = gmdate( 'Y-m-d H:i:s', time() + ( get_option( 'gmt_offset', 0 ) * HOUR_IN_SECONDS ) );
		$message    = sprintf( '[%s] %s: %s', $datetime, $level_name, $message ) . PHP_EOL;

		// did we open stream yet?
		if ( ! is_resource( $this->stream ) ) {
			// attempt to open stream
			$this->stream = @fopen( $this->file, 'c+' );
			if ( ! is_resource( $this->stream ) ) {
				return false;
			}

			// make sure first line of log file is a PHP tag + exit statement (to prevent direct file access)
			$line            = fgets( $this->stream );
			$php_exit_string = '<?php exit; ?>';
			if ( strpos( $line, $php_exit_string ) !== 0 ) {
				rewind( $this->stream );
				fwrite( $this->stream, $php_exit_string . PHP_EOL . $line );
			}

			// place pointer at end of file
			fseek( $this->stream, 0, SEEK_END );
		}

		// lock file while we write, ignore errors (not much we can do)
		flock( $this->stream, LOCK_EX );

		// write the message to the file
		fwrite( $this->stream, $message );

		// unlock file again, but don't close it for remainder of this request
		flock( $this->stream, LOCK_UN );

		return true;
	}

	/**
	 * @param string $message
	 * @return boolean
	 */
	public function warning( $message ) {
		return $this->log( self::WARNING, $message );
	}

	/**
	 * @param string $message
	 * @return boolean
	 */
	public function info( $message ) {
		return $this->log( self::INFO, $message );
	}

	/**
	 * @param string $message
	 * @return boolean
	 */
	public function error( $message ) {
		return $this->log( self::ERROR, $message );
	}

	/**
	 * @param string $message
	 * @return boolean
	 */
	public function debug( $message ) {
		return $this->log( self::DEBUG, $message );
	}

	/**
	 * Converts PSR-3 levels to local ones if necessary
	 *
	 * @param string|int Level number or name (PSR-3)
	 * @return int
	 */
	public static function to_level( $level ) {
		if ( is_string( $level ) ) {
			$level = strtoupper( $level );
			if ( defined( __CLASS__ . '::' . $level ) ) {
				return constant( __CLASS__ . '::' . $level );
			}

			throw new InvalidArgumentException( 'Level "' . $level . '" is not defined, use one of: ' . implode( ', ', array_keys( self::$levels ) ) );
		}

		return $level;
	}

	/**
	 * Gets the name of the logging level.
	 *
	 * @param  int    $level
	 * @return string
	 */
	public static function get_level_name( $level ) {
		if ( ! isset( self::$levels[ $level ] ) ) {
			throw new InvalidArgumentException( 'Level "' . $level . '" is not defined, use one of: ' . implode( ', ', array_keys( self::$levels ) ) );
		}

		return self::$levels[ $level ];
	}

	/**
	 * Tests if the log file is writable
	 *
	 * @return bool
	 */
	public function test() {
		$handle   = @fopen( $this->file, 'a' );
		$writable = false;

		if ( is_resource( $handle ) ) {
			$writable = true;
			fclose( $handle );
		}

		return $writable;
	}
}
