595 lines
14 KiB
PHP
595 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Akeeba Engine
|
|
*
|
|
* @package akeebaengine
|
|
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 3, or later
|
|
*/
|
|
|
|
namespace Akeeba\Engine\Util;
|
|
|
|
defined('AKEEBAENGINE') || die();
|
|
|
|
use Akeeba\Engine\Factory;
|
|
use Akeeba\Engine\Platform;
|
|
use Akeeba\Engine\Util\Log\LogInterface;
|
|
use Akeeba\Engine\Util\Log\WarningsLoggerAware;
|
|
use Akeeba\Engine\Util\Log\WarningsLoggerInterface;
|
|
use Psr\Log\InvalidArgumentException;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\LogLevel;
|
|
|
|
/**
|
|
* Writes messages to the backup log file
|
|
*/
|
|
class Logger implements LoggerInterface, LogInterface, WarningsLoggerInterface
|
|
{
|
|
use WarningsLoggerAware;
|
|
|
|
/** @var string Full path to log file */
|
|
protected $logName = null;
|
|
|
|
/** @var string The current log tag */
|
|
protected $currentTag = null;
|
|
|
|
/** @var resource The file pointer to the current log file */
|
|
protected $fp = null;
|
|
|
|
/** @var bool Is the logging currently paused? */
|
|
protected $paused = false;
|
|
|
|
/** @var int The minimum log level */
|
|
protected $configuredLoglevel;
|
|
|
|
/** @var string The untranslated path to the site's root */
|
|
protected $site_root_untranslated;
|
|
|
|
/** @var string The translated path to the site's root */
|
|
protected $site_root;
|
|
|
|
/**
|
|
* Public constructor. Initialises the properties with the parameters from the backup profile and platform.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->initialiseWithProfileParameters();
|
|
}
|
|
|
|
/**
|
|
* When shutting down this class always close any open log files.
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
$this->close();
|
|
}
|
|
|
|
/**
|
|
* Clears the logfile
|
|
*
|
|
* @param string $tag Backup origin
|
|
*/
|
|
public function reset($tag = null)
|
|
{
|
|
// Pause logging
|
|
$this->pause();
|
|
|
|
// Get the file names for the default log and the tagged log
|
|
$currentLogName = $this->logName;
|
|
$this->logName = $this->getLogFilename($tag);
|
|
|
|
// Close the file if it's open
|
|
if ($currentLogName == $this->logName)
|
|
{
|
|
$this->close();
|
|
}
|
|
|
|
// Remove the log file if it exists
|
|
@unlink($this->logName);
|
|
|
|
// Reset the log file
|
|
$fp = @fopen($this->logName, 'w');
|
|
$hasWritten = false;
|
|
|
|
if ($fp !== false)
|
|
{
|
|
$hasWritten = fwrite($fp, '<' . '?' . 'php die(); ' . '?' . '>' . "\n") !== false;
|
|
@fclose($fp);
|
|
}
|
|
|
|
// If I could not write to a .log.php file try using a .log file instead.
|
|
if (!$hasWritten)
|
|
{
|
|
$this->logName = $this->getLogFilename($tag, '');
|
|
$fp = @fopen($this->logName, 'w');
|
|
$hasWritten = false;
|
|
|
|
if ($fp !== false)
|
|
{
|
|
$hasWritten = fwrite($fp, "\n") !== false;
|
|
@fclose($fp);
|
|
}
|
|
}
|
|
|
|
// Delete the default log file(s) if they exists
|
|
$defaultLog = $this->getLogFilename(null);
|
|
|
|
if (!empty($tag) && @file_exists($defaultLog))
|
|
{
|
|
@unlink($defaultLog);
|
|
}
|
|
|
|
$defaultLog = $this->getLogFilename(null, '');
|
|
|
|
if (!empty($tag) && @file_exists($defaultLog))
|
|
{
|
|
@unlink($defaultLog);
|
|
}
|
|
|
|
// Set the current log tag
|
|
$this->currentTag = $tag;
|
|
|
|
// Unpause logging
|
|
$this->unpause();
|
|
}
|
|
|
|
/**
|
|
* Writes a line to the log, if the log level is high enough
|
|
*
|
|
* @param string $level The log level
|
|
* @param string $message The message to write to the log
|
|
* @param array $context The logging context. For PSR-3 compatibility but not used in text file logs.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function log($level, $message = '', array $context = [])
|
|
{
|
|
// Warnings are enqueued no matter what is the minimum log level to report in the log file
|
|
if (in_array($level, [LogLevel::WARNING, LogLevel::NOTICE]))
|
|
{
|
|
$this->enqueueWarning($message);
|
|
}
|
|
|
|
// If we are told to not log anything we can't continue
|
|
if ($this->configuredLoglevel == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Open the log if it's closed
|
|
if (is_null($this->fp))
|
|
{
|
|
$this->open($this->currentTag);
|
|
}
|
|
|
|
// If the log could not be opened we can't continue
|
|
if (is_null($this->fp))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If the logging is paused we can't continue
|
|
if ($this->paused)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the log level as an integer (compatibility with our minimum log level configuration parameter)
|
|
switch ($level)
|
|
{
|
|
case LogLevel::EMERGENCY:
|
|
case LogLevel::ALERT:
|
|
case LogLevel::CRITICAL:
|
|
case LogLevel::ERROR:
|
|
$intLevel = 1;
|
|
break;
|
|
|
|
case LogLevel::WARNING:
|
|
case LogLevel::NOTICE:
|
|
$intLevel = 2;
|
|
break;
|
|
|
|
case LogLevel::INFO:
|
|
$intLevel = 3;
|
|
break;
|
|
|
|
case LogLevel::DEBUG:
|
|
$intLevel = 4;
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidArgumentException("Unknown log level $level", 500);
|
|
break;
|
|
}
|
|
|
|
// If the minimum log level is lower than what we're trying to log we cannot continue
|
|
if ($this->configuredLoglevel < $intLevel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
$translateRoot = true;
|
|
|
|
if (array_key_exists('root_translate', $context))
|
|
{
|
|
$translateRoot = ($context['root_translate'] === 1) || ($context['root_translate'] === '1') || ($context['root_translate'] === true);
|
|
}
|
|
|
|
// Replace the site's root with <root> in the log file
|
|
if ($translateRoot && !defined('AKEEBADEBUG'))
|
|
{
|
|
$message = str_replace($this->site_root_untranslated, "<root>", $message);
|
|
$message = str_replace($this->site_root, "<root>", $message);
|
|
}
|
|
|
|
// Replace new lines
|
|
$message = str_replace("\r\n", "\n", $message);
|
|
$message = str_replace("\r", "\n", $message);
|
|
$message = str_replace("\n", ' \n ', $message);
|
|
|
|
switch ($level)
|
|
{
|
|
case LogLevel::EMERGENCY:
|
|
case LogLevel::ALERT:
|
|
case LogLevel::CRITICAL:
|
|
case LogLevel::ERROR:
|
|
$string = "ERROR |";
|
|
break;
|
|
|
|
case LogLevel::WARNING:
|
|
case LogLevel::NOTICE:
|
|
$string = "WARNING |";
|
|
break;
|
|
|
|
case LogLevel::INFO:
|
|
$string = "INFO |";
|
|
break;
|
|
|
|
default:
|
|
$string = "DEBUG |";
|
|
break;
|
|
}
|
|
|
|
$string .= gmdate('Ymd H:i:s') . "|$message\r\n";
|
|
|
|
@fwrite($this->fp, $string);
|
|
}
|
|
|
|
/**
|
|
* Calculates the absolute path to the log file
|
|
*
|
|
* @param string $tag The backup run's tag
|
|
*
|
|
* @return string The absolute path to the log file
|
|
*/
|
|
public function getLogFilename($tag = null, $extension = '.php')
|
|
{
|
|
if (empty($tag))
|
|
{
|
|
$fileName = 'akeeba.log' . $extension;
|
|
}
|
|
else
|
|
{
|
|
$fileName = "akeeba.$tag.log" . $extension;
|
|
}
|
|
|
|
// Get output directory
|
|
$registry = Factory::getConfiguration();
|
|
$outputDirectory = $registry->get('akeeba.basic.output_directory');
|
|
|
|
// Get the log file name
|
|
$absoluteLogFilename = Factory::getFilesystemTools()->TranslateWinPath($outputDirectory . DIRECTORY_SEPARATOR . $fileName);
|
|
|
|
return $absoluteLogFilename;
|
|
}
|
|
|
|
/**
|
|
* Close the currently active log and set the current tag to null.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function close()
|
|
{
|
|
// The log file changed. Close the old log.
|
|
if (is_resource($this->fp))
|
|
{
|
|
@fclose($this->fp);
|
|
}
|
|
|
|
$this->fp = null;
|
|
$this->currentTag = null;
|
|
}
|
|
|
|
/**
|
|
* Open a new log instance with the specified tag. If another log is already open it is closed before switching to
|
|
* the new log tag. If the tag is null use the default log defined in the logging system.
|
|
*
|
|
* @param string|null $tag The log to open
|
|
*
|
|
* @return void
|
|
*/
|
|
public function open($tag = null, $extension = '.php')
|
|
{
|
|
// If the log is already open do nothing
|
|
if (is_resource($this->fp) && ($tag == $this->currentTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If another log is open, close it
|
|
if (is_resource($this->fp))
|
|
{
|
|
$this->close();
|
|
}
|
|
|
|
// Re-initialise site root and minimum log level since the active profile might have changed in the meantime
|
|
$this->initialiseWithProfileParameters();
|
|
|
|
// Set the current tag
|
|
$this->currentTag = $tag;
|
|
|
|
// Get the log filename
|
|
$this->logName = $this->getLogFilename($tag, $extension);
|
|
|
|
// Touch the file
|
|
@touch($this->logName);
|
|
|
|
// Open the log file. DO NOT USE APPEND ('ab') MODE. I NEED TO SEEK INTO THE FILE. SEE FURTHER BELOW!
|
|
$this->fp = @fopen($this->logName, 'c');
|
|
|
|
// If we couldn't open the file set the file pointer to null
|
|
if ($this->fp === false)
|
|
{
|
|
$this->fp = null;
|
|
|
|
return;
|
|
}
|
|
|
|
// Go to the end of the file, emulating append mode. DO NOT REPLACE THE fopen() FILE MODE!
|
|
if (@fseek($this->fp, 0, SEEK_END) === -1)
|
|
{
|
|
@fclose($this->fp);
|
|
@unlink($this->logName);
|
|
|
|
$this->fp = null;
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* The following sounds pretty stupid but there is a reason for that convoluted code.
|
|
*
|
|
* Some hosts, like WP Engine, will now allow you to write to a log file with a .php extension. The code below
|
|
* tries to anticipate that when the log extension is .php. It will try to write to the *.log.php file and the
|
|
* text is actually resembling PHP code. Hosts like WP Engine will fail the fwrite() which will cause this
|
|
* method to terminate early and return a null pointer. Our code will catch this case and try to use a .log
|
|
* extension as a safe fallback.
|
|
*/
|
|
if ($extension !== '.php')
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Try to write something into the file
|
|
$written = @fwrite($this->fp, '<?php die("test"); ?>' . "\n");
|
|
|
|
if ($written === false)
|
|
{
|
|
@fclose($this->fp);
|
|
@unlink($this->logName);
|
|
|
|
$this->fp = null;
|
|
|
|
$this->open($tag, '');
|
|
|
|
return;
|
|
}
|
|
|
|
// Store truncate offset, we will have to rewind the internal pointer to it
|
|
$truncate_point = ftell($this->fp) - $written;
|
|
|
|
if (ftruncate($this->fp, $truncate_point) === false)
|
|
{
|
|
@fclose($this->fp);
|
|
@unlink($this->logName);
|
|
|
|
$this->fp = null;
|
|
|
|
$this->open($tag, '');
|
|
|
|
return;
|
|
}
|
|
|
|
// Finally, move the file pointer at the truncation point. Otherwise PHP will append NULL bytes to the string
|
|
// to "pad" the file length to the internal file pointer. No need to check if the operation was successful,
|
|
// worst case scenario we will have some extra NULL bytes, there's no need to kill the log operation
|
|
@fseek($this->fp, $truncate_point);
|
|
}
|
|
|
|
/**
|
|
* Temporarily pause log output. The log() method MUST respect this.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function pause()
|
|
{
|
|
$this->paused = true;
|
|
}
|
|
|
|
/**
|
|
* Resume the previously paused log output. The log() method MUST respect this.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function unpause()
|
|
{
|
|
$this->paused = false;
|
|
}
|
|
|
|
/**
|
|
* Returns the timestamp (in UNIX time long integer format) of the last log message written to the log with the
|
|
* specific tag. The timestamp MUST be read from the log itself, not from the logger object. It is used by the
|
|
* engine to find out the age of stalled backups which may have crashed.
|
|
*
|
|
* @param string|null $tag The log tag for which the last timestamp is returned
|
|
*
|
|
* @return int|null The timestamp of the last log message, in UNIX time. NULL if we can't get the timestamp.
|
|
*/
|
|
public function getLastTimestamp($tag = null)
|
|
{
|
|
$fileName = $this->getLogFilename($tag);
|
|
|
|
/**
|
|
* The log file akeeba.tag.log.php may not exist but the akeeba.tag.log does. This would be the case in some bad
|
|
* hosts, like WPEngine, which do not allow us to create .php files EVEN THOUGH that's the only way to ensure
|
|
* the privileged information in the log file is not readable over the web. You can't fix bad hosts, you can
|
|
* only work around them.
|
|
*/
|
|
if (!@file_exists($fileName) && @file_exists(substr($fileName, 0, -4)))
|
|
{
|
|
$fileName = substr($fileName, 0, -4);
|
|
}
|
|
|
|
$timestamp = @filemtime($fileName);
|
|
|
|
if ($timestamp === false)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return $timestamp;
|
|
}
|
|
|
|
/**
|
|
* System is unusable.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function emergency($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::EMERGENCY, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Action must be taken immediately.
|
|
*
|
|
* Example: Entire website down, database unavailable, etc. This should
|
|
* trigger the SMS alerts and wake you up.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function alert($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::ALERT, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Critical conditions.
|
|
*
|
|
* Example: Application component unavailable, unexpected exception.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function critical($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::CRITICAL, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Runtime errors that do not require immediate action but should typically
|
|
* be logged and monitored.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function error($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::ERROR, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* \Exceptional occurrences that are not errors.
|
|
*
|
|
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
|
* that are not necessarily wrong.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function warning($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::WARNING, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Normal but significant events.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function notice($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::NOTICE, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Interesting events.
|
|
*
|
|
* Example: User logs in, SQL logs.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function info($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::INFO, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Detailed debug information.
|
|
*
|
|
* @param string $message
|
|
* @param array $context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function debug($message, array $context = [])
|
|
{
|
|
$this->log(LogLevel::DEBUG, $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Initialise the logger properties with parameters from the backup profile and the platform
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function initialiseWithProfileParameters()
|
|
{
|
|
// Get the site's translated and untranslated root
|
|
$this->site_root_untranslated = Platform::getInstance()->get_site_root();
|
|
$this->site_root = Factory::getFilesystemTools()->TranslateWinPath($this->site_root_untranslated);
|
|
|
|
// Load the registry and fetch log level
|
|
$registry = Factory::getConfiguration();
|
|
$this->configuredLoglevel = $registry->get('akeeba.basic.log_level');
|
|
$this->configuredLoglevel = $this->configuredLoglevel * 1;
|
|
}
|
|
}
|