<?php
/**
 * @brief		Log Class
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @since		29 Mar 2016
 */

namespace IPS;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Log Class
 */
class _Log extends \IPS\Patterns\ActiveRecord
{
	/**
	 * @brief	[ActiveRecord] Multiton Store
	 */
	protected static $multitons;
	
	/**
	 * @brief	[ActiveRecord] Database Table
	 */
	public static $databaseTable = 'core_log';
	
	/**
	 * Set Default Values
	 *
	 * @return	void
	 */
	public function setDefaultValues()
	{
		$this->time = time();
	}
	
	/**
	 * @brief	Track if we are currently logging to prevent recursion for database issues
	 */
	static protected $currentlyLogging	= FALSE;

	/**
	 * Write a log message
	 *
	 * @param	\Exception|string	$message	An Exception object or a generic message to log
	 * @param	string|null			$category	An optional string identifying the type of log (for example "upgrade")
	 * @return	\IPS\Log|null
	 */
	public static function log( $message, $category = NULL )
	{
		/* Anything to log? */
		if( !$message )
		{
			return NULL;
		}

		/* Try to log it to the database */
		try
		{
			if( static::$currentlyLogging === TRUE )
			{
				throw new \RuntimeException;
			}

			static::$currentlyLogging	= TRUE;

			$log = new static;
			
			if ( $message instanceof \Exception )
			{
				$log->exception_class = \get_class( $message );
				$log->exception_code = $message->getCode();

				if ( method_exists( $message, 'extraLogData' ) AND $extraData = $message->extraLogData() )
				{
					$log->message = $extraData . "\n" . $message->getMessage();
				}
				else
				{
					$log->message = $message->getMessage();
				}

				$log->backtrace = $message->getTraceAsString();
			}
			else
			{
				if ( \is_array( $message ) )
				{
					$message = var_export( $message, TRUE );
				}
				$log->message = $message;
				$log->backtrace = ( new \Exception )->getTraceAsString();
			}

			/* If this is an actual request and not command line-invoked */
			if( \IPS\Dispatcher::hasInstance() AND mb_strpos( php_sapi_name(), 'cli' ) !== 0 )
			{
				$log->url = \IPS\Request::i()->url();
				try
				{
					// Early exceptions may occur before the logged in object is set up.
					$log->member_id = \IPS\Member::loggedIn()->member_id ?: 0;
				}
				catch( \Error $e )
				{
					$log->member_id = 0;
				}
			}

			if ( \IPS\Application::load('core')->long_version >= 106149 ) #Column was added in 106149
			{
				$log->loaded_hooks = json_encode( \IPS\IPS::$loadedHooks );
			}
			$log->category = $category;
			$log->save();

			static::$currentlyLogging	= FALSE;
			
			return $log;
		}
		/* If that fails, log to disk */
		catch ( \Exception $e )
		{
			if ( !\IPS\NO_WRITES and !\IPS\CIC )
			{
				/* What are we writing? */
				$date = date('r');
				if ( $message instanceof \Exception )
				{
					$messageToLog = $date . "\n" . \get_class( $message ) . '::' . $message->getCode() . "\n" . $message->getMessage() . "\n" . $message->getTraceAsString();
				}
				else
				{
					if ( \is_array( $message ) )
					{
						$message = var_export( $message, TRUE );
					}
					$messageToLog = $date . "\n" . $message . "\n" . ( new \Exception )->getTraceAsString();
				}
				
				/* Where are we writing it? */
				$dir = str_replace( '{root}', \IPS\ROOT_PATH, \IPS\LOG_FALLBACK_DIR );
				if ( !is_dir( $dir ) )
				{
					if ( !@mkdir( $dir ) or !@chmod( $dir, \IPS\IPS_FOLDER_PERMISSION ) )
					{
						return;
					}
				}
				
				/* Write it */
				$header = "<?php exit; ?>\n\n";
				$file = $dir . '/' . date( 'Y' ) . '_' . date( 'm' ) . '_' . date('d') . '_' . ( $category ?: 'nocategory' ) . '.php';
				if ( file_exists( $file ) )
				{
					@\file_put_contents( $file, "\n\n-------------\n\n" . $messageToLog, FILE_APPEND );
				}
				else
				{
					@\file_put_contents( $file, $header . $messageToLog );
				}
				@chmod( $file, \IPS\IPS_FILE_PERMISSION );
			}
		}
		
		return NULL;
	}
	
	/**
	 * Write a debug message
	 *
	 * @param	\Exception|string	$message	An Exception object or a generic message to log
	 * @param	string|null			$category	An optional string identifying the type of log (for example "upgrade")
	 * @return	\IPS\Log|null
	 */
	public static function debug( $message, $category = NULL )
	{
		if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
		{
			return static::log( $message, $category );
		}
		return NULL;
	}
	
	/**
	 * Get fallback directory
	 *
	 * @return	string|null
	 */
	public static function fallbackDir()
	{
		if ( \IPS\CIC )
		{
			return NULL;
		}
		return str_replace( '{root}', \IPS\ROOT_PATH, \IPS\LOG_FALLBACK_DIR );
	}
	
	/**
	 * Prune logs
	 * 
	 * @param	int		$days	Older than (days) to prune
	 * @return	void
	 */
	public static function pruneLogs( $days )
	{
		\IPS\Db::i()->delete( static::$databaseTable, array( 'time<?', \IPS\DateTime::create()->sub( new \DateInterval( 'P' . $days . 'D' ) )->getTimestamp() ) );
		
		if ( !\IPS\NO_WRITES )
		{
			$dir = static::fallbackDir();
			if ( is_dir( $dir ) )
			{
				try
				{
					$it = new \DirectoryIterator( $dir );
				}
				catch ( Exception $e )
				{
					return;
				}
				
				foreach( $it as $file )
				{
					try
					{
						if( $file->isDot() or !$file->isFile() )
						{
							continue;
						}
				
						if( preg_match( "#.cgi$#", $file->getFilename(), $matches ) or ( preg_match( "#.php$#", $file->getFilename(), $matches ) and $file->getMTime() < ( time() - ( 60 * 60 * 24 * (int) $days ) ) ) )
						{
							@unlink( $file->getPathname() );
						}
					}
					catch ( Exception $e ) { }
				}
			}
		}
	}
	
	/**
	 * Get Loaded Hooks
	 *
	 * @return	array
	 */
	public function get_loaded_hooks()
	{
		return ( $this->_data['loaded_hooks'] ) ? json_decode( $this->_data['loaded_hooks'] ) : array();
	}
	
	/**
	 * Get Hook details
	 * 
	 * @param	string		$file	filename of loaded hook
	 * @return	array
	 */
	public static function hookDetails( $file )
	{
		$bits = explode( '/', $file );
		
		$return = array();
		
		/* Plugins are always third party */
		if ( $bits[0] === 'plugins' )
		{
			$plugin = NULL;
			try
			{
				$plugin = \IPS\Plugin::getPluginFromPath( "/" . $file );
			}
			catch ( \Exception $e ) {}
			
			$return['type'] = 'plugin';
			$return['id'] = ( $plugin ) ? $plugin->id : NULL;
		}
		else
		{
			$return['type'] = 'app';
			$return['id'] = $bits[1];
		}
		return $return;
	}
}