Files

360 lines
9.2 KiB
PHP

<?php
defined( 'WPINC' ) OR exit;
/**
* Encapsulates the logic required to maintain and read log files.
*/
class DG_Logger {
/**
* @var string Name of the log purge action.
*/
const PurgeLogsAction = 'document-gallery_purge-logs';
/**
* Appends DG log file if logging is enabled.
*
* @param int $level The level of serverity (should be passed using DG_LogLevel consts).
* @param string $entry Value to be logged.
* @param bool $stacktrace Whether to include full stack trace.
* @param bool $force Whether to ignore logging flag and log no matter what.
* Only should be used when the user *must* know about something.
*/
public static function writeLog( $level, $entry, $stacktrace = false, $force = false ) {
if ( $force || self::logEnabled() ) {
$fp = fopen( self::getLogFileName(), 'a' );
if ( false !== $fp ) {
$fields = array( time(), $level, $entry );
$trace = debug_backtrace( false );
if ( $stacktrace ) {
unset( $trace[0] );
$fields[] = self::getStackTraceString( $trace );
} else {
// Ignore first item from backtrace as it's this function which is redundant.
$caller = $trace[1];
$class = isset( $caller['class'] ) ? $caller['class'] : '';
$type = isset( $caller['type'] ) ? $caller['type'] : '';
$caller = $class . $type . $caller['function'];
$fields[2] = '(' . $caller . ') ' . $fields[2];
}
fputcsv( $fp, $fields );
fclose( $fp );
} // TODO: else
}
}
/**
* Reads the current blog's log file, placing the values in to a 2-dimensional array.
*
* @param int $skip How many lines to skip before returning rows.
* @param int $limit Max number of lines to read.
*
* @return string[][]|null The rows from the log file or null if failed to open log.
*/
public static function readLog( $skip = 0, $limit = PHP_INT_MAX ) {
$ret = null;
$fp = @fopen( self::getLogFileName(), 'r' );
if ( $fp !== false ) {
$ret = array();
while ( count( $ret ) < $limit && false !== ( $fields = fgetcsv( $fp ) ) ) {
if ( $skip > 0 ) {
$skip --;
continue;
}
if ( ! is_null( $fields ) ) {
$ret[] = $fields;
}
}
@fclose( $fp );
}
return $ret;
}
/**
* Clears the log file for the active blog.
*/
public static function clearLog() {
// we don't care if the file actually exists -- it won't when we're done
@unlink( self::getLogFileName() );
}
/**
* Clears the log file for all blogs.
*/
public static function clearLogs() {
// we don't care if the files actually exist -- they won't when we're done
foreach ( DG_Util::getBlogIds() as $id ) {
@unlink( self::getLogFileName( $id ) );
}
}
/**
* Truncates all blog logs to the current purge interval.
*
* TODO: This is a memory hog. Consider switching to stream filter.
*/
public static function purgeExpiredEntries() {
self::writeLog( DG_LogLevel::Detail, 'Beginning scheduled log file purge.' );
$blogs = array( null );
if ( is_multisite() ) {
$blogs = DG_Util::getBlogIds();
}
// truncate each blog's log file
$time = time();
foreach ( $blogs as $blog ) {
$blog_num = ! is_null( $blog ) ? $blog : get_current_blog_id();
$options = self::getOptions( $blog );
$purge_time = $time - $options['purge_interval'] * DAY_IN_SECONDS;
// purging is disabled for this blog
if ( $purge_time >= $time ) {
continue;
}
// do purge for this blog
$file = self::getLogFileName( $blog_num );
if ( file_exists( $file ) ) {
$fp = @fopen( $file, 'r' );
if ( $fp !== false ) {
$truncate = false;
$offset = 0;
// find the first non-expired entry
while ( ( $fields = fgetcsv( $fp ) ) !== false ) {
if ( ! is_null( $fields ) && intval( $fields[0] ) >= $purge_time ) {
// we've reached the recent entries -- nothing beyond here will be removed
break;
}
$offset = @ftell( $fp );
if ( false === $offset ) {
break;
}
$truncate = true;
}
@fclose( $fp );
// if any expired entries exist -- remove them from the file
if ( $truncate ) {
self::writeLog( DG_LogLevel::Detail, "Purging log entries for blog #$blog_num." );
$data = file_get_contents( $file, false, null, $offset );
file_put_contents( $file, $data, LOCK_EX );
}
}
}
}
}
/**
* Generally not necessary to call external to this class -- only use if generating
* log entry will take significant resources and you want to avoid this operation
* if it will not actually be logged.
*
* @return bool Whether debug logging is currently enabled.
*/
public static function logEnabled() {
$options = self::getOptions();
return $options['enabled'];
}
/**
* Gets logging options.
*
* @param int $blog ID of the blog to be retrieved in multisite env.
*
* @return mixed[] Logger options for the blog.
*/
public static function getOptions( $blog = null ) {
$options = DocumentGallery::getOptions( $blog );
return $options['logging'];
}
/**
* @param $id int The ID of the blog to retrieve log file name for. Defaults to current blog.
*
* @return string Full path to log file for current blog.
*/
private static function getLogFileName( $id = null ) {
$id = ! is_null( $id ) ? $id : get_current_blog_id();
return DG_PATH . 'log/' . $id . '.log';
}
/**
* @param mixed[][] $trace Array containing stack trace to be converted to string.
*
* @return string The stack trace in human-readable form.
*/
private static function getStackTraceString( $trace ) {
$trace_str = '';
$i = 1;
foreach ( $trace as $node ) {
$trace_str .= "#$i ";
$file = '';
if ( isset( $node['file'] ) ) {
// convert to relative path from WP root
$file = str_replace( ABSPATH, '', $node['file'] );
}
if ( isset( $node['line'] ) ) {
$file .= "({$node['line']})";
}
if ( $file ) {
$trace_str .= "$file: ";
}
if ( isset( $node['class'] ) ) {
$trace_str .= "{$node['class']}{$node['type']}";
}
if ( isset( $node['function'] ) ) {
// only include args for first item in stack trace
$args = '';
if ( 1 === $i && isset( $node['args'] ) ) {
$args = implode( ', ', array_map( array( __CLASS__, 'print_r' ), $node['args'] ) );
}
$trace_str .= "{$node['function']}($args)" . PHP_EOL;
}
$i ++;
}
return $trace_str;
}
/**
* Wraps print_r passing true for the return argument.
*
* @param mixed $v Value to be printed.
*
* @return string Printed value.
*/
private static function print_r( $v ) {
return preg_replace( '/\s+/', ' ', print_r( $v, true ) );
}
/**
* Blocks instantiation. All functions are static.
*/
private function __construct() {
}
}
/**
* LogLevel acts as an enumeration of all possible log levels.
*/
class DG_LogLevel {
/**
* @var int Log level for anything that doesn't indicate a problem.
*/
const Detail = 0;
/**
* @var int Log level for anything that is a minor issue.
*/
const Warning = 1;
/**
* @var int Log level for when something went wrong.
*/
const Error = 2;
/**
* @var ReflectionClass Backs the getter.
*/
private static $ref = null;
/**
* @return ReflectionClass Instance of reflection class for this class.
*/
private static function getReflectionClass() {
if ( is_null( self::$ref ) ) {
self::$ref = new ReflectionClass( __CLASS__ );
}
return self::$ref;
}
/**
* @var int[] Backs the getter.
*/
private static $levels = null;
/**
* @return int[] Associative array containing all log level names mapped to their int value.
*/
public static function getLogLevels() {
if ( is_null( self::$levels ) ) {
$ref = self::getReflectionClass();
self::$levels = $ref->getConstants();
}
return self::$levels;
}
/**
* @param string $name Name to be checked for validity.
*
* @return bool Whether given name represents valid log level.
*/
public static function isValidName( $name ) {
return array_key_exists( $name, self::getLogLevels() );
}
/**
* @param int $value Value to be checked for validity.
*
* @return bool Whether given value represents valid log level.
*/
public static function isValidValue( $value ) {
return ( false !== array_search( $value, self::getLogLevels() ) );
}
/**
* @param string $name The name for which to retrieve a value.
*
* @return int|null The value associated with the given name.
*/
public static function getValueByName( $name ) {
$levels = self::getLogLevels();
return array_key_exists( $name, self::getLogLevels() ) ? $levels[ $name ] : null;
}
/**
* @param int $value The value for which to retrieve a name.
*
* @return string|null The name associated with the given value.
*/
public static function getNameByValue( $value ) {
$ret = array_search( $value, self::getLogLevels() );
return ( false !== $ret ) ? $ret : null;
}
/**
* Blocks instantiation. All functions are static.
*/
private function __construct() {
}
}