<?php
/**
* "Dumps input data to a file on disk."
* @author: Claudius Tiberiu Iacob <hide@address.com>
* @link: http://goodys-in-code.ciacob.org
* @version: 1.0
*/
class Logger extends ParamsProxy {
const NOT_INITIALIZED = "NOT_INITIALIZED";
const ILLEGAL_MAX_FILE_SIZE = "ILLEGAL_MAX_FILE_SIZE";
const ILLEGAL_OPERATING_MODE = "ILLEGAL_OPERATING_MODE";
const ILLEGAL_WRAP_LIMIT = "ILLEGAL_WRAP_LIMIT";
const ILLEGAL_BANNER_TEMPLATE = "ILLEGAL_BANNER_TEMPLATE";
const ILLEGAL_ENTRY_TEMPLATE = "ILLEGAL_ENTRY_TEMPLATE";
const ILLEGAL_CONTEXT_NAME = "ILLEGAL_CONTEXT_NAME";
const CANNOT_REDEFINE_CONTEXT = "CANNOT_REDEFINE_CONTEXT";
const ILLEGAL_CONTEXT_DEFINITION = "ILLEGAL_CONTEXT_DEFINITION";
const ILLEGAL_CLASS_NAME = "ILLEGAL_CLASS_NAME";
const CANNOT_REASIGN_CLASS_NAME = "CANNOT_REASIGN_CLASS_NAME";
const CLASS_IS_NOT_ASIGNED = "CLASS_IS_NOT_ASIGNED";
const LOGS_DIR_NOT_WRITABLE = "LOGS_DIR_NOT_WRITABLE";
const LOGS_DIR_ROOT_NOT_WRITABLE = "LOGS_DIR_ROOT_NOT_WRITABLE";
private static $instance = null;
private $isInitialized = false;
private $logsPath = null;
private $logsDirName = null;
private $logFileName = "log.txt";
private $contexts = null;
private $mainCtxName = "Main";
private $maxFileSize = null;
private $operatingMode = null;
private $trimToken = "trim";
private $splitToken = "split";
private $wrapLimit = null;
private $bannerTemplate = null;
private $entryTemplate = null;
private $bannerLinesNo = 5;
private $maxWrapLimit = 240;
private $tabSize = 4;
private $filePathPH = "FILE PATH";
private $fileSizePH = "FILE SIZE";
private $fileStatusPH = "FILE STATUS";
private $lastModifiedDate = "LAST MODIFIED DATE";
private $relatedContextName = "RELATED CONTEXT NAME";
private $yPH = "Y";
private $mPH = "M";
private $dPH = "D";
private $hPH = "H";
private $minPH = "MIN";
private $sPH = "S";
private $callingClassNamePH = "CALLING CLASS NAME";
private $contentPH = "CONTENT";
private $fullStatus = "full";
private $trimmedStatus = "trimmed";
private $volumeStatus = "vol. no. #";
private $currLogFilePath = null;
private $currLogFileSize = null;
private $currLogFileStatus = null;
private $currLogFileDate = null;
private $currCallingCtx = null;
private $currCallingCls = null;
public static function getInstance () {
if (!self::$instance instanceof self) {
self::$instance = new self();
}
return self::$instance;
}
protected function __construct () {
parent::__construct();
$this->logsDirName = "logs";
$this->contexts = array ();
$this->maxFileSize = 125;
$this->operatingMode = $this->trimToken;
$this->wrapLimit = -1;
$this->bannerTemplate = "";
$this->entryTemplate = "%" . $this->contentPH . "%\n";
parent::configure($this);
$this->logsPath = $this->getLogsPath ();
$this->contexts [$this->mainCtxName] = array ();
$this->isInitialized = true;
}
private function __clone () {
// disabling cloning
}
private function __wakeup () {
// disabling serialization
}
public function configure ($instance) {
// disabling the ability to configure an arbitrary class instance
}
public function setMaxFileSize ($value) {
if (!is_int($value)) {
trigger_error(Logger::ILLEGAL_MAX_FILE_SIZE, E_USER_ERROR);
}
if ($value < 125 || $value > 1250) {
trigger_error(Logger::ILLEGAL_MAX_FILE_SIZE, E_USER_ERROR);
}
$this->maxFileSize = $value * 1024;
}
public function setOperatingMode ($value) {
if (!is_string($value)) {
trigger_error(Logger::ILLEGAL_OPERATING_MODE, E_USER_ERROR);
}
if ($value !== $this->trimToken && $value !== $this->splitToken) {
trigger_error(Logger::ILLEGAL_OPERATING_MODE, E_USER_ERROR);
}
$this->operatingMode = $value;
}
public function setWrapLimit ($value) {
if (!is_int($value)) {
trigger_error(Logger::ILLEGAL_WRAP_LIMIT, E_USER_ERROR);
}
if ($value < 80 && $value > $this->maxWrapLimit && $value !== -1) {
trigger_error(Logger::ILLEGAL_WRAP_LIMIT, E_USER_ERROR);
}
$this->wrapLimit = $value;
}
public function setBannerTemplate ($value) {
if (!is_string($value)) {
trigger_error(Logger::ILLEGAL_BANNER_TEMPLATE, E_USER_ERROR);
}
$value = $this->sanitizeString($value);
$this->bannerTemplate = $value;
}
public function setEntryTemplate ($value) {
if (!is_string($value)) {
trigger_error(Logger::ILLEGAL_ENTRY_TEMPLATE, E_USER_ERROR);
}
$value = trim ($value);
if ($value === "") {
trigger_error(Logger::ILLEGAL_ENTRY_TEMPLATE, E_USER_ERROR);
}
if (strpos($value, "%" .$this->contentPH . "%") === false) {
trigger_error(Logger::ILLEGAL_ENTRY_TEMPLATE, E_USER_ERROR);
}
$value = $this->sanitizeString($value);
$value = chr (1) . $value;
$this->entryTemplate = $value;
}
private function __call ($funcName, $arguments) {
$ctxName = preg_replace('/^set/', "", $funcName);
$ctxName = trim ($ctxName);
if ($ctxName === "") {
trigger_error(Logger::ILLEGAL_CONTEXT_NAME, E_USER_ERROR);
}
if (strpos($ctxName, " ") !== false) {
trigger_error(Logger::ILLEGAL_CONTEXT_NAME, E_USER_ERROR);
}
if (strpos($ctxName, "__") === 0) {
trigger_error(Logger::ILLEGAL_CONTEXT_NAME, E_USER_ERROR);
}
if (preg_match('/^[_a-zA-Z]?[_a-zA-Z0-9]*$/', $ctxName) === 0) {
trigger_error(Logger::ILLEGAL_CONTEXT_NAME, E_USER_ERROR);
}
if (array_key_exists($ctxName, $this->contexts)) {
if ($ctxName !== $this->$mainCtxName) {
trigger_error(Logger::CANNOT_REDEFINE_CONTEXT, E_USER_ERROR);
}
}
$clsNames = $arguments[0];
if (!is_array($clsNames)) {
trigger_error(Logger::ILLEGAL_CONTEXT_DEFINITION, E_USER_ERROR);
}
if (count ($clsNames) === 0) {
trigger_error(Logger::ILLEGAL_CONTEXT_DEFINITION, E_USER_ERROR);
}
foreach ($clsNames as $clsName) {
if (!is_string($clsName)) {
trigger_error(Logger::ILLEGAL_CLASS_NAME, E_USER_ERROR);
}
$clsName = trim ($clsName);
if ($clsName === "") {
trigger_error(Logger::ILLEGAL_CLASS_NAME, E_USER_ERROR);
}
if (strpos($clsName, " ") !== false) {
trigger_error(Logger::ILLEGAL_CLASS_NAME, E_USER_ERROR);
}
if (strpos($clsName, "__") === 0) {
trigger_error(Logger::ILLEGAL_CLASS_NAME, E_USER_ERROR);
}
if (preg_match('/^[_a-zA-Z]?[_a-zA-Z0-9]*$/', $clsName) === 0) {
trigger_error(Logger::ILLEGAL_CLASS_NAME, E_USER_ERROR);
}
$isClsNameAssigned = ($this->array_searchRecursive ($clsName, $this->contexts, true) !==
false);
if ($isClsNameAssigned) {
trigger_error (Logger::CANNOT_REASIGN_CLASS_NAME, E_USER_ERROR);
}
}
$this->contexts [$ctxName] = $clsNames;
}
public function log ($data) {
if (!$this->isInitialized) {
trigger_error (Logger::NOT_INITIALIZED, E_USER_ERROR);
}
$callingCtxName = null;
if (count ($this->contexts) === 1) {
if (count ($this->contexts[$this->mainCtxName]) === 0) {
$callingCtxName = $this->mainCtxName;
}
}
if ($callingCtxName === null) {
$trace = debug_backtrace();
$callingClsName = $trace[1]["class"];
$res = $this->array_searchRecursive($callingClsName, $this->contexts, true);
$callingCtxName = $res [0];
}
if ($callingCtxName === null) {
trigger_error (Logger::CLASS_IS_NOT_ASIGNED, E_USER_ERROR);
}
$this->currCallingCls = $callingClsName;
$this->currCallingCtx = $callingCtxName;
clearstatcache();
if (!is_dir($this->logsPath)) {
$result = mkdir ($this->logsPath);
if ($result === false) {
trigger_error(Logger::LOGS_DIR_ROOT_NOT_WRITABLE, E_USER_ERROR);
}
chmod ($this->logsPath, 0777);
}
$folderPath = $this->logsPath . "/" . $this->currCallingCtx;
if (!is_dir($folderPath)) {
$result = mkdir ($folderPath);
if ($result === false) {
trigger_error(Logger::LOGS_DIR_NOT_WRITABLE, E_USER_ERROR);
}
chmod ($folderPath, 0777);
}
$this->currLogFilePath = $folderPath . "/" . $this->logFileName;
$this->doPrint ($data);
}
private function printBanner ($bannerStr) {
$fpointer = fopen ($this->currLogFilePath, "r+b");
$result = fwrite($fpointer, $bannerStr);
fclose($fpointer);
if ($result === false) {
trigger_error(Logger::LOGS_DIR_NOT_WRITABLE, E_USER_ERROR);
}
}
private function prepareBanner ($isArchive = false) {
$bannerStr = $this->fillBannerTemplate ($isArchive);
$bannerStr = $this->sanitizeString($bannerStr);
$lines = explode ("\n", $bannerStr);
if (count ($lines) > $this->bannerLinesNo) {
array_splice($lines, $this->bannerLinesNo);
$lines [$this->bannerLinesNo - 1] .= "...";
}
$maxLen = ($this->wrapLimit !== -1)? $this->wrapLimit : $this->maxWrapLimit;
for ($i=0; $i<count($lines); $i++) {
$line = $lines[$i];
$line = trim($line);
if (strlen ($line) > $maxLen) {
$line = substr($line, 0, $maxLen);
$line = trim ($line);
if (strlen ($line) > $maxLen -3) {
$line = substr($line, 0, $maxLen-3);
}
$line .= "...";
}
if (strlen ($line) < $maxLen) {
$line = str_pad($line, $maxLen, " ", STR_PAD_RIGHT);
}
$lines [$i] = $line;
}
$bannerStr = implode("\n", $lines);
return $bannerStr;
}
private function fillBannerTemplate ($isArchive = false) {
$template = $this->bannerTemplate;
$placeHolders = array ($this->filePathPH, $this->fileSizePH, $this->fileStatusPH,
$this->lastModifiedDate, $this->relatedContextName);
foreach ($placeHolders as $placeHolder) {
$val = "";
switch ($placeHolder) {
case $this->filePathPH:
$val = $this->currLogFilePath;
break;
case $this->fileSizePH:
clearstatcache();
$val = filesize($this->currLogFilePath);
break;
case $this->fileStatusPH:
$val = $this->fullStatus;
if ($isArchive === true) {
$volNo = $this->getLastVolOnSite($this->currLogFilePath) + 1;
$val = str_replace("#", $volNo, $this->volumeStatus);
} else {
$bannerSize = ($this->wrapLimit !== -1)?
$this->bannerLinesNo * $this->wrapLimit :
$this->bannerLinesNo * $this->maxWrapLimit;
$fpointer = fopen ($this->currLogFilePath, "rb");
$test = fread($fpointer, $bannerSize + $bannerSize / $this->bannerLinesNo);
fclose ($fpointer);
if (strpos($test, chr(8)) !== false) {
$val = $this->trimmedStatus;
}
}
break;
case $this->lastModifiedDate:
$val = date ("Y/m/d H:i:s", filemtime($this->currLogFilePath));
break;
case $this->relatedContextName:
$val = $this->currCallingCtx;
break;
}
$template = str_replace('%' . $placeHolder . '%', $val, $template);
}
return $template;
}
private function printEntry ($data) {
$isFirstWriteToFile = !file_exists($this->currLogFilePath);
$fpointer = fopen ($this->currLogFilePath, "ab");
if ($isFirstWriteToFile) {
chmod ($this->currLogFilePath, 0777);
$padLnLength = ($this->wrapLimit !== -1)? $this->wrapLimit : $this->maxWrapLimit;
for ($i=0; $i<$this->bannerLinesNo; $i++) {
$data = str_repeat(" ", $padLnLength) . "\n" . $data;
}
}
$result = fwrite ($fpointer, $data);
fclose($fpointer);
if ($result === false) {
trigger_error(Logger::LOGS_DIR_NOT_WRITABLE, E_USER_ERROR);
}
}
private function prepareEntry ($data) {
$data = $this->strignify ($data);
$data = $this->sanitizeString($data);
$data = $this->fillEntryTemplate ($data);
if ($this->wrapLimit !== -1) {
$data = wordwrap($data, $this->wrapLimit, "\n", true);
}
$data = $this->fitIntoSize ($data, $this->maxFileSize);
return $data;
}
private function doPrint ($data) {
$data = $this->prepareEntry ($data);
$this->provideRoomFor ($data);
$this->printEntry ($data);
$bannerStr = $this->prepareBanner ();
$this->printBanner ($bannerStr);
}
private function provideRoomFor ($data) {
if (file_exists($this->currLogFilePath)) {
clearstatcache();
$currFileSize = filesize($this->currLogFilePath);
$availSize = $this->maxFileSize - $currFileSize;
$requiredSize = mb_strlen($data, "8bit");
if ($requiredSize > $availSize) {
$this->handleOversize($requiredSize);
}
}
}
private function handleOversize ($oversizeAmt) {
if ($this->operatingMode === $this->trimToken) {
$this->trimBy ($oversizeAmt);
} else {
$this->makeNewVolume();
}
}
private function trimBy ($trimAmt) {
$trimmedBytes = 0;
$deletionsNo = 0;
$separator = chr (1);
$trimMark = chr (8);
$fcontent = file_get_contents($this->currLogFilePath);
$entries = explode($separator, $fcontent);
$fcontent = null;
for ($i=0; $i<count($entries); $i++) {
$entry = $entries[$i];
if ($i == 0) {
continue;
}
$trimmedBytes += mb_strlen($entry, "8bit");
$deletionsNo++;
if ($trimmedBytes >= $trimAmt) {
break;
}
}
array_splice($entries, 1, $deletionsNo);
$entries[1] = $trimMark . $entries[1];
$newContent = implode ($separator, $entries);
$fpointer = fopen ($this->currLogFilePath, "wb");
fwrite($fpointer, $newContent);
fclose($fpointer);
}
private function makeNewVolume () {
$bannerStr = $this->prepareBanner(true);
$this->printBanner($bannerStr);
$lastVolOnSite = $this->getLastVolOnSite ($this->currLogFilePath);
rename($this->currLogFilePath, $this->currLogFilePath . "." . ++$lastVolOnSite);
$fpointer = fopen ($this->currLogFilePath, "wb");
fclose ($fpointer);
$bannerStr = $this->prepareBanner ();
$this->printBanner ($bannerStr);
}
private function getLastVolOnSite ($path) {
$iterator = 0;
do {
$testPath = $path . "." . ($iterator + 1);
if (!file_exists ($testPath)) {
break;
}
$iterator++;
} while (true);
return $iterator;
}
private function fitIntoSize ($str, $size) {
$strSize = mb_strlen ($str, "8bit");
$bannerSize = ($this->wrapLimit !== -1)? $this->bannerLinesNo * $this->wrapLimit :
$this->bannerLinesNo * $this->maxWrapLimit;
$availSize = $size - $bannerSize;
if ($strSize > $availSize) {
$str = substr($str, 0, $availSize);
$str = trim ($str);
$str .= "...";
}
return $str;
}
private function fillEntryTemplate ($paramValue) {
$template = $this->entryTemplate;
$placeHolders = array ($this->yPH, $this->mPH, $this->dPH, $this->hPH, $this->minPH,
$this->sPH, $this->callingClassNamePH, $this->contentPH);
foreach ($placeHolders as $placeHolder) {
$val = "";
switch ($placeHolder) {
case $this->yPH:
$val = date("y");
break;
case $this->mPH:
$val = date("m");
break;
case $this->dPH:
$val = date("d");
break;
case $this->hPH:
$val = date("H");
break;
case $this->minPH:
$val = date("i");
break;
case $this->sPH:
$val = date("s");
break;
case $this->callingClassNamePH:
$val = $this->currCallingCls;
break;
case $this->contentPH:
$val = $paramValue;
break;
}
$template = str_replace('%' . $placeHolder . '%', $val, $template);
}
$template .= "\n";
return $template;
}
private function strignify ($arg) {
// Log strings literally:
if (is_string ($arg)) {
if (strlen ($arg) == 0) {
return "<empty string>\n";
}
return "$arg\n";
}
// Log resources by their type and id:
if (is_resource ($arg)) {
return ucfirst (get_resource_type ($arg)) . " resource ($arg)\n";
}
// Use var_dump for anything else:
ob_start();
var_dump ($arg);
$val = ob_get_contents();
ob_end_clean();
return $val;
}
private function getLogsPath () {
$segments = explode ("/", $this->configFolderPath);
array_pop($segments);
$path = implode("/", $segments) . "/" . $this->logsDirName;
$path = preg_replace('/\x5c{1,}/', '/', $path);
$path = preg_replace('/\x2f$/', '', $path);
return $path;
}
private function sanitizeString ($value) {
if ($value !== "") {
$value = preg_replace('/\x00/', "", $value); // remove NULL chars, if any
$value = preg_replace('/\x08/', "", $value); // remove BACKSPACE chars, if any
$value = preg_replace('/\x09/', str_repeat(" ", $this->tabSize), $value); // convert TABs
$value = preg_replace('/\x0d\x0a/', "\n", $value); // convert Windows line ends
$value = preg_replace('/\x0d/', "\n", $value); // convert Mac line ends
$lines = explode("\n", $value);
for ($i=0; $i<count($lines); $i++) {
$lines[$i] = trim($lines[$i]);
}
$value = implode ("\n", $lines);
}
return $value;
}
private function array_searchRecursive ($needle, $haystack, $strict=false, $path=array()) {
if (!is_array($haystack)) {
return false;
}
foreach ($haystack as $key => $val) {
if (is_array ($val) && $subPath = $this->array_searchRecursive ($needle, $val, $strict,
$path)) {
$path = array_merge ($path, array($key), $subPath);
return $path;
} elseif ((!$strict && $val == $needle) || ($strict && $val === $needle)) {
$path[] = $key;
return $path;
}
}
return false;
}
}
?>