diff --git a/.gitignore b/.gitignore index 3e2c1db..f6075c5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ src/config.php /doc-site/docs/user-guide/chapters/ src/index.html +.phpunit.result.cache diff --git a/src/export.php b/src/export.php index 1b017bc..4e39b92 100644 --- a/src/export.php +++ b/src/export.php @@ -148,6 +148,11 @@ // No limit - export all matching records $maxExportRecords = PHP_INT_MAX; } + + // --- Read export-specific config settings + $content['ExportAllMatchPages'] = GetConfigSetting("ExportAllMatchPages", 0, CFGLEVEL_USER) == 1; + $content['SuppressDuplicatedMessages'] = GetConfigSetting("ExportSuppressDuplicatedMessages", 0, CFGLEVEL_USER) == 1; + // --- // Copy current used columns here! $content['Columns'] = $content['Views'][$currentViewID]['Columns']; @@ -209,33 +214,59 @@ // We found matching records, so continue if ( $ret == SUCCESS ) { + // --- Init duplicate suppression state + $szLastMessage = ""; + $duplicateCount = 0; + // --- + //Loop through the messages! do { // --- Extra stuff for suppressing messages - if ( - GetConfigSetting("SuppressDuplicatedMessages", 0, CFGLEVEL_USER) == 1 - && - isset($logArray[SYSLOG_MESSAGE]) - ) + if ( $content['SuppressDuplicatedMessages'] && isset($logArray[SYSLOG_MESSAGE]) ) { - - if ( !isset($szLastMessage) ) // Only set lastmgr - $szLastMessage = $logArray[SYSLOG_MESSAGE]; + if ( $szLastMessage !== "" && $szLastMessage == $logArray[SYSLOG_MESSAGE] ) + { + // It's a duplicate — count and skip + $duplicateCount++; + + // Update period_end_ts for the duplicate record before skipping + if ( isset($logArray['timereported'][EVTIME_TIMESTAMP]) ) + $content['period_end_ts'] = $logArray['timereported'][EVTIME_TIMESTAMP]; + + // Read next entry + do { + $ret = $stream->ReadNext($uID, $logArray); + } while ( $ret == ERROR_MSG_SKIPMESSAGE ); + continue; + } else { - // Skip if same msg - if ( $szLastMessage == $logArray[SYSLOG_MESSAGE] ) + // Different message — flush any pending duplicate summary + if ( $duplicateCount > 0 ) { - // Set last mgr - $szLastMessage = $logArray[SYSLOG_MESSAGE]; - - // Skip entry - continue; + foreach ( $content['Columns'] as $mycolkey ) + { + $content['syslogmessages'][$counter][$mycolkey]['FieldColumn'] = $mycolkey; + $content['syslogmessages'][$counter][$mycolkey]['uid'] = ''; + $content['syslogmessages'][$counter][$mycolkey]['fieldvalue'] = ''; + } + if ( isset($content['fields'][SYSLOG_MESSAGE]) ) + $content['syslogmessages'][$counter][SYSLOG_MESSAGE]['fieldvalue'] = "... suppressed $duplicateCount duplicate(s)..."; + $counter++; + $duplicateCount = 0; } + $szLastMessage = $logArray[SYSLOG_MESSAGE]; } } - // --- + // --- + + // --- Track period timestamps for the export filename + if ( !isset($content['period_start_ts']) && isset($logArray['timereported'][EVTIME_TIMESTAMP]) ) + $content['period_start_ts'] = $logArray['timereported'][EVTIME_TIMESTAMP]; + if ( isset($logArray['timereported'][EVTIME_TIMESTAMP]) ) + $content['period_end_ts'] = $logArray['timereported'][EVTIME_TIMESTAMP]; + // --- // --- Now we populate the values array! foreach($content['Columns'] as $mycolkey) @@ -293,15 +324,41 @@ // Increment Counter $counter++; - } while ($counter < $maxExportRecords && ($ret = $stream->ReadNext($uID, $logArray)) == SUCCESS); + + // Safety limit — stop if we've hit the maximum export record count + if ( $counter >= $maxExportRecords ) + break; + + // Read next entry, skipping filtered-out entries + do { + $ret = $stream->ReadNext($uID, $logArray); + } while ( $ret == ERROR_MSG_SKIPMESSAGE ); + + // If not exporting all pages, stop after the current page size + if ( !$content['ExportAllMatchPages'] && $counter >= $content['CurrentViewEntriesPerPage'] ) + break; + + } while ($ret == SUCCESS); + + // Flush any trailing duplicate summary row + if ( $content['SuppressDuplicatedMessages'] && $duplicateCount > 0 ) + { + foreach ( $content['Columns'] as $mycolkey ) + { + $content['syslogmessages'][$counter][$mycolkey]['FieldColumn'] = $mycolkey; + $content['syslogmessages'][$counter][$mycolkey]['uid'] = ''; + $content['syslogmessages'][$counter][$mycolkey]['fieldvalue'] = ''; + } + if ( isset($content['fields'][SYSLOG_MESSAGE]) ) + $content['syslogmessages'][$counter][SYSLOG_MESSAGE]['fieldvalue'] = "... suppressed $duplicateCount duplicate(s)..."; + $counter++; + } if ( $content['read_direction'] == EnumReadDirection::Forward ) { // Back Button was clicked, so we need to flip the array $content['syslogmessages'] = array_reverse ( $content['syslogmessages'] ); } -// DEBUG -//print_r ( $content['syslogmessages'] ); } } else @@ -338,7 +395,9 @@ $szOutputMimeType = "text/plain"; $szOutputCharset = ""; - $szOutputFileName = "ExportMessages"; + $szOutputFileName = isset($content['period_start_ts']) + ? "ExportMessages_" . date('Ymd\THis', $content['period_start_ts']) . "-" . date('Ymd\THis', $content['period_end_ts']) + : "ExportMessages"; $szOutputFileExtension = ".txt"; $szOPFieldSeparator = " "; $szOPFirstLineFieldNames = true; diff --git a/src/include/config.sample.php b/src/include/config.sample.php index cb477e4..55b151c 100644 --- a/src/include/config.sample.php +++ b/src/include/config.sample.php @@ -105,6 +105,9 @@ $CFG['EnableContextLinks'] = 1; // if enabled, context links within the messages will automatically be created and added. Set this to 0 to disable all context links. $CFG['EnableIPAddressResolve'] = 1; // If enabled, IP Addresses inline messages are automatically resolved and the result is added in brackets {} behind the IP Address $CFG['SuppressDuplicatedMessages'] = 0; // If enabled, duplicated messages will be suppressed in the main display. +$CFG['ExportSuppressDuplicatedMessages'] = 0; // If enabled, export will suppress duplicate records (see also DuplicateRecordMaxTsDistance) +$CFG['ExportAllMatchPages'] = 0; // If enabled, export includes all matched records, not just the current page +$CFG['DuplicateRecordMaxTsDistance'] = PHP_INT_MAX; // Max timestamp delta (seconds) between two matching records to be considered duplicates $CFG['TreatNotFoundFiltersAsTrue'] = 0; // If you filter / search for messages, and the fields you are filtering for is not found, the filter result is treaten as TRUE! $CFG['PopupMenuTimeout'] = 3000; // This variable defines the default timeout value for popup menus in milliseconds. (those menus which popup when you click on the value of a field. $CFG['PhplogconLogoUrl'] = ""; // Put an Url to a custom toplogo you want to use. diff --git a/src/include/functions_common.php b/src/include/functions_common.php index bd614fc..845b66e 100644 --- a/src/include/functions_common.php +++ b/src/include/functions_common.php @@ -1322,8 +1322,26 @@ function RedirectResult( $szMsg, $newpage ) */ function GetEventTime($szTimStr) { + // Strip optional leading backslash (URL-encoded filter values may include it) + if ( strlen($szTimStr) > 0 && $szTimStr[0] === '\\' ) + $szTimStr = substr($szTimStr, 1); + + // Relative date syntax, samples: T00:00:00, T12:30, -1T, -2T01:30 + if ( preg_match("/^(-?[0-9]{1,2})?T([0-9]{0,2}):?([0-9]{0,2}):?([0-9]{0,2})$/", $szTimStr, $out ) ) + { + $days = (isset($out[1]) && strlen($out[1]) > 0) ? intval($out[1]) : 0; + $hh = strlen($out[2]) > 0 ? intval($out[2]) : 0; + $mm = strlen($out[3]) > 0 ? intval($out[3]) : 0; + $ss = strlen($out[4]) > 0 ? intval($out[4]) : 0; + $szTime = mktime($hh, $mm, $ss); + if ( $days !== 0 ) + $szTime = strtotime("$days days", $szTime); + $eventtime[EVTIME_TIMESTAMP] = $szTime; + $eventtime[EVTIME_TIMEZONE] = date('O'); + $eventtime[EVTIME_MICROSECONDS] = 0; + } // Sample: Mar 10 14:45:44 - if ( preg_match("/(...) ([0-9]{1,2}) ([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})/", $szTimStr, $out ) ) + else if ( preg_match("/(...) ([0-9]{1,2}) ([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})/", $szTimStr, $out ) ) { // RFC 3164 typical timestamp $eventtime[EVTIME_TIMESTAMP] = mktime($out[3], $out[4], $out[5], GetMonthFromString($out[1]), $out[2], date("Y") ); diff --git a/src/include/functions_filters.php b/src/include/functions_filters.php index 6524e09..e9ba686 100644 --- a/src/include/functions_filters.php +++ b/src/include/functions_filters.php @@ -323,6 +323,10 @@ function GetMessageTypeDisplayName( $nMsgTypeID ) function GetTimeStampFromTimeString($szTimeString) { + // Strip optional leading backslash (URL-encoded filter values may include it) + if ( strlen($szTimeString) > 0 && $szTimeString[0] === '\\' ) + $szTimeString = substr($szTimeString, 1); + //Sample: 2008-4-1T00:00:00 if ( preg_match("/([0-9]{4,4})-([0-9]{1,2})-([0-9]{1,2})T([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$/", $szTimeString, $out) ) { @@ -335,6 +339,18 @@ function GetTimeStampFromTimeString($szTimeString) // return new timestamp return mktime(0,0,0, $out[2], $out[3], $out[1]); } + // Relative date syntax, samples: T00:00:00, T12:30, -1T, -2T01:30 + else if ( preg_match("/^(-?[0-9]{1,2})?T([0-9]{0,2}):?([0-9]{0,2}):?([0-9]{0,2})$/", $szTimeString, $out) ) + { + $days = (isset($out[1]) && strlen($out[1]) > 0) ? intval($out[1]) : 0; + $hh = strlen($out[2]) > 0 ? intval($out[2]) : 0; + $mm = strlen($out[3]) > 0 ? intval($out[3]) : 0; + $ss = strlen($out[4]) > 0 ? intval($out[4]) : 0; + $szTime = mktime($hh, $mm, $ss); + if ( $days != 0 ) + $szTime = strtotime("$days days", $szTime); + return $szTime; + } else { OutputDebugMessage("Unparseable Time in GetTimeStampFromTimeString - '" . $szTimeString . "'", DEBUG_WARN); diff --git a/src/index.php b/src/index.php index d182491..f7ef723 100644 --- a/src/index.php +++ b/src/index.php @@ -333,39 +333,70 @@ $myMsgCharLimit = GetConfigSetting("ViewMessageCharacterLimit", 80, CFGLEVEL_USER); $myStrCharLimit = GetConfigSetting("ViewStringCharacterLimit", 30, CFGLEVEL_USER); $ViewColoredCells = GetConfigSetting("ViewColoredCells", 0, CFGLEVEL_USER); + $DuplicateRecordMaxTsDistance = GetConfigSetting("DuplicateRecordMaxTsDistance", PHP_INT_MAX, CFGLEVEL_USER); + // --- + + // --- Init duplicate suppression state + $szLastMessage = ""; + $szLastMessageTimestamp = 0; + $duplicateCount = 0; + $duplicateCountTotal = 0; // --- //Loop through the messages! do { // --- Extra stuff for suppressing messages - if ( - GetConfigSetting("SuppressDuplicatedMessages", 0, CFGLEVEL_USER) == 1 - && - isset($logArray[SYSLOG_MESSAGE]) - ) + if ( GetConfigSetting("SuppressDuplicatedMessages", 0, CFGLEVEL_USER) == 1 && isset($logArray[SYSLOG_MESSAGE]) ) { + $szCurrentMessage = $logArray[SYSLOG_MESSAGE]; + $szCurrentTs = isset($logArray['timereported'][EVTIME_TIMESTAMP]) ? $logArray['timereported'][EVTIME_TIMESTAMP] : 0; + $tsDiff = ($szLastMessageTimestamp > 0 && $szCurrentTs > 0) ? abs($szCurrentTs - $szLastMessageTimestamp) : 0; - if ( !isset($szLastMessage) ) // Only set lastmgr - $szLastMessage = $logArray[SYSLOG_MESSAGE]; - else + if ( $szLastMessage !== "" && $szLastMessage == $szCurrentMessage && $tsDiff <= $DuplicateRecordMaxTsDistance ) { - // Skip if same msg - if ( $szLastMessage == $logArray[SYSLOG_MESSAGE] ) + // It's a duplicate + $duplicateCount++; + $duplicateCountTotal++; + $szLastMessageTimestamp = $szCurrentTs; + + // --- Extra Loop to get the next entry! + do { - // Set last mgr - $szLastMessage = $logArray[SYSLOG_MESSAGE]; + $ret = $stream->ReadNext($uID, $logArray); + } while ( $ret == ERROR_MSG_SKIPMESSAGE ); + // --- - // --- Extra Loop to get the next entry! - do + // Skip entry + continue; + } + else + { + // Different message — flush any pending duplicate summary row + if ( $duplicateCount > 0 ) + { + $content['syslogmessages'][$counter]['cssclass'] = "line1"; + $content['syslogmessages'][$counter]['MiscShowDebugGridCounter'] = $content['MiscShowDebugGridCounter']; + foreach ( $content['Columns'] as $mycolkey ) { - $ret = $stream->ReadNext($uID, $logArray); - } while ( $ret == ERROR_MSG_SKIPMESSAGE ); - // --- - - // Skip entry - continue; + $content['syslogmessages'][$counter]['values'][$mycolkey]['FieldColumn'] = $mycolkey; + $content['syslogmessages'][$counter]['values'][$mycolkey]['uid'] = ''; + $content['syslogmessages'][$counter]['values'][$mycolkey]['FieldAlign'] = isset($fields[$mycolkey]) ? $fields[$mycolkey]['FieldAlign'] : 'left'; + $content['syslogmessages'][$counter]['values'][$mycolkey]['fieldcssclass'] = "line1"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['fieldbgcolor'] = ""; + $content['syslogmessages'][$counter]['values'][$mycolkey]['isnowrap'] = "nowrap"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['hasdetails'] = "false"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['detailimagealign'] = "TOP"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['detaillink'] = "#"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['fieldvalue'] = ''; + } + if ( isset($content['syslogmessages'][$counter]['values'][SYSLOG_MESSAGE]) ) + $content['syslogmessages'][$counter]['values'][SYSLOG_MESSAGE]['fieldvalue'] = "... suppressed $duplicateCount duplicate(s)..."; + $counter++; + $duplicateCount = 0; } + $szLastMessage = $szCurrentMessage; + $szLastMessageTimestamp = $szCurrentTs; } } // --- @@ -689,7 +720,33 @@ } while ( $ret == ERROR_MSG_SKIPMESSAGE ); // --- } while ( $counter < $content['CurrentViewEntriesPerPage'] && ($ret == SUCCESS) ); -//print_r ( $content['syslogmessages'] ); + + // Flush any trailing duplicate summary row that was pending when the loop ended + if ( GetConfigSetting("SuppressDuplicatedMessages", 0, CFGLEVEL_USER) == 1 && $duplicateCount > 0 ) + { + $content['syslogmessages'][$counter]['cssclass'] = "line1"; + $content['syslogmessages'][$counter]['MiscShowDebugGridCounter'] = $content['MiscShowDebugGridCounter']; + foreach ( $content['Columns'] as $mycolkey ) + { + $content['syslogmessages'][$counter]['values'][$mycolkey]['FieldColumn'] = $mycolkey; + $content['syslogmessages'][$counter]['values'][$mycolkey]['uid'] = ''; + $content['syslogmessages'][$counter]['values'][$mycolkey]['FieldAlign'] = isset($fields[$mycolkey]) ? $fields[$mycolkey]['FieldAlign'] : 'left'; + $content['syslogmessages'][$counter]['values'][$mycolkey]['fieldcssclass'] = "line1"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['fieldbgcolor'] = ""; + $content['syslogmessages'][$counter]['values'][$mycolkey]['isnowrap'] = "nowrap"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['hasdetails'] = "false"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['detailimagealign'] = "TOP"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['detaillink'] = "#"; + $content['syslogmessages'][$counter]['values'][$mycolkey]['fieldvalue'] = ''; + } + if ( isset($content['syslogmessages'][$counter]['values'][SYSLOG_MESSAGE]) ) + $content['syslogmessages'][$counter]['values'][SYSLOG_MESSAGE]['fieldvalue'] = "... suppressed $duplicateCount duplicate(s)..."; + $counter++; + } + + // Expose suppressed record count and flag for the UI + $content['main_suppressed_recordcount'] = $duplicateCountTotal; + $content['SUPPRESS_ENABLED'] = GetConfigSetting("SuppressDuplicatedMessages", 0, CFGLEVEL_USER) == 1 ? "true" : "false"; // Move below processing - Read First and LAST UID's before start reading the stream! // $content['uid_last'] = $stream->GetLastPageUID(); diff --git a/src/lang/en/main.php b/src/lang/en/main.php index 2fd9d6e..ba157cd 100644 --- a/src/lang/en/main.php +++ b/src/lang/en/main.php @@ -38,6 +38,7 @@ $content['LN_GEN_NEXTPAGE'] = "Next Page"; $content['LN_GEN_PREVIOUSPAGE'] = "Previous Page"; $content['LN_GEN_RECORDCOUNT'] = "Total records found"; +$content['LN_GEN_SUPPRESSEDRECORDCOUNT'] = "Suppressed"; $content['LN_GEN_PAGERSIZE'] = "Records per page"; $content['LN_GEN_PAGE'] = "Page"; $content['LN_GEN_PREDEFINEDSEARCHES'] = "Predefined Searches"; diff --git a/src/templates/index.html b/src/templates/index.html index 862ab23..847296c 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -173,6 +173,11 @@ {main_recordcount} + + {LN_GEN_SUPPRESSEDRECORDCOUNT}: + {main_suppressed_recordcount} + + {LN_GEN_PAGERSIZE}: