diff --git a/wire/core/FileLog.php b/wire/core/FileLog.php
index 703d18cd..4942c7bb 100644
--- a/wire/core/FileLog.php
+++ b/wire/core/FileLog.php
@@ -11,15 +11,73 @@
*/
class FileLog extends Wire {
-
- const defaultChunkSize = 12288;
- const debug = false;
- protected $logFilename = false;
- protected $itemsLogged = array();
+ /**
+ * Default size of chunks used for reading from logs
+ *
+ */
+ const defaultChunkSize = 12288;
+
+ /**
+ * Debug mode used during development of this class
+ *
+ */
+ const debug = false;
+
+ /**
+ * Chunk size used when reading from logs and not overridden
+ *
+ * @var int
+ *
+ */
+ protected $chunkSize = self::defaultChunkSize;
+
+ /**
+ * Full path to log file or false when not yet set
+ *
+ * @var bool|string
+ *
+ */
+ protected $logFilename = false;
+
+ /**
+ * Log items saved during this request where array keys are md5 hash of log entries and values ignored
+ *
+ * @var array
+ *
+ */
+ protected $itemsLogged = array();
+
+ /**
+ * Delimiter used in log entries
+ *
+ * @var string
+ *
+ */
protected $delimeter = "\t";
+
+ /**
+ * Maximum allowed line length for a single log line
+ *
+ * @var int
+ *
+ */
protected $maxLineLength = 8192;
+
+ /**
+ * File extension used for log files
+ *
+ * @var string
+ *
+ */
protected $fileExtension = 'txt';
+
+ /**
+ * Path where log files are stored
+ *
+ * @var string
+ *
+ */
protected $path = '';
/**
@@ -58,52 +116,157 @@ class FileLog extends Wire {
protected function cleanStr($str) {
$str = str_replace(array("\r\n", "\r", "\n"), ' ', trim($str));
if(strlen($str) > $this->maxLineLength) $str = substr($str, 0, $this->maxLineLength);
+ if(strpos($str, ' ^+') !== false) $str = str_replace(' ^=', ' ^ +', $str); // disallowed sequence
return $str;
}
/**
* Save the given log entry string
*
- * @param $str
- * @return bool Success state
+ * @param string $str
+ * @param array $options options to modify behavior (Added 3.0.143)
+ * - `allowDups` (bool): Allow duplicating same log entry in same runtime/request? (default=true)
+ * - `mergeDups` (int): Merge previous duplicate entries that also appear near end of file? Specify int for bytes from EOF (default=1024)
+ * - `maxTries` (int): If log entry fails to save, maximum times to re-try (default=20)
+ * - `maxTriesDelay` (int): Micro seconds (millionths of a second) to delay between re-tries (default=2000)
+ * @return bool Success state: true if log written, false if not.
*
*/
- public function save($str) {
-
- if(!$this->logFilename) return false;
+ public function save($str, array $options = array()) {
+
+ $defaults = array(
+ 'mergeDups' => 1024,
+ 'allowDups' => true,
+ 'maxTries' => 20,
+ 'maxTriesDelay' => 2000,
+ );
+ if(!$this->logFilename) return false;
+
+ $options = array_merge($defaults, $options);
$hash = md5($str);
-
- // if we've already logged this during this instance, then don't do it again
- if(in_array($hash, $this->itemsLogged)) return true;
-
$ts = date("Y-m-d H:i:s");
$str = $this->cleanStr($str);
- $fp = fopen($this->logFilename, "a");
+ $line = $this->delimeter . $str; // log entry, excluding timestamp
+ $hasLock = false; // becomes true when lock obtained
+ $fp = false; // becomes resource when file is open
- if($fp) {
- $trys = 0;
- $stop = false;
+ // if we've already logged this during this instance, then don't do it again
+ if(!$options['allowDups'] && isset($this->itemsLogged[$hash])) return true;
- while(!$stop) {
- if(flock($fp, LOCK_EX)) {
- fwrite($fp, "$ts{$this->delimeter}$str\n");
- flock($fp, LOCK_UN);
- $this->itemsLogged[] = $hash;
- $stop = true;
- } else {
- usleep(2000);
- if($trys++ > 20) $stop = true;
- }
- }
+ // determine write mode
+ $mode = file_exists($this->logFilename) ? 'a' : 'w';
+ if($mode === 'a' && $options['mergeDups']) $mode = 'r+';
- fclose($fp);
- $this->wire('files')->chmod($this->logFilename);
- return true;
- } else {
+ // open the log file
+ for($tries = 0; $tries <= $options['maxTries']; $tries++) {
+ $fp = fopen($this->logFilename, $mode);
+ if($fp) break;
+ // if unable to open for reading/writing, see if we can open for append instead
+ if($mode === 'r+' && $tries > ($options['maxTries'] / 2)) $mode = 'a';
+ usleep($options['maxTriesDelay']);
+ }
+
+ // if unable to open, exit now
+ if(!$fp) return false;
+
+ // obtain a lock
+ for($tries = 0; $tries <= $options['maxTries']; $tries++) {
+ $hasLock = flock($fp, LOCK_EX);
+ if($hasLock) break;
+ usleep($options['maxTriesDelay']);
+ }
+
+ // if unable to obtain a lock, we cannot write to the log
+ if(!$hasLock) {
+ fclose($fp);
return false;
}
+ // if opened for reading and writing, merge duplicates of $line
+ if($mode === 'r+' && $options['mergeDups']) {
+ // do not repeat the same log entry in the same chunk
+ $chunkSize = is_int($options['mergeDups']) ? $options['mergeDups'] : $this->chunkSize;
+ fseek($fp, -1 * $chunkSize, SEEK_END);
+ $chunk = fread($fp, $chunkSize);
+ // check if our log line already appears in the immediate earlier chunk
+ if(strpos($chunk, $line) !== false) {
+ // this log entry already appears 1+ times within the last chunk of the file
+ // remove the duplicates and replace the chunk
+ $chunkLength = strlen($chunk);
+ $this->removeLineFromChunk($line, $chunk, $chunkSize);
+ fseek($fp, 0, SEEK_END);
+ $oldLength = ftell($fp);
+ $newLength = $chunkLength > $oldLength ? $oldLength - $chunkLength : 0;
+ ftruncate($fp, $newLength);
+ fseek($fp, 0, SEEK_END);
+ fwrite($fp, $chunk);
+ }
+ } else {
+ // already at EOF because we are appending or creating
+ }
+
+ // add the log line
+ $result = fwrite($fp, "$ts$line\n");
+
+ // release the lock and close the file
+ flock($fp, LOCK_UN);
+ fclose($fp);
+
+ if($result && !$options['allowDups']) $this->itemsLogged[$hash] = true;
+
+ // if we were creating the file, make sure it has the right permission
+ if($mode === 'w') {
+ $files = $this->wire('files'); /** @var WireFileTools $files */
+ $files->chmod($this->logFilename);
+ }
+
+ return (int) $result > 0;
+ }
+
+ /**
+ * Remove given $line from $chunk and add counter to end of $line indicating quantity that was removed
+ *
+ * @param string $line
+ * @param string $chunk
+ * @param int $chunkSize
+ * @since 3.0.143
+ *
+ */
+ protected function removeLineFromChunk(&$line, &$chunk, $chunkSize) {
+
+ $qty = 0;
+ $chunkLines = explode("\n", $chunk);
+
+ foreach($chunkLines as $key => $chunkLine) {
+
+ $x = 1;
+ if($key === 0 && strlen($chunk) >= $chunkSize) continue; // skip first line since it’s likely a partial line
+
+ // check if line appears in this chunk line
+ if(strpos($chunkLine, $line) === false) continue;
+
+ // check if line also indicates a previous quantity that we should add to our quantity
+ if(strpos($chunkLine, ' ^+') !== false) {
+ list($chunkLine, $n) = explode(' ^+', $chunkLine, 2);
+ if(ctype_digit($n)) $x += (int) $n;
+ }
+
+ // verify that these are the same line
+ if(strpos(trim($chunkLine) . "\n", trim($line) . "\n") === false) continue;
+
+ // remove the line
+ unset($chunkLines[$key]);
+
+ // update the quantity
+ $qty += $x;
+ }
+
+ if($qty) {
+ // append quantity to line, i.e. “^+2” indicating 2 more indentical lines were above
+ $chunk = implode("\n", array_values($chunkLines));
+ $line .= " ^+$qty";
+ }
}
public function size() {
@@ -145,7 +308,7 @@ class FileLog extends Wire {
*
*/
protected function getChunkArray($chunkNum = 1, $chunkSize = 0, $reverse = true) {
- if($chunkSize < 1) $chunkSize = self::defaultChunkSize;
+ if($chunkSize < 1) $chunkSize = $this->chunkSize;
$lines = explode("\n", $this->getChunk($chunkNum, $chunkSize, $reverse));
foreach($lines as $key => $line) {
$line = trim($line);
@@ -165,15 +328,16 @@ class FileLog extends Wire {
* Returned string is automatically adjusted at the beginning and
* ending to contain only full log lines.
*
- * @param int $chunkNum Current pagination number (default=1)
- * @param int $chunkSize Number of bytes to retrieve (default=12288)
+ * @param int $chunkNum Current chunk/pagination number (default=1, first)
+ * @param int $chunkSize Number of bytes to retrieve (default=0, which assigns default chunk size of 12288)
* @param bool $reverse True=pull from end of file, false=pull from beginning (default=true)
+ * @param bool $clean Get a clean chunk that starts at the beginning of a line? (default=true)
* @return string
*
*/
- protected function getChunk($chunkNum = 1, $chunkSize = 0, $reverse = true) {
+ protected function getChunk($chunkNum = 1, $chunkSize = 0, $reverse = true, $clean = true) {
- if($chunkSize < 1) $chunkSize = self::defaultChunkSize;
+ if($chunkSize < 1) $chunkSize = $this->chunkSize;
if($reverse) {
$offset = -1 * ($chunkSize * $chunkNum);
@@ -181,7 +345,9 @@ class FileLog extends Wire {
$offset = $chunkSize * ($chunkNum-1);
}
- if(self::debug) $this->message("chunkNum=$chunkNum, chunkSize=$chunkSize, offset=$offset, filesize=" . filesize($this->logFilename));
+ if(self::debug) {
+ $this->message("chunkNum=$chunkNum, chunkSize=$chunkSize, offset=$offset, filesize=" . filesize($this->logFilename));
+ }
$data = '';
$totalChunks = $this->getTotalChunks($chunkSize);
@@ -190,23 +356,27 @@ class FileLog extends Wire {
if(!$fp = fopen($this->logFilename, "r")) return $data;
fseek($fp, $offset, ($reverse ? SEEK_END : SEEK_SET));
-
- // make chunk include up to beginning of first line
- fseek($fp, -1, SEEK_CUR);
- while(ftell($fp) > 0) {
- $chr = fread($fp, 1);
- if($chr == "\n") break;
- fseek($fp, -2, SEEK_CUR);
- $data = $chr . $data;
+
+ if($clean) {
+ // make chunk include up to beginning of first line
+ fseek($fp, -1, SEEK_CUR);
+ while(ftell($fp) > 0) {
+ $chr = fread($fp, 1);
+ if($chr == "\n") break;
+ fseek($fp, -2, SEEK_CUR);
+ $data = $chr . $data;
+ }
+ fseek($fp, $offset, ($reverse ? SEEK_END : SEEK_SET));
}
// get the big part of the chunk
- fseek($fp, $offset, ($reverse ? SEEK_END : SEEK_SET));
$data .= fread($fp, $chunkSize);
-
- // remove last partial line
- $pos = strrpos($data, "\n");
- if($pos) $data = substr($data, 0, $pos);
+
+ if($clean) {
+ // remove last partial line
+ $pos = strrpos($data, "\n");
+ if($pos) $data = substr($data, 0, $pos);
+ }
fclose($fp);
@@ -221,9 +391,9 @@ class FileLog extends Wire {
*
*/
protected function getTotalChunks($chunkSize = 0) {
- if($chunkSize < 1) $chunkSize = self::defaultChunkSize;
+ if($chunkSize < 1) $chunkSize = $this->chunkSize;
$filesize = filesize($this->logFilename);
- return ceil($filesize / $chunkSize);
+ return $filesize > 0 ? ceil($filesize / $chunkSize) : 0;
}
/**
@@ -234,7 +404,7 @@ class FileLog extends Wire {
*/
public function getTotalLines() {
- if(filesize($this->logFilename) < self::defaultChunkSize) {
+ if(filesize($this->logFilename) < $this->chunkSize) {
$data = file($this->logFilename);
return count($data);
}
@@ -243,7 +413,7 @@ class FileLog extends Wire {
$totalLines = 0;
while(!feof($fp)) {
- $data = fread($fp, self::defaultChunkSize);
+ $data = fread($fp, $this->chunkSize);
$totalLines += substr_count($data, "\n");
}
@@ -323,7 +493,7 @@ class FileLog extends Wire {
$cnt = 0; // number that will be written or returned by this
$n = 0; // number total
$chunkNum = 0;
- $totalChunks = $this->getTotalChunks(self::defaultChunkSize);
+ $totalChunks = $this->getTotalChunks($this->chunkSize);
$stopNow = false;
$chunkLineHashes = array();
@@ -517,6 +687,19 @@ class FileLog extends Wire {
public function setFileExtension($ext) {
$this->fileExtension = $ext;
}
+
+ /**
+ * Get or set the default chunk size used when reading from logs and not overridden by method argument
+ *
+ * @param int $chunkSize Specify chunk size to set, or omit to get
+ * @return int
+ * @since 3.0.143
+ *
+ */
+ public function chunkSize($chunkSize = 0) {
+ if($chunkSize > 0) $this->chunkSize = (int) $chunkSize;
+ return $this->chunkSize;
+ }
}
diff --git a/wire/core/WireLog.php b/wire/core/WireLog.php
index ba18c201..0da1df07 100644
--- a/wire/core/WireLog.php
+++ b/wire/core/WireLog.php
@@ -8,12 +8,11 @@
*
* #pw-summary Enables creation of logs, logging of events, and management of logs.
*
- * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
+ * ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* https://processwire.com
*
* @method bool save($name, $text, $options = array())
*
- * @todo option to avoid saving same log entry text back-to-back
* @todo option to disable logs by name
*
*/
@@ -22,6 +21,14 @@ class WireLog extends Wire {
protected $logExtension = 'txt';
+ /**
+ * FileLog instances indexed by filename
+ *
+ * @var array
+ *
+ */
+ protected $fileLogs = array();
+
/**
* Record an informational or 'success' message in the message log (messages.txt)
*
@@ -192,28 +199,42 @@ class WireLog extends Wire {
*
* #pw-group-retrieval
*
+ * @param bool $sortNewest Sort by newest to oldest rather than by name? (default=false) Added 3.0.143
* @return array
*
*/
- public function getLogs() {
+ public function getLogs($sortNewest = false) {
$logs = array();
$dir = new \DirectoryIterator($this->wire('config')->paths->logs);
foreach($dir as $file) {
if($file->isDot() || $file->isDir()) continue;
- if($file->getExtension() != 'txt') continue;
- $name = basename($file, '.txt');
+ if($file->getExtension() != $this->logExtension) continue;
+ $name = basename($file, '.' . $this->logExtension);
if($name != $this->wire('sanitizer')->pageName($name)) continue;
- $logs[$name] = array(
+
+ if($sortNewest) {
+ $sortKey = $file->getMTime();
+ while(isset($logs[$sortKey])) $sortKey++;
+ } else {
+ $sortKey = $name;
+ }
+
+ $logs[$sortKey] = array(
'name' => $name,
'file' => $file->getPathname(),
'size' => $file->getSize(),
'modified' => $file->getMTime(),
);
}
+
+ if($sortNewest) {
+ krsort($logs);
+ } else {
+ ksort($logs);
+ }
- ksort($logs);
return $logs;
}
@@ -448,9 +469,13 @@ class WireLog extends Wire {
*
*/
public function getFileLog($name, array $options = array()) {
- $log = $this->wire(new FileLog($this->getFilename($name)));
- if(isset($options['delimiter'])) $log->setDelimeter($options['delimiter']);
- else $log->setDelimeter("\t");
+ $delimiter = isset($options['delimiter']) ? $options['delimiter'] : "\t";
+ $filename = $this->getFilename($name);
+ $key = "$filename$delimiter";
+ if(isset($this->fileLogs[$key])) return $this->fileLogs[$key];
+ $log = $this->wire(new FileLog($filename));
+ $log->setDelimiter($delimiter);
+ $this->fileLogs[$key] = $log;
return $log;
}
diff --git a/wire/modules/Process/ProcessLogger/ProcessLogger.module b/wire/modules/Process/ProcessLogger/ProcessLogger.module
index 1324c1e6..a3dafdb3 100644
--- a/wire/modules/Process/ProcessLogger/ProcessLogger.module
+++ b/wire/modules/Process/ProcessLogger/ProcessLogger.module
@@ -3,7 +3,7 @@
/**
* ProcessWire Logger (Logs Viewer)
*
- * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
+ * ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* https://processwire.com
*
*
@@ -15,7 +15,7 @@ class ProcessLogger extends Process {
return array(
'title' => __('Logs', __FILE__),
'summary' => __('View and manage system logs.', __FILE__),
- 'version' => 1,
+ 'version' => 2,
'author' => 'Ryan Cramer',
'icon' => 'tree',
'permission' => 'logs-view',
@@ -50,12 +50,16 @@ class ProcessLogger extends Process {
$options['itemLabel2'] = 'when';
$options['add'] = false;
$options['edit'] = 'view/{name}/';
- $options['items'] = $this->wire('log')->getLogs();
+ $options['items'] = $this->wire('log')->getLogs(true);
+ $options['sort'] = false;
foreach($options['items'] as $key => $item) {
$item['when'] = wireRelativeTimeStr($item['modified'], true, false);
- if(time() - $item['modified'] > 86400) $item['icon'] = 'file-text-o';
- else $item['icon'] = 'file-text';
+ if(time() - $item['modified'] > 86400) {
+ $item['icon'] = 'file-text-o';
+ } else {
+ $item['icon'] = 'file-text';
+ }
$options['items'][$key] = $item;
}
@@ -63,6 +67,7 @@ class ProcessLogger extends Process {
}
public function ___execute() {
+ /** @var MarkupAdminDataTable $table */
$table = $this->wire('modules')->get('MarkupAdminDataTable');
$table->setEncodeEntities(false);
$table->headerRow(array(
@@ -178,22 +183,27 @@ class ProcessLogger extends Process {
} while(1);
if($this->wire('config')->ajax) return $this->renderLogAjax($items, $name);
-
+
+ /** @var InputfieldForm $form */
$form = $this->wire('modules')->get('InputfieldForm');
+
+ /** @var InputfieldFieldset $fieldset */
$fieldset = $this->wire('modules')->get('InputfieldFieldset');
$fieldset->attr('id', 'FieldsetTools');
$fieldset->label = $this->_('Helpers');
$fieldset->collapsed = Inputfield::collapsedYes;
$fieldset->icon = 'sun-o';
$form->add($fieldset);
-
+
+ /** @var InputfieldText $f */
$f = $this->wire('modules')->get('InputfieldText');
$f->attr('name', 'q');
$f->label = $this->_('Text Search');
$f->icon = 'search';
$f->columnWidth = 50;
- $fieldset->add($f);
-
+ $fieldset->add($f);
+
+ /** @var InputfieldDatetime $f */
$f = $this->wire('modules')->get('InputfieldDatetime');
$f->attr('name', 'date_from');
$f->label = $this->_('Date From');
@@ -202,7 +212,8 @@ class ProcessLogger extends Process {
$f->datepicker = InputfieldDatetime::datepickerFocus;
$f->attr('placeholder', 'yyyy-mm-dd');
$fieldset->add($f);
-
+
+ /** @var InputfieldDatetime $f */
$f = $this->wire('modules')->get('InputfieldDatetime');
$f->attr('name', 'date_to');
$f->icon = 'calendar';
@@ -211,7 +222,8 @@ class ProcessLogger extends Process {
$f->attr('placeholder', 'yyyy-mm-dd');
$f->datepicker = InputfieldDatetime::datepickerFocus;
$fieldset->add($f);
-
+
+ /** @var InputfieldSelect $f */
$f = $this->modules->get('InputfieldSelect');
$f->attr('name', 'action');
$f->label = $this->_('Actions');
@@ -226,7 +238,8 @@ class ProcessLogger extends Process {
$f->addOption('add', $this->_('Grow (Add Entry)'));
$f->addOption('prune', $this->_('Chop (Prune)'));
$f->addOption('delete', $this->_('Burn (Delete)'));
-
+
+ /** @var InputfieldInteger $f */
$f = $this->wire('modules')->get('InputfieldInteger');
$f->attr('name', 'prune_days');
$f->label = $this->_('Chop To # Days');
@@ -236,6 +249,7 @@ class ProcessLogger extends Process {
$f->showIf = "action=prune";
$fieldset->add($f);
+ /** @var InputfieldText $f */
$f = $this->wire('modules')->get('InputfieldText');
$f->attr('name', 'add_text');
$f->label = $this->_('New Log Entry');
@@ -243,6 +257,7 @@ class ProcessLogger extends Process {
$f->showIf = "action=add";
$fieldset->add($f);
+ /** @var InputfieldSubmit $f */
$f = $this->wire('modules')->get('InputfieldSubmit');
$f->value = $this->_('Chop this log file now');
$f->icon = 'cut';
@@ -320,9 +335,11 @@ class ProcessLogger extends Process {
}
protected function renderLog(array $items, $name, $time = 0) {
-
+
+ /** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
-
+
+ /** @var MarkupAdminDataTable $table */
$table = $this->wire('modules')->get('MarkupAdminDataTable');
$table->setSortable(false);
$table->setEncodeEntities(false);
@@ -354,6 +371,10 @@ class ProcessLogger extends Process {
$date = " $date";
}
+ if(strpos($entry['text'], '&') !== false) {
+ $entry['text'] = $this->wire('sanitizer')->unentities($entry['text']);
+ }
+
foreach($entry as $key => $value) {
$entry[$key] = $sanitizer->entities($value);
}
@@ -362,18 +383,19 @@ class ProcessLogger extends Process {
if(count($templateItem) >= 4) {
- $url = preg_replace('{^https?://[^/]+}', '', $entry['url']);
+ $row[] = $entry['user'];
+
+ $entry['url'] = preg_replace('{^https?://[^/]+}', '', $entry['url']);
+ $url = $entry['url'];
if($url == '/?/') {
$url = 2; // array key
$entry['url'] = '?';
}
-
- if(strlen($url) > 50) $url = substr($url, 0, 50) . '…';
- $row[] = $entry['user'];
- $row[$url] = $entry['url'];
+ $urlLabel = $this->formatLogUrlLabel($entry['url']);
+ $row[$urlLabel] = $url;
}
-
- $row[] = $entry['text'];
+
+ $row[] = $this->formatLogText($entry['text'], $name);
$table->row($row);
}
@@ -384,10 +406,12 @@ class ProcessLogger extends Process {
reset($items);
$key = key($items);
list($n, $total, $start, $end, $limit) = explode('/', $key);
+ if($n && $end) {} // ignore
$entries->import($items);
$entries->setLimit($limit);
$entries->setStart($start);
$entries->setTotal($total);
+ /** @var MarkupPagerNav $pager */
$pager = $this->wire('modules')->get('MarkupPagerNav');
$options = array('baseUrl' => "../$name/");
$pagerOut = $pager->render($entries, $options);
@@ -420,5 +444,110 @@ class ProcessLogger extends Process {
}
+ /**
+ * Format log URL label
+ *
+ * @param string $url
+ * @return string
+ *
+ */
+ protected function formatLogUrlLabel($url) {
+
+ if($url === '?') return $url;
+
+ if(strpos($url, '://') !== false) {
+ $url = preg_replace('{^https?://[^/]+}', '', $url);
+ }
+
+ $config = $this->wire('config');
+ $rootUrl = $config->urls->root;
+ $adminUrl = $config->urls->admin;
+ $isAdmin = false;
+
+ if(strpos($url, $adminUrl) === 0) {
+ $isAdmin = true;
+ $url = substr($url, strlen($adminUrl));
+ } else if($rootUrl !== '/' && strpos($url, $rootUrl) === 0) {
+ $url = substr($url, strlen($rootUrl)-1);
+ }
+
+ if($isAdmin && strpos($url, 'page/edit/') !== false && preg_match('/[?&]id=(\d+)/', $url, $matches)) {
+ $url = 'page/edit/?id=' . $matches[1];
+ } else if($url === '/http404/') {
+ $url = $this->_('404 not found');
+ }
+
+ if(strlen($url) > 50) {
+ $url = substr($url, 0, 50) . '…';
+ }
+
+ return $url;
+ }
+
+ /**
+ * Format log line txt
+ *
+ * @param string $text
+ * @param string $logName
+ * @return string
+ *
+ */
+ protected function formatLogText($text, $logName = '') {
+
+ $config = $this->wire('config');
+
+ // shorten paths
+ foreach(array('site', 'wire') as $name) {
+ if(strpos($text, "/$name/") === false) continue;
+ $path = $config->paths($name);
+ if(strpos($text, $path) !== false) {
+ $text = str_replace($path, "/$name/", $text);
+ } else {
+ // $text = preg_replace('![-_/\\:a-zA-Z0-9]+/' . $name . '/!', "/$name/", $text);
+ }
+ }
+
+ // shorten assumed namespaces
+ if(strpos($text, 'ProcessWire\\') !== false) {
+ $text = str_replace('ProcessWire\\', '', $text);
+ }
+
+ // formatting of stack traces in errors/exceptions logs
+ if($logName === 'errors' || $logName === 'exceptions') {
+ if(strpos($text, '(line ') && preg_match('/\((line \d+ of [^)]+)\)/', $text, $matches)) {
+ $text = str_replace($matches[0], "
" . ucfirst($matches[1]) . "", $text);
+ } else if(strpos($text, '(in ') && preg_match('!\((in /[^)]+? line \d+)\)!', $text, $matches)) {
+ $text = str_replace($matches[0], "
" . ucfirst($matches[1]) . "", $text);
+ }
+ if(strpos($text, ' #0 /')) {
+ list($text, $traces) = explode(' #0 /', $text, 2);
+ $traces = preg_split('! #\d+ /!', $traces);
+ $text .= "";
+ foreach($traces as $key => $trace) {
+ $n = $key + 1;
+ $text .= "
$n. /$trace";
+ }
+ $text .= "";
+ }
+ }
+
+ // identify recurring instances
+ if(strpos($text, ' ^+')) {
+ $_text = $text;
+ list($text, $qty) = explode(' ^+', $text, 2);
+ if(ctype_digit($qty)) {
+ $text .= "
" .
+ "" .
+ sprintf($this->_n('Plus %d ealier duplicate ', 'Plus %d earlier duplicates', $qty), $qty) .
+ "";
+ } else {
+ // oops, restore
+ $text = $_text;
+ }
+ }
+
+ return $text;
+ }
+
}