mirror of
https://gitlab.easter-eggs.com/ee/ldapsaisie.git
synced 2024-11-22 09:59:06 +01:00
LSaddon accesslog: global improvments and add self logging feature
This commit is contained in:
parent
80a50f98f1
commit
13d83dbf75
4 changed files with 286 additions and 69 deletions
|
@ -23,3 +23,14 @@
|
||||||
// Accesslog base DN
|
// Accesslog base DN
|
||||||
define('LS_ACCESSLOG_BASEDN', 'cn=ldapsaisie-accesslog');
|
define('LS_ACCESSLOG_BASEDN', 'cn=ldapsaisie-accesslog');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable logging write events on LDAP entries managed by LdapSaisie in the accesslog base
|
||||||
|
*
|
||||||
|
* The feature permit to LdapSaisie to write it self log entries in accesslog base about
|
||||||
|
* modifications made with its system account with the right author DN.
|
||||||
|
*
|
||||||
|
* Note: If your LDAP directory ACL permit to your users to do them self modifications on LDAP
|
||||||
|
* directory, the recommanded way is to use authz proxy authentication (see useAuthzProxyControl
|
||||||
|
* configuration parameter).
|
||||||
|
*/
|
||||||
|
define('LS_ACCESSLOG_LOG_WRITE_EVENTS', false);
|
||||||
|
|
36
src/css/default/showObjectAccessLogs.css
Normal file
36
src/css/default/showObjectAccessLogs.css
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
table.objectAccessLogs tbody tr {
|
||||||
|
border-bottom: 1px dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.objectAccessLogs table.mods {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.objectAccessLogs table.mods td, table.objectAccessLogs table.mods th {
|
||||||
|
border-left: 1px dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.objectAccessLogs table.mods tr {
|
||||||
|
border: none!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.objectAccessLogs .col-attr {
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.objectAccessLogs .col-op {
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.objectAccessLogs .col-value {
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.objectAccessLogs .col-value div {
|
||||||
|
overflow: scroll;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
|
@ -25,29 +25,36 @@ LSerror :: defineError('ACCESSLOG_SUPPORT_01',
|
||||||
);
|
);
|
||||||
|
|
||||||
$GLOBALS['accesslog_reqTypes'] = array(
|
$GLOBALS['accesslog_reqTypes'] = array(
|
||||||
'add' => _('Add'),
|
'add' => ___('Add'),
|
||||||
'bind' => _('Log in'),
|
'bind' => ___('Log in'),
|
||||||
'compare' => _('Compare'),
|
'compare' => ___('Compare'),
|
||||||
'delete' => _('Delete'),
|
'delete' => ___('Delete'),
|
||||||
'extended' => _('Extended'),
|
'extended' => ___('Extended'),
|
||||||
'modify' => _('Modify'),
|
'modify' => ___('Modify'),
|
||||||
'modrdn' => _('Modify RDN'),
|
'modrdn' => ___('Modify RDN'),
|
||||||
'search' => _('Search'),
|
'search' => ___('Search'),
|
||||||
'unbind' => _('Log out'),
|
'unbind' => ___('Log out'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$GLOBALS['accesslog_modOps'] = array(
|
$GLOBALS['accesslog_modOps'] = array(
|
||||||
'+' => _('Add'),
|
'+' => ___('Add'),
|
||||||
'-' => _('Delete'),
|
'-' => ___('Delete'),
|
||||||
'=' => _('Replace'),
|
'=' => ___('Replace'),
|
||||||
'' => _('Replace'),
|
'' => ___('Replace'),
|
||||||
'#' => _('Increment'),
|
'#' => ___('Increment'),
|
||||||
);
|
);
|
||||||
|
|
||||||
function LSaddon_accesslog_support() {
|
function LSaddon_accesslog_support() {
|
||||||
if (!defined('LS_ACCESSLOG_BASEDN')) {
|
$MUST_DEFINE_CONST= array(
|
||||||
LSerror :: addErrorCode('ACCESSLOG_SUPPORT_01', 'LS_ACCESSLOG_BASEDN');
|
'LS_ACCESSLOG_BASEDN',
|
||||||
return false;
|
'LS_ACCESSLOG_LOG_WRITE_EVENTS',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach($MUST_DEFINE_CONST as $const) {
|
||||||
|
if (!defined($const) || is_empty(constant($const))) {
|
||||||
|
LSerror :: addErrorCode('ACCESSLOG_SUPPORT_01', $const);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (php_sapi_name() === 'cli') {
|
if (php_sapi_name() === 'cli') {
|
||||||
LScli::add_command(
|
LScli::add_command(
|
||||||
|
@ -57,10 +64,18 @@ function LSaddon_accesslog_support() {
|
||||||
'[entry DN] [page]'
|
'[entry DN] [page]'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
elseif (LS_ACCESSLOG_LOG_WRITE_EVENTS && LSsession :: loadLSclass('LSldap')) {
|
||||||
|
LSldap :: addEvent('updated', 'onEntryUpdated');
|
||||||
|
LSldap :: addEvent('moved', 'onEntryMoved');
|
||||||
|
LSldap :: addEvent('user_password_updated', 'onEntryUserPasswordUpdated');
|
||||||
|
LSldap :: addEvent('deleted', 'onEntryDeleted');
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapAccessLogEntry(&$entry) {
|
function mapAccessLogEntry(&$entry) {
|
||||||
|
foreach($entry['attrs'] as $attr => $values)
|
||||||
|
$entry['attrs'][$attr] = ensureIsArray($values);
|
||||||
$attrs = $entry['attrs'];
|
$attrs = $entry['attrs'];
|
||||||
$entry['start'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqStart'));
|
$entry['start'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqStart'));
|
||||||
$entry['end'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqEnd'));
|
$entry['end'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqEnd'));
|
||||||
|
@ -73,49 +88,67 @@ function mapAccessLogEntry(&$entry) {
|
||||||
$entry['type'] = LSldap::getAttr($attrs, 'reqType');
|
$entry['type'] = LSldap::getAttr($attrs, 'reqType');
|
||||||
$entry['result'] = ldap_err2str(LSldap::getAttr($attrs, 'reqResult'));
|
$entry['result'] = ldap_err2str(LSldap::getAttr($attrs, 'reqResult'));
|
||||||
$entry['message'] = LSldap::getAttr($attrs, 'reqMessage');
|
$entry['message'] = LSldap::getAttr($attrs, 'reqMessage');
|
||||||
if ($entry['type'] === 'modify' && LSldap::getAttr($attrs, 'reqMod', true)) {
|
$mods = array();
|
||||||
$mods = array();
|
foreach(LSldap::getAttr($attrs, 'reqMod', true) as $mod) {
|
||||||
foreach(LSldap::getAttr($attrs, 'reqMod', true) as $mod) {
|
if (preg_match('/^(?P<attr>[^\:]+)\:(?P<op>[^ ]?)( (?P<value>.*))?$/', $mod, $m)) {
|
||||||
if (preg_match('/^([^\:]+)\:([^ ]?) (.*)$/', $mod, $m)) {
|
$attr = $m['attr'];
|
||||||
$attr = $m[1];
|
$op = $m['op'];
|
||||||
$op = $m[2];
|
$value = isset($m['value'])?$m['value']:null;
|
||||||
$value = $m[3];
|
if (!array_key_exists($attr, $mods)) {
|
||||||
if (!array_key_exists($attr, $mods)) {
|
$mods[$attr] = array(
|
||||||
$mods[$attr] = array(
|
'changes' => array(),
|
||||||
'mods' => array(),
|
'old_values' => array(),
|
||||||
'old_values' => array(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$mods[$attr]['changes'][] = array(
|
|
||||||
'op' => array_key_exists($op, $GLOBALS['accesslog_modOps']) ? $GLOBALS['accesslog_modOps'][$op] : $op,
|
|
||||||
'value' => $value,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
$mods[$attr]['changes'][] = array(
|
||||||
|
'op' => (
|
||||||
|
array_key_exists($op, $GLOBALS['accesslog_modOps'])?
|
||||||
|
_($GLOBALS['accesslog_modOps'][$op]): $op
|
||||||
|
),
|
||||||
|
'value' => $value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (LSldap::getAttr($attrs, 'reqOld', true)) {
|
}
|
||||||
foreach(LSldap::getAttr($attrs, 'reqOld', true) as $old) {
|
if (LSldap::getAttr($attrs, 'reqOld', true)) {
|
||||||
if (preg_match('/^([^\:]+)\: (.*)$/', $old, $m) && array_key_exists($m[1], $mods)) {
|
foreach(LSldap::getAttr($attrs, 'reqOld', true) as $old) {
|
||||||
$mods[$m[1]]['old_values'][] = $m[2];
|
if (preg_match('/^([^\:]+)\: (.*)$/', $old, $m) && array_key_exists($m[1], $mods)) {
|
||||||
}
|
$mods[$m[1]]['old_values'][] = $m[2];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if ($mods)
|
||||||
$entry['mods'] = $mods;
|
$entry['mods'] = $mods;
|
||||||
|
if ($entry['type'] === 'modrdn') {
|
||||||
|
$new_rdn = LSldap::getAttr($attrs, 'reqNewRDN', false);
|
||||||
|
$superior_dn = LSldap::getAttr($attrs, 'reqNewSuperior', false);
|
||||||
|
if (!$superior_dn) {
|
||||||
|
$superior_dn = parentDn(LSldap::getAttr($attrs, 'reqDN', false));
|
||||||
|
}
|
||||||
|
$entry['new_dn'] = "$new_rdn,$superior_dn";
|
||||||
}
|
}
|
||||||
if (array_key_exists($entry['type'], $GLOBALS['accesslog_reqTypes'])) {
|
if (array_key_exists($entry['type'], $GLOBALS['accesslog_reqTypes'])) {
|
||||||
$entry['type'] = $GLOBALS['accesslog_reqTypes'][$entry['type']];
|
$entry['type'] = _($GLOBALS['accesslog_reqTypes'][$entry['type']]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortLogEntryByDate($a, $b) {
|
function sortLogEntriesByDate(&$a, &$b) {
|
||||||
return ($a['start'] === $b['start']) ? 0 : ($a['start'] < $b['start']) ? -1 : 1;
|
$astart = LSldap::getAttr($a['attrs'], 'reqStart');
|
||||||
|
$bstart = LSldap::getAttr($b['attrs'], 'reqStart');
|
||||||
|
return ($astart === $bstart) ? 0 : ($astart < $bstart) ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntryAccessLog($dn) {
|
function getEntryAccessLog($dn, $start_date=null) {
|
||||||
$data = LSldap::search(
|
$filter = Net_LDAP2_Filter::create('reqDn', 'equals', $dn);
|
||||||
Net_LDAP2_Filter::create('reqDn', 'equals', $dn),
|
if ($start_date) {
|
||||||
|
$date_filter = Net_LDAP2_Filter::create('reqStart', 'greaterOrEqual', $start_date);
|
||||||
|
$filter = Net_LDAP2_Filter::combine('and', array($filter, $date_filter));
|
||||||
|
}
|
||||||
|
$entries = LSldap::search(
|
||||||
|
$filter,
|
||||||
LS_ACCESSLOG_BASEDN,
|
LS_ACCESSLOG_BASEDN,
|
||||||
array(
|
array(
|
||||||
'attributes' => array(
|
'attributes' => array(
|
||||||
|
'reqDN',
|
||||||
'reqStart',
|
'reqStart',
|
||||||
'reqEnd',
|
'reqEnd',
|
||||||
'reqAuthzID',
|
'reqAuthzID',
|
||||||
|
@ -124,22 +157,33 @@ function getEntryAccessLog($dn) {
|
||||||
'reqMessage',
|
'reqMessage',
|
||||||
'reqMod',
|
'reqMod',
|
||||||
'reqOld',
|
'reqOld',
|
||||||
|
'reqNewRDN',
|
||||||
|
'reqNewSuperior',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (!is_array($data)) {
|
if (!is_array($entries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
usort($entries, 'sortLogEntriesByDate');
|
||||||
$logs = array();
|
$logs = array();
|
||||||
foreach($data as $entry) {
|
$new_dn = null;
|
||||||
foreach($entry['attrs'] as $attr => $values) {
|
$rename_date = null;
|
||||||
$entry['attrs'][$attr] = ensureIsArray($values);
|
foreach($entries as $entry) {
|
||||||
}
|
|
||||||
mapAccessLogEntry($entry);
|
mapAccessLogEntry($entry);
|
||||||
$logs[] = $entry;
|
$logs[] = $entry;
|
||||||
|
if (isset($entry['new_dn'])) {
|
||||||
|
$new_dn = $entry['new_dn'];
|
||||||
|
$rename_date = LSldap::formatDate($entry['start']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
usort($logs, 'sortLogEntryByDate');
|
if ($new_dn) {
|
||||||
return array_reverse($logs);
|
$next_logs = getEntryAccessLog($new_dn, $rename_date);
|
||||||
|
if (is_array($next_logs))
|
||||||
|
$logs = array_merge($logs, $next_logs);
|
||||||
|
}
|
||||||
|
return $start_date?$logs:array_reverse($logs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntryAccessLogPage($dn, $page = false, $nbByPage = 30) {
|
function getEntryAccessLogPage($dn, $page = false, $nbByPage = 30) {
|
||||||
|
@ -183,11 +227,136 @@ function showObjectAccessLogs($obj) {
|
||||||
'action' => 'view',
|
'action' => 'view',
|
||||||
);
|
);
|
||||||
LStemplate::assign('LSview_actions', $LSview_actions);
|
LStemplate::assign('LSview_actions', $LSview_actions);
|
||||||
|
LStemplate::addCSSFile('showObjectAccessLogs.css');
|
||||||
LSsession::setTemplate('showObjectAccessLogs.tpl');
|
LSsession::setTemplate('showObjectAccessLogs.tpl');
|
||||||
LSsession::displayTemplate();
|
LSsession::displayTemplate();
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEntryUpdated($data) {
|
||||||
|
$now = LSldap::formatDate();
|
||||||
|
$dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN;
|
||||||
|
$new_entry = $data['original_entry']->isNew();
|
||||||
|
$attrs = array(
|
||||||
|
'reqStart' => array($now),
|
||||||
|
'reqEnd' => array($now),
|
||||||
|
'reqType' => array($new_entry?"add":"modify"),
|
||||||
|
'reqSession' => array("1024"),
|
||||||
|
'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')),
|
||||||
|
'reqDN' => array($data["dn"]),
|
||||||
|
'reqResult' => array("0"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compute modifications
|
||||||
|
$mods = array();
|
||||||
|
$olds = array();
|
||||||
|
if ($new_entry)
|
||||||
|
foreach(ensureIsArray($data['entry']->getValue('objectClass', 'all')) as $value)
|
||||||
|
$mods[] = "objectClass:+ $value";
|
||||||
|
foreach($data['changes'] as $attr => $values) {
|
||||||
|
if (strtolower($attr) == 'userpassword')
|
||||||
|
foreach(array_keys($values) as $idx)
|
||||||
|
$values[$idx] = hashPasswordForLogs($values[$idx]);
|
||||||
|
if ($values) {
|
||||||
|
foreach($values as $value)
|
||||||
|
$mods[] = $new_entry?"$attr:+ $value":"$attr:= $value";
|
||||||
|
}
|
||||||
|
else if (!$new_entry) {
|
||||||
|
$mods[] = "$attr:=";
|
||||||
|
}
|
||||||
|
if (!$new_entry)
|
||||||
|
foreach(ensureIsArray($data['original_entry']->getValue($attr, 'all')) as $value)
|
||||||
|
$olds[] = "$attr: $value";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$mods) return true;
|
||||||
|
$attrs['reqMod'] = $mods;
|
||||||
|
if ($olds)
|
||||||
|
$attrs['reqOld'] = $olds;
|
||||||
|
|
||||||
|
LSldap::getNewEntry($dn, array($new_entry?'auditAdd':'auditModify'), $attrs, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEntryUserPasswordUpdated($data) {
|
||||||
|
$now = LSldap::formatDate();
|
||||||
|
$dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN; $mods = array();
|
||||||
|
|
||||||
|
|
||||||
|
// Compute modifications
|
||||||
|
$mods = array();
|
||||||
|
|
||||||
|
// Retreive fresh entry to retreive hased/stored password
|
||||||
|
$attrs = LSldap :: getAttrs($data['dn'], null, array('userPassword'));
|
||||||
|
$new_passwords = $data["new_passwords"];
|
||||||
|
if ($attrs)
|
||||||
|
$new_passwords = LSldap :: getAttr($attrs, 'userPassword', true);
|
||||||
|
if ($new_passwords) {
|
||||||
|
foreach($new_passwords as $password)
|
||||||
|
$mods[] = "userPassword:= ".hashPasswordForLogs($password);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$mods[] = "userPassword:=";
|
||||||
|
$attrs = array(
|
||||||
|
'reqStart' => array($now),
|
||||||
|
'reqEnd' => array($now),
|
||||||
|
'reqType' => array("modify"),
|
||||||
|
'reqSession' => array("1024"),
|
||||||
|
'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')),
|
||||||
|
'reqDN' => array($data["dn"]),
|
||||||
|
'reqResult' => array("0"),
|
||||||
|
'reqMod' => $mods,
|
||||||
|
);
|
||||||
|
LSldap::getNewEntry($dn, array('auditModify'), $attrs, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEntryMoved($data) {
|
||||||
|
$now = LSldap::formatDate();
|
||||||
|
$dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN;
|
||||||
|
$attrs = array(
|
||||||
|
'reqStart' => array($now),
|
||||||
|
'reqEnd' => array($now),
|
||||||
|
'reqType' => array("modrdn"),
|
||||||
|
'reqSession' => array("1024"),
|
||||||
|
'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')),
|
||||||
|
'reqDN' => array($data["old"]),
|
||||||
|
'reqNewRDN' => array(getRdn($data["new"])),
|
||||||
|
'reqResult' => array("0"),
|
||||||
|
);
|
||||||
|
$new_superior = parentDn($data["new"]);
|
||||||
|
$old_superior = parentDn($data["old"]);
|
||||||
|
if ($new_superior != $old_superior)
|
||||||
|
$attrs['reqNewSuperior'] = array($new_superior);
|
||||||
|
LSldap::getNewEntry($dn, array('auditModRDN'), $attrs, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEntryDeleted($data) {
|
||||||
|
$now = LSldap::formatDate();
|
||||||
|
$dn = "reqStart=$now,".LS_ACCESSLOG_BASEDN;
|
||||||
|
$attrs = array(
|
||||||
|
'reqStart' => array($now),
|
||||||
|
'reqEnd' => array($now),
|
||||||
|
'reqType' => array("delete"),
|
||||||
|
'reqSession' => array("1024"),
|
||||||
|
'reqAuthzID' => array(LSsession :: get('authenticated_user_dn')),
|
||||||
|
'reqDN' => array($data["dn"]),
|
||||||
|
'reqResult' => array("0"),
|
||||||
|
);
|
||||||
|
LSldap::getNewEntry($dn, array('auditDelete'), $attrs, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashPasswordForLogs($password) {
|
||||||
|
if (preg_match('/^{[^}]+}.*/', $password))
|
||||||
|
// Already hashed
|
||||||
|
return $password;
|
||||||
|
if(defined('PASSWORD_ARGON2I'))
|
||||||
|
return '{ARGON2}'.password_hash($password, PASSWORD_ARGON2I);
|
||||||
|
if(defined('MHASH_SHA512') && function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
|
||||||
|
mt_srand( (double) microtime() * 1000000 );
|
||||||
|
$salt = mhash_keygen_s2k(MHASH_SHA512, $password, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 );
|
||||||
|
return "{SSHA512}".base64_encode(mhash($mhash_type, $password.$salt).$salt);
|
||||||
|
}
|
||||||
|
return '[not logged]';
|
||||||
|
}
|
||||||
if (php_sapi_name() !== 'cli') {
|
if (php_sapi_name() !== 'cli') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<h1>{$pagetitle}</h1>
|
<h1>{$pagetitle}</h1>
|
||||||
{include file='ls:LSview_actions.tpl'}
|
{include file='ls:LSview_actions.tpl'}
|
||||||
|
|
||||||
<table class='LStable'>
|
<table class='LStable objectAccessLogs'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{tr msg="Date"}</th>
|
<th>{tr msg="Date"}</th>
|
||||||
|
@ -22,23 +22,23 @@
|
||||||
<td class="center">{$log.result}{if $log.message} <img class='LStips' src="{img name='help'}" alt="?" title='{$log.message|escape:quotes}'/>{/if}</td>
|
<td class="center">{$log.result}{if $log.message} <img class='LStips' src="{img name='help'}" alt="?" title='{$log.message|escape:quotes}'/>{/if}</td>
|
||||||
<td>
|
<td>
|
||||||
{if $log.mods}
|
{if $log.mods}
|
||||||
<table style='margin: auto'>
|
<table class="mods">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{tr msg="Attribute"}</th>
|
<th class="col-op">{tr msg="Operation"}</th>
|
||||||
<th>{tr msg="Operation"}</th>
|
<th class="col-attr">{tr msg="Attribute"}</th>
|
||||||
<th>{tr msg="Value"}</th>
|
<th class="col-value">{tr msg="Old value(s)"}</th>
|
||||||
<th>{tr msg="Old value(s)"}</th>
|
<th class="col-value">{tr msg="Value"}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{foreach $log.mods as $attr => $info}
|
{foreach $log.mods as $attr => $info}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="center" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>{$attr}</td>
|
<td class="col-op center" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>{$info.changes.0.op|escape:htmlall}</td>
|
||||||
<td class="center">{$info.changes.0.op|escape:htmlall}</td>
|
<td class="col-attr center" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>{$attr}</td>
|
||||||
<td>{$info.changes.0.value|escape:htmlall}</td>
|
<td class="col-value" {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>
|
||||||
<td {if count($info.changes)>1}rowspan={$info.changes|count}{/if}>
|
|
||||||
{if $info.old_values}
|
{if $info.old_values}
|
||||||
|
<div class="copyable copyable-no-btn">
|
||||||
{if count($info.old_values) == 1}
|
{if count($info.old_values) == 1}
|
||||||
{$info.old_values[0]|escape:'htmlall'}
|
{$info.old_values[0]|escape:'htmlall'}
|
||||||
{else}
|
{else}
|
||||||
|
@ -48,17 +48,18 @@
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="col-value"><div class="copyable copyable-no-btn">{$info.changes.0.value|escape:htmlall}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
{if count($info.changes) > 1}
|
{if count($info.changes) > 1}
|
||||||
{section name=change loop=$info.changes step=1 start=1}
|
{section name=change loop=$info.changes step=1 start=1}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{$info.changes[change].op|escape:htmlall}</td>
|
<td class="col-value"><div class="copyable copyable-no-btn">{$info.changes[change].value|escape:htmlall}</div></td>
|
||||||
<td>{$info.changes[change].value|escape:htmlall}</td>
|
</tr>
|
||||||
</tr>
|
{/section}
|
||||||
{/section}
|
{/if}
|
||||||
{/if}
|
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
Loading…
Reference in a new issue