diff --git a/.gitignore b/.gitignore
index 96fd026..d32ce77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-
vendor/
-
composer.lock
+.phpunit.result.cache
+*.code-workspace
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 5e4ae31..3a9122e 100644
--- a/composer.json
+++ b/composer.json
@@ -27,5 +27,22 @@
"issues": "https://github.com/schuhwerk/php-error-log-viewer/issues",
"source": "https://github.com/schuhwerk/php-error-log-viewer"
},
- "type": "development-tool"
-}
+ "type": "development-tool",
+ "config": {
+ "sort-packages": true,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9",
+ "squizlabs/php_codesniffer": "^3",
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7"
+ },
+ "scripts": {
+ "test": "phpunit",
+ "phpcs": [
+ "vendor/bin/phpcs --standard=phpcs.xml --extensions=php"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..8542f7c
--- /dev/null
+++ b/index.php
@@ -0,0 +1,28 @@
+handle_ajax_requests();
+
+readfile('./src/error-log-viewer-frontend.html');
diff --git a/php-error-log-screenshot.png b/php-error-log-screenshot.png
new file mode 100644
index 0000000..4cb4e3c
Binary files /dev/null and b/php-error-log-screenshot.png differ
diff --git a/php-error-log-viewer.php b/php-error-log-viewer.php
deleted file mode 100644
index 8b2da70..0000000
--- a/php-error-log-viewer.php
+++ /dev/null
@@ -1,456 +0,0 @@
-handle_ajax_requests();
-
-/**
- * Read, process, delete log files. Output as json.
- */
-class Pelv_Log_Handler {
-
- /**
- * Contains grouped content of the log file.
- *
- * @var array
- */
- public $content = array();
-
- /**
- * Index used for grouping same messages.
- * Created via crc32().
- *
- * @var int[]
- */
- public $index = array();
-
- /**
- * The size of the file.
- *
- * @var int
- */
- public $filesize = 0;
-
- /**
- * The settings which are being applied.
- *
- * @var array
- */
- public $settings = array();
-
- /**
- * The default setting
- *
- * @var array
- */
- public $default_settings = array(
- 'file_path' => 'debug.log',
- 'vscode_links' => true, // Stack trace references files. link them to your repo (https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls).
- 'vscode_path_search' => '', // This is needed if you develop on a vm. like '/srv/www/...'.
- 'vscode_path_replace' => '', // The local path to your repo. like 'c:/users/...'.
- );
-
- public function __construct( $settings ) {
- $this->settings = array_merge( $this->default_settings, $settings );
- }
-
- public function handle_ajax_requests() {
- $this->ajax_handle_errors();
- if ( isset( $_GET['get_log'] ) ) {
- $this->ajax_json_log();
- }
- if ( isset( $_GET['delete_log'] ) ) {
- $this->ajax_delete();
- }
- if ( isset( $_GET['filesize'] ) ) {
- $this->ajax_filesize();
- }
- }
-
- public function ajax_handle_errors() {
- $used = array_diff( array( 'get_log', 'delete_log', 'filesize' ), array_keys( $_GET ) );
- if ( count( $used ) === 3 ) {
- return;
- }
- $log_file_valid = $this->is_file_valid();
- if ( ! $log_file_valid ) {
- $this->ajax_header();
- echo $log_file_valid;
- die();
- }
- }
-
- /**
- * Read the log-file.
- *
- * @return string|false The read string or false on failure.
- */
- public function get_file() {
- $my_file = fopen( $this->settings['file_path'], 'r' );
- $size = $this->get_size();
- return ( $my_file && $size ) ? fread( $my_file, $size ) : false;
- }
-
- /**
- * Get the size of the log-file.
- *
- * @return int|false The size of the log file in bytes or false.
- */
- public function get_size() {
- if ( empty( $this->filesize ) ) {
- $this->filesize = filesize( $this->settings['file_path'] );
- }
- return $this->filesize;
- }
-
- /**
- * Check if a file is valid.
- *
- * @return boolean|string true or error message.
- */
- public function is_file_valid() {
- if ( ! file_exists( $this->settings['file_path'] ) ) {
- return 'The file you specified does not exist (' . $this->settings['file_path'] . ')';
- }
- if ( 0 == $this->get_size() ) {
- return 'Your log file is empty.';
- }
- $mbs = $this->get_size() / 1024 / 1024; // in MB.
- if ( $mbs > 100 ) {
- if ( ! isset( $_GET['ignore'] ) ) {
- return( "Aborting. debug.log is larger than 100 MB ($mbs).
- If you want to continue anyway add the 'ignore' queryvar"
- );
- }
- }
- return true;
- }
-
- /**
- * Triggers preg_replace_callback which calls
- * replace_callback function which stores values in $this->content.
- *
- * @param string $raw The content of the log file.
- * @return void
- */
- public function parse( $raw ) {
- $error = preg_replace_callback( '~^\[([^\]]*)\]((?:[^\r\n]*[\r\n]?(?!\[).*)*)~m', array( $this, 'replace_callback' ), $raw );
- }
-
- public function link_vscode_files( $string ) {
- $string = preg_replace_callback( '$([A-Z]:)?([\\\/][^:(\s]+)(?: on line |[:\(])([0-9]+)\)?$', array( $this, 'vscode_link_filter' ), $string );
- return $string;
- }
-
- public function vscode_link_filter( $matches ) {
- $link = 'vscode://file/' . $matches[1] . $matches[2] . ':' . $matches[3];
- $root = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : $_SERVER['DOCUMENT_ROOT'];
- $val = parse_url( $root, PHP_URL_QUERY );
- parse_str( $val, $get_array );
- $link = str_replace( $this->settings['vscode_path_search'], $this->settings['vscode_path_replace'], $link );
- return "" . $matches[0] . '';
- }
-
- /**
- * Callback function which is triggered by preg_replace_callback.
- * Doesn't return but writes to $this->content.
- *
- * @param array $arr
- * looks like that:
- * array (
- * 0 => [01-Jun-2016 09:24:02 UTC] PHP Fatal error: Allowed memory size of 456 bytes exhausted (tried to allocate 27 bytes) in ...
- * 1 => [01-Jun-2016 09:24:02 UTC]
- * 2 => PHP Fatal error: Allowed memory size of 56 bytes exhausted (tried to allocate 15627 bytes) in ... *
- * )
- * @return void
- */
- public function replace_callback( $arr ) {
- $err_id = crc32( trim( $arr[2] ) ); // create a unique identifier for the error message.
- if ( ! isset( $this->content[ $err_id ] ) ) { // we have a new error.
- $this->content[ $err_id ] = array();
- $this->content[ $err_id ]['id'] = $err_id; // err_id.
- $this->content[ $err_id ]['cnt'] = 1; // counter.
- $this->index[] = $err_id;
- } else { // we already have that error...
- $this->content[ $err_id ]['cnt']++; // counter.
- }
-
- $date = date_create( $arr[1] ); // false if no valid date.
- $this->content[ $err_id ]['time'] = $date ? $date->format( DateTime::ATOM ) : $arr[1]; // ISO8601, readable in js
- $message = htmlspecialchars( trim( $arr[2] ), ENT_QUOTES );
- $this->content[ $err_id ]['msg'] = $this->settings['vscode_links'] ? $this->link_vscode_files( $message ) : $message;
- $this->content[ $err_id ]['cls'] = implode(
- ' ',
- array_slice(
- str_word_count( $this->content[ $err_id ]['msg'], 2 ),
- 1,
- 2
- )
- ); // the first few words of the message become class items.
- }
-
- public function delete() {
- if ( ! file_exists( $this->settings['file_path'] ) ) {
- return 'there was no file to delete';
- }
- if ( ! is_writeable( realpath( $this->settings['file_path'] ) ) ) {
- return 'debug.log is not writable';
- }
- $f = @fopen( $this->settings['file_path'], 'r+' );
- if ( $f !== false ) {
- ftruncate( $f, 0 );
- fclose( $f );
- return 'emptied file';
- } else {
- return 'file could not be emptied';
- }
- }
-
- public function ajax_header() {
- header( 'Content-Type: application/json' );
- header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
- header( 'Cache-Control: post-check=0, pre-check=0', false );
- header( 'Pragma: no-cache' );
- }
-
- public function ajax_json_log() {
- $this->ajax_header();
- $file = $this->get_file();
- if ( ! $file ) {
- die( "File is empty or can't be opened." );
- }
- $this->parse( $file ); // writes to $this->content. preg_replace_callback is odd.
- echo( json_encode( array_values( $this->content ) ) );
- die();
- }
-
- public function ajax_delete() {
- $this->ajax_header();
- echo $this->delete();
- die();
- }
-
- public function ajax_filesize() {
- $this->ajax_header();
- echo json_encode( $this->get_size() );
- die();
- }
-}
-?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ readableFilesize() }}
-
-
Debug.log {{ readableFilesize() }}
-
-
-
-
-
-
- Autoreload
-
- delete
- Empty file. This can not be undone.
-
-
-
-
-
-
-
-
-
-
-
-
- {{ item.cnt }}
-
-
-
-
{{ item.msg }}
-
-
-
-
-
-
-
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..cae7199
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ PHPCS configuration file.
+
+ .
+
+ vendor/
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..06b4768
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ ./tests
+
+
+
\ No newline at end of file
diff --git a/readme.md b/readme.md
index c2877d1..a3d67ed 100644
--- a/readme.md
+++ b/readme.md
@@ -1,23 +1,27 @@
# PHP Error Log Viewer
-A single-file GUI for your php log files which groups similar errors. Written in PHP and Vue.js.
+
+![Build Status](https://travis-ci.org/schuhwerk/php-error-log-viewer.svg?branch=master)
+
+A GUI for your php log files which groups similar errors. Written in PHP and Vue.js.
- Reads the specified log file and automatically shows you new errors when they appear.
- Groups errors with similar text. This is far from perfect and just works with the (default) log-format like:
```[12-Jun-2030 12:58:55] PHP Notice: ...```
- Can be configured so clicking on an error will directly bring you to the mentioned file and line in vscode ([more below](#linking)).
+ - Requires > PHP 7.4
## Disclaimer
- This contains code for deleting your log-file.
- Might be heavy for you server for large log files (regexp-parsing).
- It is meant for development-environments.
- - Created for log-files in the format ``[31-Mar-2021 14:25:56 UTC] PHP Notice: ...``
+ - Created for log-files in the format ``[31-Mar-2021 14:25:56 UTC] PHP Notice: ...`` (nginx logs are currently not supported).
- There is still room for improvement (especially where the log-file is parsed).
- It does not work offline, as we rely on [cdns](https://en.wikipedia.org/wiki/Content_delivery_network) to load dependencies like vue.
## Getting Started
-Just copy the file next to your debug.log.
+Just copy the folder next to your debug.log.
or
```bash
@@ -26,8 +30,8 @@ composer require-dev schuhwerk/php-error-log-viewer
## Usage
-Open the file in your browser (like http://mydomain.local/php-error-log-viewer.php)
-![Screenshot of the viewer interface](screenshot.png)
+Open the folder in your browser (like http://mydomain.local/php-error-log-viewer)
+![Screenshot of the viewer interface](php-error-log-screenshot.png)
## Settings
@@ -53,3 +57,11 @@ This works for the following samples:
- PHP Fatal error: Uncaught TypeError: ..., called in C:\foo\bar/themes/defaultspace/functions.php on line 605
- ... and defined in C:\foo\bar/themes/defaultspace/functions.php:63
+## Ideas
+- This was a single-file gui. As it went bigger we separated files (to improve readability). There could be a build-step which brings things back to a single file (like [adminer](https://github.com/vrana/adminer) uses).
+- Update to vue3
+- Use vuetify instead vue-material.
+- Make offline useable.
+- Find a way to keep stack-traces together (while sorting)
+
+
diff --git a/screenshot.png b/screenshot.png
deleted file mode 100644
index 2d9abbf..0000000
Binary files a/screenshot.png and /dev/null differ
diff --git a/src/AjaxHandler.php b/src/AjaxHandler.php
new file mode 100644
index 0000000..540948f
--- /dev/null
+++ b/src/AjaxHandler.php
@@ -0,0 +1,78 @@
+log_handler = $log_handler;
+ }
+
+ public function handle_ajax_requests()
+ {
+ $this->ajax_handle_errors();
+ if (isset($_GET['get_log'])) {
+ $this->ajax_json_log();
+ }
+ if (isset($_GET['delete_log'])) {
+ $this->ajax_delete();
+ }
+ if (isset($_GET['filesize'])) {
+ $this->ajax_filesize();
+ }
+ }
+
+ public function ajax_handle_errors()
+ {
+ $used = array_diff(array( 'get_log', 'delete_log', 'filesize' ), array_keys($_GET));
+ if (count($used) === 3) {
+ return;
+ }
+ $file_issues = $this->log_handler->get_file_issues();
+
+ if ( ! empty( $file_issues ) ) {
+ $this->ajax_header();
+ echo $file_issues;
+ die();
+ }
+ }
+
+ public function ajax_header()
+ {
+ header('Content-Type: application/json');
+ header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
+ header('Cache-Control: post-check=0, pre-check=0', false);
+ header('Pragma: no-cache');
+ }
+
+ public function ajax_json_log()
+ {
+ $this->ajax_header();
+ $content = $this->log_handler->get_parsed_content();
+ echo( json_encode(array_values($content)) );
+ die();
+ }
+
+ public function ajax_delete()
+ {
+ $this->ajax_header();
+ echo $this->log_handler->delete();
+ die();
+ }
+
+ public function ajax_filesize()
+ {
+ $this->ajax_header();
+ echo json_encode($this->log_handler->get_size());
+ die();
+ }
+}
diff --git a/src/LogHandler.php b/src/LogHandler.php
new file mode 100644
index 0000000..9ca8480
--- /dev/null
+++ b/src/LogHandler.php
@@ -0,0 +1,201 @@
+ '../debug.log',
+ 'vscode_links' => true, // Stack trace references files. link them to your repo (https://code.visualstudio.com/docs/editor/command-line#_opening-vs-code-with-urls).
+ 'vscode_path_search' => '', // This is needed if you develop on a vm. like '/srv/www/...'.
+ 'vscode_path_replace' => '', // The local path to your repo. like 'c:/users/...'.
+ );
+
+ public function __construct($settings)
+ {
+ $this->settings = array_merge($this->default_settings, $settings);
+ }
+
+ /**
+ * Read the log-file.
+ *
+ * @return string|false The read string or false on failure.
+ */
+ public function get_file()
+ {
+ $my_file = fopen($this->settings['file_path'], 'r');
+ $size = $this->get_size();
+ return ( $my_file && $size ) ? fread($my_file, $size) : false;
+ }
+
+ /**
+ * Get the size of the log-file.
+ *
+ * @return int|false The size of the log file in bytes or false.
+ */
+ public function get_size()
+ {
+ if (empty($this->filesize)) {
+ $this->filesize = filesize($this->settings['file_path']);
+ }
+ return $this->filesize;
+ }
+
+ /**
+ * Get a description of any issue with the log-file. Empty string if no issue.
+ *
+ * @return string The description of the issue (or empty string).
+ */
+ public function get_file_issues()
+ {
+ if (! file_exists($this->settings['file_path'])) {
+ return "The file ({$this->settings['file_path']}) was not found. " .
+ 'You can specify a different file/location in the settings (check readme.md).';
+ }
+ if (0 == $this->get_size()) {
+ return 'Your log file is empty.';
+ }
+ $mbs = $this->get_size() / 1024 / 1024; // in MB.
+ if ($mbs > 100) {
+ if (! isset($_GET['ignore'])) {
+ return( "Aborting. debug.log is larger than 100 MB ($mbs).
+ If you want to continue anyway add the 'ignore' queryvar"
+ );
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Triggers preg_replace_callback which calls
+ * replace_callback function which stores values in $this->content.
+ *
+ * @param string $raw The content of the log file.
+ * @return void
+ */
+ private function parse($raw)
+ {
+ $error = preg_replace_callback('~^\[([^\]]*)\]((?:[^\r\n]*[\r\n]?(?!\[).*)*)~m', array( $this, 'replace_callback' ), $raw);
+ }
+
+ public function get_parsed_content()
+ {
+ $file = $this->get_file();
+ if (! $file) {
+ die("File is empty or can't be opened.");
+ }
+ $this->parse($file); // writes to $this->content. preg_replace_callback is odd.
+ return array_values($this->content);
+ }
+
+ public function link_vscode_files($string)
+ {
+ $string = preg_replace_callback('$([A-Z]:)?([\\\/][^:(\s]+)(?: on line |[:\(])([0-9]+)\)?$', array( $this, 'vscode_link_filter' ), $string);
+ return $string;
+ }
+
+ public function vscode_link_filter($matches)
+ {
+ $link = 'vscode://file/' . $matches[1] . $matches[2] . ':' . $matches[3];
+ // $root = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : $_SERVER['DOCUMENT_ROOT'];
+ // $val = parse_url( $root, PHP_URL_QUERY );
+ $link = str_replace($this->settings['vscode_path_search'], $this->settings['vscode_path_replace'], $link);
+ return "" . $matches[0] . '';
+ }
+
+ /**
+ * Callback function which is triggered by preg_replace_callback.
+ * Doesn't return but writes to $this->content.
+ *
+ * @param array $arr
+ * looks like that:
+ * array (
+ * 0 => [01-Jun-2016 09:24:02 UTC] PHP Fatal error: Allowed memory size of 456 bytes exhausted (tried to allocate 27 bytes) in ...
+ * 1 => [01-Jun-2016 09:24:02 UTC]
+ * 2 => PHP Fatal error: Allowed memory size of 56 bytes exhausted (tried to allocate 15627 bytes) in ... *
+ * )
+ * @return void
+ */
+ public function replace_callback($arr)
+ {
+ $err_id = crc32(trim($arr[2])); // create a unique identifier for the error message.
+ if (! isset($this->content[ $err_id ])) { // we have a new error.
+ $this->content[ $err_id ] = array();
+ $this->content[ $err_id ]['id'] = $err_id; // err_id.
+ $this->content[ $err_id ]['cnt'] = 1; // counter.
+ $this->index[] = $err_id;
+ } else { // we already have that error...
+ $this->content[ $err_id ]['cnt']++; // counter.
+ }
+
+ $date = date_create($arr[1]); // false if no valid date.
+ $this->content[ $err_id ]['time'] = $date ? $date->format(\DateTime::ATOM) : $arr[1]; // ISO8601, readable in js
+ $message = htmlspecialchars(trim($arr[2]), ENT_QUOTES);
+ $this->content[ $err_id ]['msg'] = $this->settings['vscode_links'] ? $this->link_vscode_files($message) : $message;
+ $this->content[ $err_id ]['cls'] = implode(
+ ' ',
+ array_slice(
+ str_word_count($this->content[ $err_id ]['msg'], 2),
+ 1,
+ 2
+ )
+ ); // the first few words of the message become class items.
+ }
+
+ public function delete()
+ {
+ if (! file_exists($this->settings['file_path'])) {
+ return 'There was no file to delete';
+ }
+ if (! is_writeable(realpath($this->settings['file_path']))) {
+ return 'Your log file is not writable';
+ }
+ $f = @fopen($this->settings['file_path'], 'r+');
+ if ($f !== false) {
+ ftruncate($f, 0);
+ fclose($f);
+ return 'Emptied file';
+ } else {
+ return 'File could not be emptied';
+ }
+ }
+}
diff --git a/src/error-log-viewer-frontend.html b/src/error-log-viewer-frontend.html
new file mode 100644
index 0000000..54fa979
--- /dev/null
+++ b/src/error-log-viewer-frontend.html
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ readableFilesize() }}
+
+
+
+
Log Viewer {{ readableFilesize() }}
+
+
+
+
+
+
+
+ Autoreload
+
+ delete
+ Empty file. This can not be undone.
+
+