<?php
/*******************************************************************************
 * Copyright (C) 2022 Easter-eggs
 * http://ldapsaisie.labs.libre-entreprise.org
 *
 * Author: See AUTHORS file in top-level directory.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

******************************************************************************/

LSerror :: defineError('ACCESSLOG_SUPPORT_01',
  ___("accesslog Support : The constant %{const} is not defined.")
);

$GLOBALS['accesslog_reqTypes'] = array(
  'add' => ___('Add'),
  'bind' => ___('Log in'),
  'compare' => ___('Compare'),
  'delete' => ___('Delete'),
  'extended' => ___('Extended'),
  'modify' => ___('Modify'),
  'modrdn' => ___('Modify RDN'),
  'search' => ___('Search'),
  'unbind' => ___('Log out'),
);

$GLOBALS['accesslog_modOps'] = array(
  '+' => ___('Add'),
  '-' => ___('Delete'),
  '=' => ___('Replace'),
  '' => ___('Replace'),
  '#' => ___('Increment'),
);

function LSaddon_accesslog_support() {
  $MUST_DEFINE_CONST= array(
    'LS_ACCESSLOG_BASEDN',
    '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') {
    LScli::add_command(
      'getEntryAccessLog',
      'cli_getEntryAccessLog',
      'Get entry access log',
      '[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;
}

function mapAccessLogEntry(&$entry) {
  foreach($entry['attrs'] as $attr => $values)
    $entry['attrs'][$attr] = ensureIsArray($values);
  $attrs = $entry['attrs'];
  $entry['start'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqStart'));
  $entry['end'] = LSldap::parseDate(LSldap::getAttr($attrs, 'reqEnd'));
  $entry['author_dn'] = LSldap::getAttr($attrs, 'reqAuthzID');
  $entry['author_rdn'] = (
    $entry['author_dn']?
    explode('=', explode(',', $entry['author_dn'])[0])[1]:
    null
  );
  $entry['type'] = LSldap::getAttr($attrs, 'reqType');
  $entry['result'] = ldap_err2str(LSldap::getAttr($attrs, 'reqResult'));
  $entry['message'] = LSldap::getAttr($attrs, 'reqMessage');
  $mods = array();
  foreach(LSldap::getAttr($attrs, 'reqMod', true) as $mod) {
    if (preg_match('/^(?P<attr>[^\:]+)\:(?P<op>[^ ]?)( (?P<value>.*))?$/', $mod, $m)) {
      $attr = $m['attr'];
      $value = isset($m['value'])?$m['value']:null;
      if (!array_key_exists($attr, $mods)) {
        $mods[$attr] = array(
          'changes' => array(),
          'old_values' => array(),
        );
      }
      $op = (
        array_key_exists($m['op'], $GLOBALS['accesslog_modOps'])?
        _($GLOBALS['accesslog_modOps'][$m['op']]): $m['op']
      );
      if (!array_key_exists($op, $mods[$attr]['changes']))
        $mods[$attr]['changes'][$op] = array();
      $mods[$attr]['changes'][$op][] = $value;
    }
  }
  if (LSldap::getAttr($attrs, 'reqOld', true)) {
    foreach(LSldap::getAttr($attrs, 'reqOld', true) as $old) {
      if (preg_match('/^([^\:]+)\: (.*)$/', $old, $m) && array_key_exists($m[1], $mods)) {
        $mods[$m[1]]['old_values'][] = $m[2];
      }
    }
  }
  if ($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'])) {
    $entry['type'] = _($GLOBALS['accesslog_reqTypes'][$entry['type']]);
  }
}

function sortLogEntriesByDate(&$a, &$b) {
  $astart = LSldap::getAttr($a['attrs'], 'reqStart');
  $bstart = LSldap::getAttr($b['attrs'], 'reqStart');
  return ($astart === $bstart) ? 0 : ($astart < $bstart) ? -1 : 1;
}

function getEntryAccessLog($dn, $start_date=null, $include_ldapsaisie=true) {
  $filter = 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));
  }
  if (!$include_ldapsaisie) {
    $not_ldapsaisie_filter = Net_LDAP2_Filter::combine('not', array(
      Net_LDAP2_Filter::create(
        'reqAuthzID', 'equals',
        LSconfig::get('ldap_servers.'.LSsession::get('ldap_server_id').'.ldap_config.binddn')
      )
    ));
    $filter = Net_LDAP2_Filter::combine('and', array($filter, $not_ldapsaisie_filter));
  }
  $entries = LSldap::search(
    $filter,
    LS_ACCESSLOG_BASEDN,
    array(
      'attributes' => array(
        'reqDN',
        'reqStart',
        'reqEnd',
        'reqAuthzID',
        'reqType',
        'reqResult',
        'reqMessage',
        'reqMod',
        'reqOld',
        'reqNewRDN',
        'reqNewSuperior',
      ),
    )
  );
  if (!is_array($entries)) {
    return;
  }
  usort($entries, 'sortLogEntriesByDate');
  $logs = array();
  $new_dn = null;
  $rename_date = null;
  foreach($entries as $entry) {
    mapAccessLogEntry($entry);
    $logs[] = $entry;
    if (isset($entry['new_dn']) && $entry['new_dn'] != $dn) {
      $new_dn = $entry['new_dn'];
      $rename_date = LSldap::formatDate($entry['start']);
      break;
    }
  }
  if ($new_dn) {
    $next_logs = getEntryAccessLog($new_dn, $rename_date, $include_ldapsaisie);
    if (is_array($next_logs))
      $logs = array_merge($logs, $next_logs);
  }
  return $start_date?$logs:array_reverse($logs);
}

function getEntryAccessLogPage($dn, $page = false, $refresh=false, $include_ldapsaisie=true, $nbByPage = null) {
  $nbByPage = is_null($nbByPage)?30:intval($nbByPage);
  if (!isset($_SESSION['entryAccessLogPages'])) {
    $_SESSION['entryAccessLogPages'] = array();
  }
  if (!isset($_SESSION['entryAccessLogPages'][$dn]) || $refresh) {
    $_SESSION['entryAccessLogPages'][$dn] = getEntryAccessLog($dn, null, $include_ldapsaisie);
  }
  if (!is_int($page)) {
    $page = 1;
  }
  return array(
    'nb' => $page,
    'nbPages' => ceil(count($_SESSION['entryAccessLogPages'][$dn]) / $nbByPage),
    'logs' => array_slice(
      $_SESSION['entryAccessLogPages'][$dn],
      $page > 1 ? (($page - 1) * $nbByPage) - 1 : 0,
      $nbByPage
    )
  );
}

function showObjectAccessLogs($obj) {
  $refresh = isset($_REQUEST['refresh']);
  $include_ldapsaisie = !LS_ACCESSLOG_LOG_WRITE_EVENTS;
  if (isset($_REQUEST['include_ldapsaisie'])) {
    $include_ldapsaisie = boolval($_REQUEST['include_ldapsaisie']);
    $refresh = true;
  }
  elseif (isset($_SESSION['accesslog_include_ldapsaisie']))
    $include_ldapsaisie = $_SESSION['accesslog_include_ldapsaisie'];
  $_SESSION['accesslog_include_ldapsaisie'] = $include_ldapsaisie;
  $pageNb = isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
  $dn = $obj->getDn();
  $page = getEntryAccessLogPage($dn, $pageNb, $refresh, $include_ldapsaisie);
  if (!is_array($page)) {
    return;
  }
  LStemplate::assign('page', $page);
  $LSview_actions = array();
  $LSview_actions['include_ldapsaisie'] = array (
    'label' => $include_ldapsaisie?_('Hide LdapSaisie modifications'):_('Show LdapSaisie modifications'),
    'url' => 'object/'.$obj->getType().'/'.urlencode($dn).'/customAction/showObjectAccessLogs?include_ldapsaisie='.intval(!$include_ldapsaisie),
    'action' => $include_ldapsaisie?'hide':'view',
  );
  $LSview_actions['refresh'] = array (
    'label' => _('Refresh'),
    'url' => 'object/'.$obj->getType().'/'.urlencode($dn).'/customAction/showObjectAccessLogs?refresh',
    'action' => 'refresh',
  );
  $LSview_actions['return'] = array (
    'label' => _('Go back'),
    'url' => 'object/'.$obj->getType().'/'.urlencode($dn),
    'action' => 'view',
  );
  LStemplate::assign('LSview_actions', $LSview_actions);
  LStemplate::addCSSFile('showObjectAccessLogs.css');
  LSsession::setTemplate('showObjectAccessLogs.tpl');
  LSsession::displayTemplate();
  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_SHA512, $password.$salt).$salt);
  }
  return '[not logged]';
}
if (php_sapi_name() !== 'cli') {
  return true;
}

function cli_getEntryAccessLog($command_args) {
  if (count($command_args) < 1) {
    LSlog::fatal('You must specify entry DN as first parameter');
  }
  $dn = $command_args[0];
  $page = isset($command_args[1]) ? intval($command_args[1]) : 1;
  echo json_encode(
    getEntryAccessLogPage($dn, $page),
    JSON_PRETTY_PRINT
  );
}
