diff --git a/install/sql/incremental/upd_dev_collection.sql b/install/sql/incremental/upd_dev_collection.sql
index 4fc891f8e845d8ff65e442e14452aa29e07b03a7..91488d8464a95a578c7a3fa073fb8b08b9f10e28 100644
--- a/install/sql/incremental/upd_dev_collection.sql
+++ b/install/sql/incremental/upd_dev_collection.sql
@@ -161,4 +161,5 @@ CREATE TABLE IF NOT EXISTS `sys_mailqueue` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
-ALTER TABLE `web_domain` ADD `jailkit_jkupdate_cron` enum('n','y') NOT NULL DEFAULT 'y' AFTER `custom_php_ini`;
\ No newline at end of file
+ALTER TABLE `web_domain` ADD `jailkit_jkupdate_cron` enum('n','y') NOT NULL DEFAULT 'y' AFTER `custom_php_ini`;
+ALTER TABLE `sys_datalog` ADD `session_id` varchar(64) NOT NULL DEFAULT '' AFTER `error`;
diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql
index c3d17421d8c30371f0984de5da5ad33c83d22283..23df197329940bd256cf4b55f6fb031ad2025722 100644
--- a/install/sql/ispconfig3.sql
+++ b/install/sql/ispconfig3.sql
@@ -1491,6 +1491,7 @@ CREATE TABLE `sys_datalog` (
`data` longtext,
`status` set('pending','ok','warning','error') NOT NULL default 'ok',
`error` mediumtext,
+ `session_id` varchar(64) NOT NULL DEFAULT '',
PRIMARY KEY (`datalog_id`),
KEY `server_id` (`server_id`,`status`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
diff --git a/interface/lib/classes/db_mysql.inc.php b/interface/lib/classes/db_mysql.inc.php
index b03ad55676ddb3c30cf01c4e12eed534865d8585..8443d106b1b9c8e3b75bbec659165f9f8c10b50f 100644
--- a/interface/lib/classes/db_mysql.inc.php
+++ b/interface/lib/classes/db_mysql.inc.php
@@ -719,8 +719,8 @@ class db
if($action == 'INSERT') $action = 'i';
if($action == 'UPDATE') $action = 'u';
if($action == 'DELETE') $action = 'd';
- $sql = "INSERT INTO sys_datalog (dbtable,dbidx,server_id,action,tstamp,user,data) VALUES (?, ?, ?, ?, ?, ?, ?)";
- $app->db->query($sql, $db_table, $dbidx, $server_id, $action, time(), $username, $diffstr);
+ $sql = "INSERT INTO sys_datalog (dbtable,dbidx,server_id,action,tstamp,user,data,session_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
+ $app->db->query($sql, $db_table, $dbidx, $server_id, $action, time(), $username, $diffstr, session_id());
}
return true;
diff --git a/interface/lib/classes/finediff.inc.php b/interface/lib/classes/finediff.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..43b542f3629ec5ce14741267a0cdf991865bd193
--- /dev/null
+++ b/interface/lib/classes/finediff.inc.php
@@ -0,0 +1,752 @@
+copy->insert
+* command (swap) for when the inserted segment is exactly the same
+* as the deleted one, and with only a copy operation in between.
+* TODO: How often this case occurs? Is it worth it? Can only
+* be done as a postprocessing method (->optimize()?)
+*/
+abstract class FineDiffOp {
+ abstract public function getFromLen();
+ abstract public function getToLen();
+ abstract public function getOpcode();
+ }
+
+class FineDiffDeleteOp extends FineDiffOp {
+ public function __construct($len) {
+ $this->fromLen = $len;
+ }
+ public function getFromLen() {
+ return $this->fromLen;
+ }
+ public function getToLen() {
+ return 0;
+ }
+ public function getOpcode() {
+ if ( $this->fromLen === 1 ) {
+ return 'd';
+ }
+ return "d{$this->fromLen}";
+ }
+ }
+
+class FineDiffInsertOp extends FineDiffOp {
+ public function __construct($text) {
+ $this->text = $text;
+ }
+ public function getFromLen() {
+ return 0;
+ }
+ public function getToLen() {
+ return strlen($this->text);
+ }
+ public function getText() {
+ return $this->text;
+ }
+ public function getOpcode() {
+ $to_len = strlen($this->text);
+ if ( $to_len === 1 ) {
+ return "i:{$this->text}";
+ }
+ return "i{$to_len}:{$this->text}";
+ }
+ }
+
+class FineDiffReplaceOp extends FineDiffOp {
+ public function __construct($fromLen, $text) {
+ $this->fromLen = $fromLen;
+ $this->text = $text;
+ }
+ public function getFromLen() {
+ return $this->fromLen;
+ }
+ public function getToLen() {
+ return strlen($this->text);
+ }
+ public function getText() {
+ return $this->text;
+ }
+ public function getOpcode() {
+ if ( $this->fromLen === 1 ) {
+ $del_opcode = 'd';
+ }
+ else {
+ $del_opcode = "d{$this->fromLen}";
+ }
+ $to_len = strlen($this->text);
+ if ( $to_len === 1 ) {
+ return "{$del_opcode}i:{$this->text}";
+ }
+ return "{$del_opcode}i{$to_len}:{$this->text}";
+ }
+ }
+
+class FineDiffCopyOp extends FineDiffOp {
+ public function __construct($len) {
+ $this->len = $len;
+ }
+ public function getFromLen() {
+ return $this->len;
+ }
+ public function getToLen() {
+ return $this->len;
+ }
+ public function getOpcode() {
+ if ( $this->len === 1 ) {
+ return 'c';
+ }
+ return "c{$this->len}";
+ }
+ public function increase($size) {
+ return $this->len += $size;
+ }
+ }
+
+/**
+* FineDiff ops
+*
+* Collection of ops
+*/
+class FineDiffOps {
+ public function appendOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+ $edits[] = new FineDiffCopyOp($from_len);
+ }
+ else if ( $opcode === 'd' ) {
+ $edits[] = new FineDiffDeleteOp($from_len);
+ }
+ else /* if ( $opcode === 'i' ) */ {
+ $edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len));
+ }
+ }
+ public $edits = array();
+ }
+
+/**
+* FineDiff class
+*
+* TODO: Document
+*
+*/
+class FineDiff {
+
+ /**------------------------------------------------------------------------
+ *
+ * Public section
+ *
+ */
+
+ /**
+ * Constructor
+ * ...
+ * The $granularityStack allows FineDiff to be configurable so that
+ * a particular stack tailored to the specific content of a document can
+ * be passed.
+ */
+ public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
+ // setup stack for generic text documents by default
+ $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
+ $this->edits = array();
+ $this->from_text = $from_text;
+ $this->doDiff($from_text, $to_text);
+ }
+
+ public function getOps() {
+ return $this->edits;
+ }
+
+ public function getOpcodes() {
+ $opcodes = array();
+ foreach ( $this->edits as $edit ) {
+ $opcodes[] = $edit->getOpcode();
+ }
+ return implode('', $opcodes);
+ }
+
+ public function renderDiffToHTML() {
+ $in_offset = 0;
+ ob_start();
+ foreach ( $this->edits as $edit ) {
+ $n = $edit->getFromLen();
+ if ( $edit instanceof FineDiffCopyOp ) {
+ FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffDeleteOp ) {
+ FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffInsertOp ) {
+ FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+ FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ $in_offset += $n;
+ }
+ return ob_get_clean();
+ }
+
+ /**------------------------------------------------------------------------
+ * Return an opcodes string describing the diff between a "From" and a
+ * "To" string
+ */
+ public static function getDiffOpcodes($from, $to, $granularities = null) {
+ $diff = new FineDiff($from, $to, $granularities);
+ return $diff->getOpcodes();
+ }
+
+ /**------------------------------------------------------------------------
+ * Return an iterable collection of diff ops from an opcodes string
+ */
+ public static function getDiffOpsFromOpcodes($opcodes) {
+ $diffops = new FineDiffOps();
+ FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
+ return $diffops->edits;
+ }
+
+ /**------------------------------------------------------------------------
+ * Re-create the "To" string from the "From" string and an "Opcodes" string
+ */
+ public static function renderToTextFromOpcodes($from, $opcodes) {
+ ob_start();
+ FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+ return ob_get_clean();
+ }
+
+ /**------------------------------------------------------------------------
+ * Render the diff to an HTML string -- UTF8 unsafe
+ */
+ public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+ ob_start();
+ FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+ return ob_get_clean();
+ }
+
+ /**------------------------------------------------------------------------
+ * Render the diff to an HTML string -- UTF8 safe
+ */
+ public static function renderUTF8DiffToHTMLFromOpcodes($from, $opcodes) {
+ ob_start();
+ FineDiff::renderUTF8FromOpcode($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+ return ob_get_clean();
+ }
+
+ /**------------------------------------------------------------------------
+ * Generic opcodes parser, user must supply callback for handling
+ * single opcode
+ */
+ public static function renderFromOpcodes($from, $opcodes, $callback) {
+ if ( !is_callable($callback) ) {
+ return;
+ }
+ $opcodes_len = strlen($opcodes);
+ $from_offset = $opcodes_offset = 0;
+ while ( $opcodes_offset < $opcodes_len ) {
+ $opcode = substr($opcodes, $opcodes_offset, 1);
+ $opcodes_offset++;
+ $n = intval(substr($opcodes, $opcodes_offset));
+ if ( $n ) {
+ $opcodes_offset += strlen(strval($n));
+ }
+ else {
+ $n = 1;
+ }
+ if ( $opcode === 'c' ) { // copy n characters from source
+ call_user_func($callback, 'c', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else if ( $opcode === 'd' ) { // delete n characters from source
+ call_user_func($callback, 'd', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+ call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+ $opcodes_offset += 1 + $n;
+ }
+ }
+ }
+
+ /**------------------------------------------------------------------------
+ * Generic opcodes parser, user must supply callback for handling
+ * single opcode
+ */
+ private static function renderUTF8FromOpcode($from, $opcodes, $callback) {
+ if ( !is_callable($callback) ) {
+ return;
+ }
+ $from_len = strlen($from);
+ $opcodes_len = strlen($opcodes);
+ $from_offset = $opcodes_offset = 0;
+ $last_to_chars = '';
+ while ( $opcodes_offset < $opcodes_len ) {
+ $opcode = substr($opcodes, $opcodes_offset, 1);
+ $opcodes_offset++;
+ $n = intval(substr($opcodes, $opcodes_offset));
+ if ( $n ) {
+ $opcodes_offset += strlen(strval($n));
+ }
+ else {
+ $n = 1;
+ }
+ if ( $opcode === 'c' || $opcode === 'd' ) {
+ $beg = $from_offset;
+ $end = $from_offset + $n;
+ while ( $beg > 0 && (ord($from[$beg]) & 0xC0) === 0x80 ) { $beg--; }
+ while ( $end < $from_len && (ord($from[$end]) & 0xC0) === 0x80 ) { $end++; }
+ if ( $opcode === 'c' ) { // copy n characters from source
+ call_user_func($callback, 'c', $from, $beg, $end - $beg, '');
+ $last_to_chars = substr($from, $beg, $end - $beg);
+ }
+ else /* if ( $opcode === 'd' ) */ { // delete n characters from source
+ call_user_func($callback, 'd', $from, $beg, $end - $beg, '');
+ }
+ $from_offset += $n;
+ }
+ else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+ $opcodes_offset += 1;
+ if ( strlen($last_to_chars) > 0 && (ord($opcodes[$opcodes_offset]) & 0xC0) === 0x80 ) {
+ $beg = strlen($last_to_chars) - 1;
+ while ( $beg > 0 && (ord($last_to_chars[$beg]) & 0xC0) === 0x80 ) { $beg--; }
+ $prefix = substr($last_to_chars, $beg);
+ } else {
+ $prefix = '';
+ }
+ $end = $from_offset;
+ while ( $end < $from_len && (ord($from[$end]) & 0xC0) === 0x80 ) { $end++; }
+ $toInsert = $prefix . substr($opcodes, $opcodes_offset, $n) . substr($from, $end, $end - $from_offset);
+ call_user_func($callback, 'i', $toInsert, 0, strlen($toInsert));
+ $opcodes_offset += $n;
+ $last_to_chars = $toInsert;
+ }
+ }
+ }
+
+ /**
+ * Stock granularity stacks and delimiters
+ */
+
+ const paragraphDelimiters = "\n\r";
+ public static $paragraphGranularity = array(
+ FineDiff::paragraphDelimiters
+ );
+ const sentenceDelimiters = ".\n\r";
+ public static $sentenceGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters
+ );
+ const wordDelimiters = " \t.\n\r";
+ public static $wordGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters,
+ FineDiff::wordDelimiters
+ );
+ const characterDelimiters = "";
+ public static $characterGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters,
+ FineDiff::wordDelimiters,
+ FineDiff::characterDelimiters
+ );
+
+ public static $textStack = array(
+ ".",
+ " \t.\n\r",
+ ""
+ );
+
+ /**------------------------------------------------------------------------
+ *
+ * Private section
+ *
+ */
+
+ /**
+ * Entry point to compute the diff.
+ */
+ private function doDiff($from_text, $to_text) {
+ $this->last_edit = false;
+ $this->stackpointer = 0;
+ $this->from_text = $from_text;
+ $this->from_offset = 0;
+ // can't diff without at least one granularity specifier
+ if ( empty($this->granularityStack) ) {
+ return;
+ }
+ $this->_processGranularity($from_text, $to_text);
+ }
+
+ /**
+ * This is the recursive function which is responsible for
+ * handling/increasing granularity.
+ *
+ * Incrementally increasing the granularity is key to compute the
+ * overall diff in a very efficient way.
+ */
+ private function _processGranularity($from_segment, $to_segment) {
+ $delimiters = $this->granularityStack[$this->stackpointer++];
+ $has_next_stage = $this->stackpointer < count($this->granularityStack);
+ foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
+ // increase granularity
+ if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
+ $this->_processGranularity(
+ substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
+ $fragment_edit->getText()
+ );
+ }
+ // fuse copy ops whenever possible
+ else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
+ $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
+ $this->from_offset += $fragment_edit->getFromLen();
+ }
+ else {
+ /* $fragment_edit instanceof FineDiffCopyOp */
+ /* $fragment_edit instanceof FineDiffDeleteOp */
+ /* $fragment_edit instanceof FineDiffInsertOp */
+ $this->edits[] = $this->last_edit = $fragment_edit;
+ $this->from_offset += $fragment_edit->getFromLen();
+ }
+ }
+ $this->stackpointer--;
+ }
+
+ /**
+ * This is the core algorithm which actually perform the diff itself,
+ * fragmenting the strings as per specified delimiters.
+ *
+ * This function is naturally recursive, however for performance purpose
+ * a local job queue is used instead of outright recursivity.
+ */
+ private static function doFragmentDiff($from_text, $to_text, $delimiters) {
+ // Empty delimiter means character-level diffing.
+ // In such case, use code path optimized for character-level
+ // diffing.
+ if ( empty($delimiters) ) {
+ return FineDiff::doCharDiff($from_text, $to_text);
+ }
+
+ $result = array();
+
+ // fragment-level diffing
+ $from_text_len = strlen($from_text);
+ $to_text_len = strlen($to_text);
+ $from_fragments = FineDiff::extractFragments($from_text, $delimiters);
+ $to_fragments = FineDiff::extractFragments($to_text, $delimiters);
+
+ $jobs = array(array(0, $from_text_len, 0, $to_text_len));
+
+ $cached_array_keys = array();
+
+ while ( $job = array_pop($jobs) ) {
+
+ // get the segments which must be diff'ed
+ list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+
+ // catch easy cases first
+ $from_segment_length = $from_segment_end - $from_segment_start;
+ $to_segment_length = $to_segment_end - $to_segment_start;
+ if ( !$from_segment_length || !$to_segment_length ) {
+ if ( $from_segment_length ) {
+ $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
+ }
+ else if ( $to_segment_length ) {
+ $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
+ }
+ continue;
+ }
+
+ // find longest copy operation for the current segments
+ $best_copy_length = 0;
+
+ $from_base_fragment_index = $from_segment_start;
+
+ $cached_array_keys_for_current_segment = array();
+
+ while ( $from_base_fragment_index < $from_segment_end ) {
+ $from_base_fragment = $from_fragments[$from_base_fragment_index];
+ $from_base_fragment_length = strlen($from_base_fragment);
+ // performance boost: cache array keys
+ if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
+ if ( !isset($cached_array_keys[$from_base_fragment]) ) {
+ $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
+ }
+ else {
+ $to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
+ }
+ // get only indices which falls within current segment
+ if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
+ $to_fragment_indices = array();
+ foreach ( $to_all_fragment_indices as $to_fragment_index ) {
+ if ( $to_fragment_index < $to_segment_start ) { continue; }
+ if ( $to_fragment_index >= $to_segment_end ) { break; }
+ $to_fragment_indices[] = $to_fragment_index;
+ }
+ $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
+ }
+ else {
+ $to_fragment_indices = $to_all_fragment_indices;
+ }
+ }
+ else {
+ $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
+ }
+ // iterate through collected indices
+ foreach ( $to_fragment_indices as $to_base_fragment_index ) {
+ $fragment_index_offset = $from_base_fragment_length;
+ // iterate until no more match
+ for (;;) {
+ $fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
+ if ( $fragment_from_index >= $from_segment_end ) {
+ break;
+ }
+ $fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
+ if ( $fragment_to_index >= $to_segment_end ) {
+ break;
+ }
+ if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
+ break;
+ }
+ $fragment_length = strlen($from_fragments[$fragment_from_index]);
+ $fragment_index_offset += $fragment_length;
+ }
+ if ( $fragment_index_offset > $best_copy_length ) {
+ $best_copy_length = $fragment_index_offset;
+ $best_from_start = $from_base_fragment_index;
+ $best_to_start = $to_base_fragment_index;
+ }
+ }
+ $from_base_fragment_index += strlen($from_base_fragment);
+ // If match is larger than half segment size, no point trying to find better
+ // TODO: Really?
+ if ( $best_copy_length >= $from_segment_length / 2) {
+ break;
+ }
+ // no point to keep looking if what is left is less than
+ // current best match
+ if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
+ break;
+ }
+ }
+
+ if ( $best_copy_length ) {
+ $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
+ $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
+ $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
+ }
+ else {
+ $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
+ }
+ }
+
+ ksort($result, SORT_NUMERIC);
+ return array_values($result);
+ }
+
+ /**
+ * Perform a character-level diff.
+ *
+ * The algorithm is quite similar to doFragmentDiff(), except that
+ * the code path is optimized for character-level diff -- strpos() is
+ * used to find out the longest common subequence of characters.
+ *
+ * We try to find a match using the longest possible subsequence, which
+ * is at most the length of the shortest of the two strings, then incrementally
+ * reduce the size until a match is found.
+ *
+ * I still need to study more the performance of this function. It
+ * appears that for long strings, the generic doFragmentDiff() is more
+ * performant. For word-sized strings, doCharDiff() is somewhat more
+ * performant.
+ */
+ private static function doCharDiff($from_text, $to_text) {
+ $result = array();
+ $jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
+ while ( $job = array_pop($jobs) ) {
+ // get the segments which must be diff'ed
+ list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+ $from_segment_len = $from_segment_end - $from_segment_start;
+ $to_segment_len = $to_segment_end - $to_segment_start;
+
+ // catch easy cases first
+ if ( !$from_segment_len || !$to_segment_len ) {
+ if ( $from_segment_len ) {
+ $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
+ }
+ else if ( $to_segment_len ) {
+ $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
+ }
+ continue;
+ }
+ if ( $from_segment_len >= $to_segment_len ) {
+ $copy_len = $to_segment_len;
+ while ( $copy_len ) {
+ $to_copy_start = $to_segment_start;
+ $to_copy_start_max = $to_segment_end - $copy_len;
+ while ( $to_copy_start <= $to_copy_start_max ) {
+ $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
+ if ( $from_copy_start !== false ) {
+ $from_copy_start += $from_segment_start;
+ break 2;
+ }
+ $to_copy_start++;
+ }
+ $copy_len--;
+ }
+ }
+ else {
+ $copy_len = $from_segment_len;
+ while ( $copy_len ) {
+ $from_copy_start = $from_segment_start;
+ $from_copy_start_max = $from_segment_end - $copy_len;
+ while ( $from_copy_start <= $from_copy_start_max ) {
+ $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
+ if ( $to_copy_start !== false ) {
+ $to_copy_start += $to_segment_start;
+ break 2;
+ }
+ $from_copy_start++;
+ }
+ $copy_len--;
+ }
+ }
+ // match found
+ if ( $copy_len ) {
+ $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
+ $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
+ $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
+ }
+ // no match, so delete all, insert all
+ else {
+ $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
+ }
+ }
+ ksort($result, SORT_NUMERIC);
+ return array_values($result);
+ }
+
+ /**
+ * Efficiently fragment the text into an array according to
+ * specified delimiters.
+ * No delimiters means fragment into single character.
+ * The array indices are the offset of the fragments into
+ * the input string.
+ * A sentinel empty fragment is always added at the end.
+ * Careful: No check is performed as to the validity of the
+ * delimiters.
+ */
+ private static function extractFragments($text, $delimiters) {
+ // special case: split into characters
+ if ( empty($delimiters) ) {
+ $chars = str_split($text, 1);
+ $chars[strlen($text)] = '';
+ return $chars;
+ }
+ $fragments = array();
+ $start = $end = 0;
+ for (;;) {
+ $end += strcspn($text, $delimiters, $end);
+ $end += strspn($text, $delimiters, $end);
+ if ( $end === $start ) {
+ break;
+ }
+ $fragments[$start] = substr($text, $start, $end - $start);
+ $start = $end;
+ }
+ $fragments[$start] = '';
+ return $fragments;
+ }
+
+ /**
+ * Stock opcode renderers
+ */
+ private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' || $opcode === 'i' ) {
+ echo substr($from, $from_offset, $from_len);
+ }
+ }
+
+ private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+ echo htmlspecialchars(substr($from, $from_offset, $from_len));
+ }
+ else if ( $opcode === 'd' ) {
+ $deletion = substr($from, $from_offset, $from_len);
+ if ( strcspn($deletion, " \n\r") === 0 ) {
+ $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+ }
+ echo '', htmlspecialchars($deletion), '';
+ }
+ else /* if ( $opcode === 'i' ) */ {
+ echo '', htmlspecialchars(substr($from, $from_offset, $from_len)), '';
+ }
+ }
+ }
+
diff --git a/interface/web/login/index.php b/interface/web/login/index.php
index 5fb94d80062b1bfca38ead5d8f0a9518f5965913..b55ac74f191eee74e4991fbdef7a8091e0921bb8 100644
--- a/interface/web/login/index.php
+++ b/interface/web/login/index.php
@@ -262,7 +262,7 @@ if(count($_POST) > 0) {
$app->plugin->raiseEvent('login', $username);
//* Save successfull login message to var
- $authlog = 'Successful login for user \''. $username .'\' from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s');
+ $authlog = 'Successful login for user \''. $username .'\' from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s') . ' with session ID ' .session_id();
$authlog_handle = fopen($conf['ispconfig_log_dir'].'/auth.log', 'a');
fwrite($authlog_handle, $authlog ."\n");
fclose($authlog_handle);
diff --git a/interface/web/monitor/dataloghistory_list.php b/interface/web/monitor/dataloghistory_list.php
new file mode 100644
index 0000000000000000000000000000000000000000..cab8da8a3671454d32c6598bde1a74e5a1e512b5
--- /dev/null
+++ b/interface/web/monitor/dataloghistory_list.php
@@ -0,0 +1,54 @@
+auth->check_module_permissions('monitor');
+
+$app->uses('listform_actions');
+
+$app->listform_actions->SQLOrderBy = "ORDER BY sys_datalog.tstamp DESC, sys_datalog.datalog_id DESC";
+
+$app->listform_actions->onLoad();
+
+
+?>
diff --git a/interface/web/monitor/dataloghistory_undo.php b/interface/web/monitor/dataloghistory_undo.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ff08faa0aa2a49fef1518c2262dc21861aad5d5
--- /dev/null
+++ b/interface/web/monitor/dataloghistory_undo.php
@@ -0,0 +1,70 @@
+auth->check_module_permissions('monitor');
+
+// Loading the template
+$app->uses('tpl');
+$app->tpl->newTemplate("form.tpl.htm");
+$app->tpl->setInclude('content_tpl', 'templates/dataloghistory_undo.htm');
+
+require('lib/lang/'.$_SESSION['s']['language'].'_dataloghistory_undo.lng');
+$app->tpl->setvar($wb);
+
+$id = intval($_GET['id']);
+
+$record = $app->db->queryOneRecord('SELECT * FROM sys_datalog WHERE datalog_id = ?', $id);
+
+$dbidx = explode(':', $record['dbidx']);
+
+$old_record = $app->db->queryOneRecord('SELECT * FROM ?? WHERE ??=?', $record['dbtable'], $dbidx[0], $dbidx[1]);
+
+if (is_array($old_record)) {
+ if(!$data = unserialize(stripslashes($record['data']))) {
+ $data = unserialize($record['data']);
+ }
+
+ $new_record = $data['old'];
+
+ $app->db->datalogUpdate($record['dbtable'], $new_record, $dbidx[0], $dbidx[1]);
+
+ $app->tpl->setVar('success', true);
+} else {
+ $app->tpl->setVar('success', false);
+}
+
+$app->tpl_defaults();
+$app->tpl->pparse();
+
+?>
diff --git a/interface/web/monitor/dataloghistory_view.php b/interface/web/monitor/dataloghistory_view.php
new file mode 100644
index 0000000000000000000000000000000000000000..e3b5e7b63c24b89bfc7bf75b6607c4d549f545cb
--- /dev/null
+++ b/interface/web/monitor/dataloghistory_view.php
@@ -0,0 +1,130 @@
+auth->check_module_permissions('monitor');
+
+$app->load('finediff');
+
+// Loading the template
+$app->uses('tpl');
+$app->tpl->newTemplate("form.tpl.htm");
+$app->tpl->setInclude('content_tpl', 'templates/dataloghistory_view.htm');
+
+$app->load_language_file('web/monitor/lib/lang/'.$_SESSION['s']['language'].'_dataloghistory_view.lng');
+require('lib/lang/'.$_SESSION['s']['language'].'_dataloghistory_view.lng');
+$app->tpl->setvar($wb);
+
+$id = intval($_GET['id']);
+
+$record = $app->db->queryOneRecord('SELECT * FROM sys_datalog WHERE datalog_id = ?', $id);
+
+$out['id'] = $id;
+
+$out['timestamp'] = date($app->lng('conf_format_datetime'), $record['tstamp']);
+$out['table'] = $record['dbtable'];
+
+$out['action_char'] = $record['action'];
+$out['action_name'] = $app->lng($record['action']);
+
+$out['session_id'] = $record['session_id'];
+
+if(!$data = unserialize(stripslashes($record['data']))) {
+ $data = unserialize($record['data']);
+}
+
+switch ($record['action']) {
+ case 'i':
+ $inserts = array();
+ foreach ($data['new'] as $key=>$value) {
+ $inserts[] = array(
+ 'key' => $key,
+ 'value' => nl2br($value),
+ );
+ }
+ $app->tpl->setLoop('inserts', $inserts);
+ break;
+ case 'u':
+ $updates = array();
+ foreach ($data['new'] as $key=>$value) {
+ if ($value != $data['old'][$key]) {
+ $old = $data['old'][$key];
+ $new = $value;
+ $changes = show_diff_if_needed($old, $new);
+ $updates[] = array(
+ 'key' => $key,
+ 'is_diff' => $changes['is_diff'],
+ 'old' => nl2br($changes['old']),
+ 'new' => nl2br($changes['new']),
+ 'diff' => nl2br($changes['diff']),
+ );
+ }
+ }
+ if (count($updates) > 0) {
+ $app->tpl->setLoop('updates', $updates);
+ } else {
+ $out['no_changes'] = true;
+ }
+ break;
+ case 'd':
+ $deletes = array();
+ foreach ($data['old'] as $key=>$value) {
+ $deletes[] = array(
+ 'key' => $key,
+ 'value' => nl2br($value),
+ );
+ }
+ $app->tpl->setLoop('deletes', $deletes);
+ break;
+}
+
+$app->tpl->setVar($out);
+
+$app->tpl_defaults();
+$app->tpl->pparse();
+
+function show_diff_if_needed($old, $new) {
+ global $app;
+
+ $diff_min_lines = 6;
+
+ if (substr_count($old, "\n") >= $diff_min_lines || substr_count($new, "\n") >= $diff_min_lines) {
+ $opcodes = FineDiff::getDiffOpcodes($old, $new);
+ $html = FineDiff::renderUTF8DiffToHTMLFromOpcodes($old, $opcodes);
+ return array('is_diff'=>true, 'old'=>'', 'new'=>'', 'diff'=>$html);
+ } else {
+ return array('is_diff'=>false, 'old'=>$old, 'new'=>$new, 'diff'=>'');
+ }
+}
+
+?>
diff --git a/interface/web/monitor/lib/lang/de.lng b/interface/web/monitor/lib/lang/de.lng
index 8e70741ef97864b85b05417f6a966bbaf328c741..33998b5ed8127a185f6311ee5c670b8fbdc5ef06 100644
--- a/interface/web/monitor/lib/lang/de.lng
+++ b/interface/web/monitor/lib/lang/de.lng
@@ -40,6 +40,7 @@ $wb['Show Clamav-Log'] = 'ClamAV Protokoll anzeigen';
$wb['Show ISPConfig-Log'] = 'ISPConfig Protokoll anzeigen';
$wb['Show RKHunter-Log'] = 'RKHunter Protokoll anzeigen';
$wb['Show Jobqueue'] = 'Jobwarteschlange anzeigen';
+$wb['Show Data Log History'] = 'Datalog-History anzeigen';
$wb['Show ISPC Cron-Log'] = 'Cron Protokoll anzeigen';
$wb['no_data_updates_txt'] = 'Derzeit stehen keine Daten über Updates zur Verfügung. Bitte später erneut überprüfen.';
$wb['no_data_raid_txt'] = 'Derzeit stehen keine Daten über RAID zur Verfügung. Bitte später erneut überprüfen.';
diff --git a/interface/web/monitor/lib/lang/de_dataloghistory_list.lng b/interface/web/monitor/lib/lang/de_dataloghistory_list.lng
new file mode 100644
index 0000000000000000000000000000000000000000..2c2b6c9fc2d41c11f5d37d11d691140229718b7a
--- /dev/null
+++ b/interface/web/monitor/lib/lang/de_dataloghistory_list.lng
@@ -0,0 +1,8 @@
+
diff --git a/interface/web/monitor/lib/lang/de_dataloghistory_undo.lng b/interface/web/monitor/lib/lang/de_dataloghistory_undo.lng
new file mode 100644
index 0000000000000000000000000000000000000000..c47a6c658580fa14880a6f6dddef86c6494a335f
--- /dev/null
+++ b/interface/web/monitor/lib/lang/de_dataloghistory_undo.lng
@@ -0,0 +1,6 @@
+
diff --git a/interface/web/monitor/lib/lang/de_dataloghistory_view.lng b/interface/web/monitor/lib/lang/de_dataloghistory_view.lng
new file mode 100644
index 0000000000000000000000000000000000000000..e91d341ca67d84022356e2fb47ac2a9a1207b23d
--- /dev/null
+++ b/interface/web/monitor/lib/lang/de_dataloghistory_view.lng
@@ -0,0 +1,26 @@
+
diff --git a/interface/web/monitor/lib/lang/en.lng b/interface/web/monitor/lib/lang/en.lng
index 3721f692e735f35911c42873525b18bdf8d8dbba..baa0252ceac6e4fcf357c65a7f8b66e647daf687 100644
--- a/interface/web/monitor/lib/lang/en.lng
+++ b/interface/web/monitor/lib/lang/en.lng
@@ -46,6 +46,7 @@ $wb['Show Clamav-Log'] = 'Show Clamav-Log';
$wb['Show ISPConfig-Log'] = 'Show ISPConfig-Log';
$wb['Show RKHunter-Log'] = 'Show RKHunter-Log';
$wb['Show Jobqueue'] = 'Show Jobqueue';
+$wb['Show Data Log History'] = 'Show Data Log History';
$wb['Show fail2ban-Log'] = 'Show fail2ban-Log';
$wb['Show MongoDB-Log'] = 'Show MongoDB-Log';
$wb['Show IPTables'] = 'Show IPTables';
diff --git a/interface/web/monitor/lib/lang/en_dataloghistory_list.lng b/interface/web/monitor/lib/lang/en_dataloghistory_list.lng
new file mode 100644
index 0000000000000000000000000000000000000000..9f9afd6347ba4443cc46796810fb1c4a24106ccf
--- /dev/null
+++ b/interface/web/monitor/lib/lang/en_dataloghistory_list.lng
@@ -0,0 +1,8 @@
+
diff --git a/interface/web/monitor/lib/lang/en_dataloghistory_undo.lng b/interface/web/monitor/lib/lang/en_dataloghistory_undo.lng
new file mode 100644
index 0000000000000000000000000000000000000000..258d8866966c781d0302373230bd7e5b2f0391b9
--- /dev/null
+++ b/interface/web/monitor/lib/lang/en_dataloghistory_undo.lng
@@ -0,0 +1,6 @@
+
diff --git a/interface/web/monitor/lib/lang/en_dataloghistory_view.lng b/interface/web/monitor/lib/lang/en_dataloghistory_view.lng
new file mode 100644
index 0000000000000000000000000000000000000000..df9ddd286f46e816e06132e7465929ab8dd87229
--- /dev/null
+++ b/interface/web/monitor/lib/lang/en_dataloghistory_view.lng
@@ -0,0 +1,26 @@
+
diff --git a/interface/web/monitor/lib/module.conf.php b/interface/web/monitor/lib/module.conf.php
index 1f40d7b4d3b31fbbb33c6d01e710de6d722ec76c..0bba401ade6d17eb57e522e598ad7df40e75c94d 100644
--- a/interface/web/monitor/lib/module.conf.php
+++ b/interface/web/monitor/lib/module.conf.php
@@ -26,6 +26,11 @@ $items[] = array( 'title' => 'Show Jobqueue',
'link' => 'monitor/datalog_list.php',
'html_id' => 'jobqueue');
+$items[] = array( 'title' => 'Show Data Log History',
+ 'target' => 'content',
+ 'link' => 'monitor/dataloghistory_list.php',
+ 'html_id' => 'dataloghistory');
+
$module["nav"][] = array( 'title' => 'System State (All Servers)',
'open' => 1,
'items' => $items);
diff --git a/interface/web/monitor/list/dataloghistory.list.php b/interface/web/monitor/list/dataloghistory.list.php
new file mode 100644
index 0000000000000000000000000000000000000000..1757125e8d2b33690f4bf7522513ca5c0eb3c653
--- /dev/null
+++ b/interface/web/monitor/list/dataloghistory.list.php
@@ -0,0 +1,91 @@
+ "tstamp",
+ 'datatype' => "DATETIME",
+ 'formtype' => "TEXT",
+ 'op' => "like",
+ 'prefix' => "",
+ 'suffix' => "",
+ 'width' => "",
+ 'value' => "");
+
+
+$liste['item'][] = array( 'field' => 'server_id',
+ 'datatype' => 'INTEGER',
+ 'formtype' => 'SELECT',
+ 'op' => '=',
+ 'prefix' => '',
+ 'suffix' => '',
+ 'datasource' => array ( 'type' => 'SQL',
+ 'querystring' => 'SELECT server_id,server_name FROM server WHERE {AUTHSQL} ORDER BY server_name',
+ 'keyfield'=> 'server_id',
+ 'valuefield'=> 'server_name'
+ ),
+ 'width' => '',
+ 'value' => '');
+
+$liste["item"][] = array( 'field' => "action",
+ 'datatype' => "VARCHAR",
+ 'formtype' => "SELECT",
+ 'op' => "=",
+ 'prefix' => "",
+ 'suffix' => "",
+ 'width' => "",
+ 'value' => array('i' => "Insert", 'u' => "Update", 'd' => "Delete"));
+
+
+$liste["item"][] = array( 'field' => "dbtable",
+ 'datatype' => "VARCHAR",
+ 'formtype' => "TEXT",
+ 'op' => "like",
+ 'prefix' => "%",
+ 'suffix' => "%",
+ 'width' => "",
+ 'value' => "");
+
+
+?>
diff --git a/interface/web/monitor/templates/dataloghistory_list.htm b/interface/web/monitor/templates/dataloghistory_list.htm
new file mode 100644
index 0000000000000000000000000000000000000000..0cd3c4f4d27f52fa40977b3e00b80bf0c65cfbb4
--- /dev/null
+++ b/interface/web/monitor/templates/dataloghistory_list.htm
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/interface/web/monitor/templates/dataloghistory_undo.htm b/interface/web/monitor/templates/dataloghistory_undo.htm
new file mode 100644
index 0000000000000000000000000000000000000000..997231a885ad3fb3e412658fabdd5ce7a62712de
--- /dev/null
+++ b/interface/web/monitor/templates/dataloghistory_undo.htm
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/interface/web/monitor/templates/dataloghistory_view.htm b/interface/web/monitor/templates/dataloghistory_view.htm
new file mode 100644
index 0000000000000000000000000000000000000000..0ce595e2652c90709d649bc0881210ca3856d5e0
--- /dev/null
+++ b/interface/web/monitor/templates/dataloghistory_view.htm
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
|
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+
+
+ |
+
+ ( / )
+
+ |
+
+
+
+ |
+ |
+ |
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
diff --git a/interface/web/themes/default/assets/stylesheets/ispconfig.css b/interface/web/themes/default/assets/stylesheets/ispconfig.css
index 049322f29c9ac346bda1246f79e1399ea53d3e4d..449c05cdbc1b334a33fe900571f9a257687467bd 100644
--- a/interface/web/themes/default/assets/stylesheets/ispconfig.css
+++ b/interface/web/themes/default/assets/stylesheets/ispconfig.css
@@ -809,3 +809,16 @@ span.pbvaluemargin {
.ip_suggestion_server {
font-weight: bold;
}
+.finediff {
+ font-family: monospace;
+}
+.finediff ins {
+ color: green;
+ background: #dfd;
+ text-decoration: none;
+}
+.finediff del {
+ color: red;
+ background: #fdd;
+ text-decoration: none;
+}
\ No newline at end of file