<?php
/**
 * User administration code.
 *
 * @author Jan Dittberner <jan@dittberner.info>
 * @version $Id$
 * @license GPL
 * @package DAVAdmin
 *
 * Copyright (c) 2007 Jan Dittberner
 *
 * This file is part of DAVAdmin.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

/** Include common code. */
include_once('common.inc.php');

/**
 * Gets the names of the given users's groups from the group file.
 *
 * @param string $username user name
 * @return array array of group names
 * @access private
 */
function _getGroupNames($username) {
  $groupdata = file($GLOBALS['davconfig']['group.file']);
  $retval = array();
  foreach ($groupdata as $line) {
    list($group, $users) = explode(":", $line);
    $group = trim($group);
    $users = explode(" ", $users);
    foreach ($users as $user) {
      if (trim($user) == $username) {
        array_push($retval, $group);
      }
    }
  }
  return $retval;
}

/**
 * Gets XML encoded data for a user.
 * 
 * @param int $uid user id
 * @return string XML string
 */
function getUserData($uid) {
  if  (!(is_numeric($uid) && array_key_exists($uid, $GLOBALS['namemap']))) {
    errorAsXml(sprintf(_("Invalid user id %s"), $uid));
  }

  $row = $GLOBALS['namemap'][$uid];
  $groups = _getGroupNames($row['username']);
  $retval = sprintf('<?xml version="1.0" encoding="utf8"?><userdata><uid>%d</uid><username>%s</username><firstname>%s</firstname><lastname>%s</lastname><groups>%s</groups><loggedin>0</loggedin></userdata>',
                    $uid, $row['username'], $row['firstname'],
                    $row['lastname'], implode(", ", $groups));
  header("Content-Type: text/xml; charset=UTF-8");
  return $retval;
}

/**
 * Gets XML encoded data for a deleted user.
 *
 * @param int $uid user id
 * @return string XML string
 */
function getDeletedUserData($uid) {
  if  (!is_numeric($uid)) {
    errorAsXml(sprintf(_("Invalid user id %s"), $uid));
  }
  return sprintf('<?xml version="1.0" encoding="utf8"?><userdata><uid>%d</uid></userdata>', $uid);
}

/**
 * Validates the given user data array for correctness.
 *
 * @param array &$userdata reference to an user data array
 * @param boolean $forinsert if this is true the check skips field
 * that will be created during insert
 * @return array an array with validation error messages or an empty
 * array
 */
function validateUserData(&$userdata, $forinsert) {
  $errormsgs = array();
  // normalize the user data array
  foreach ($userdata as $key => $value) {
    $userdata[$key] = trim($value);
  }
  if (!$forinsert) {
    if (!is_numeric($userdata['uid'])) {
      array_push($errormsgs, _("Uid must be numeric."));
    }
    if (!empty($userdata['password']) && strlen($userdata['password']) < 8) {
      array_push($errormsgs, _("Password must be at least 8 characters long."));
    }
  } else {
    if (!preg_match('/^[a-zA-Z0-9]{2,}$/', $userdata['username'])) {
      array_push($errormsgs, _("Username must be at least 2 characters long and must contain letters and digits only."));
    }
    if (empty($userdata['password']) || strlen($userdata['password']) < 8) {
      array_push($errormsgs, _("Password must be at least 8 characters long."));
    }
  }
  if (empty($userdata['firstname'])) {
    $userdata['firstname'] = null;
  }
  if (empty($userdata['lastname'])) {
    $userdata['lastname'] = null;
  }
  if (!preg_match('/^([0-9a-zA-z]+[,\s]*)+$/', $userdata['groups'])) {
    array_push($errormsgs, _('Groups must be a list of group names separated by commas. Group names must consist of letters and digits.'));
  }
  return $errormsgs;
}

/**
 * Create an entry for a digest authentication file.
 *
 * @param string $username user name
 * @param string $realm realm name
 * @param string $password password
 * @return string digest entry
 */
function createDigest($username, $realm, $password) {
  return sprintf("%s:%s:%s", $username, $realm,
         md5(sprintf("%s:%s:%s", $username, $realm, $password)));
}

/**
 * Update the user to name mapping file.
 */
function updateNameMap() {
  $fh = fopen($GLOBALS['davconfig']['namemap.file'], 'w');
  fwrite($fh, json_encode($GLOBALS['namemap']));
  fclose($fh);
}

/**
 * Update the digest file with the user information.
 *
 * @param &array reference to an associative array of user data
 */
function updateDigest(&$userdata) {
  if ($userdata['password']) {
    $digests = file($GLOBALS['davconfig']['digest.file']);
    $written = false;
    $fh = fopen($GLOBALS['davconfig']['digest.file'], 'w');
    foreach ($digests as $digest) {
      list($username, $realm, $data) = explode(":", $digest);
      if ($username == $userdata['username']
          && $realm == $GLOBALS['davconfig']['dav.realm']) {
        fwrite($fh, createDigest($userdata['username'],
                                 $GLOBALS['davconfig']['dav.realm'],
                                 $userdata['password']) . "\n");
        $written = true;
      } else {
        fwrite($fh, $digest);
      }
    }
    if (!$written) {
      fwrite($fh, createDigest($userdata['username'],
                               $GLOBALS['davconfig']['dav.realm'],
                               $userdata['password']) . "\n");
    }
    fclose($fh);
  }
}

/**
 * Update the group file with the group assignments of the user.
 *
 * @param &array reference to an associative array of user data
 */
function updateGroups(&$userdata) {
  if ($userdata['groups']) {
    $written = array();
    foreach (explode(",", $userdata['groups']) as $group) {
      $written[trim($group)] = false;
    }
    $groupdata = file($GLOBALS['davconfig']['group.file']);
    $fh = fopen($GLOBALS['davconfig']['group.file'], 'w');
    foreach ($groupdata as $groupline) {
      list ($group, $users) = explode(":", $groupline);
      $group = trim($group);
      $users = explode(" ", trim($users));
      foreach ($users as $key => $user) {
        $users[$key] = trim($user);
      }
      if (array_key_exists($group, $written)) {
        if (!in_array($userdata['username'], $users)) {
          array_push($users, $userdata['username']);
        }
        fprintf($fh, "%s: %s\n", $group, implode(" ", $users));
        $written[$group] = true;
      } else {
        fwrite($fh, $groupline);
      }
    }
    foreach ($written as $group => $done) {
      if (!$done) {
        fprintf($fh, "%s: %s\n", $group, $userdata['username']);
      }
    }
    fclose($fh);
  }
}

/**
 * Updates the data of a user in the database.
 *
 * @param array &$userdata reference to a user data array
 */
function updateUser(&$userdata) {
  $validation = validateUserData($userdata, false);
  if (!empty($validation)) {
    errorAsXml(implode("\n", $validation));
  }
  $GLOBALS['namemap'][$userdata['uid']] =
    array('uid' => $userdata['uid'],
          'username' => $userdata['username'],
          'firstname' => $userdata['firstname'],
          'lastname' => $userdata['lastname']);
  // write name mapping data
  updateNameMap();
  // write digest data
  updateDigest($userdata);
  // update group file
  updateGroups($userdata);
}

/**
 * Inserts a new user and its group assignments into the database.
 *
 * @param array &$userdata reference to an user data array
 */
function insertUser(&$userdata) {
  $validation = validateUserData($userdata, true);
  if (!empty($validation)) {
    errorAsXml(implode("\n", $validation));
  }
  $uids = array_keys($GLOBALS['namemap']);
  if (empty($uids)) {
    $uid = 1;
  } else {
    foreach ($uids as $currentuid) {
      $uid = max($uid, $currentuid);
    }
    $uid += 1;
  }
  $GLOBALS['namemap'][$uid] =
    array('uid' => $uid,
          'username' => $userdata['username'],
          'firstname' => $userdata['firstname'],
          'lastname' => $userdata['lastname']);
  // write name mapping data
  updateNameMap();
  // write digest data
  updateDigest($userdata);
  // update group file
  updateGroups($userdata);
  return $uid;
}

/**
 * Remove the digest entries of the given user from the digest file.
 *
 * @param string $username
 */
function removeDigest($username) {
  $digests = file($GLOBALS['davconfig']['digest.file']);
  $fh = fopen($GLOBALS['davconfig']['digest.file'], 'w');
  foreach ($digests as $digest) {
    list($username, $realm, $data) = explode(":", $digest);
    if (!($username == $userdata['username']
          && $realm == $GLOBALS['davconfig']['dav.realm'])) {
        fwrite($fh, $digest);
    }
  }
  fclose($fh);
}

/**
 * Remove the given user from all groups in the group file.
 *
 * @param string $username
 */
function removeFromGroups($username) {
  $groupdata = file($GLOBALS['davconfig']['group.file']);
  $fh = fopen($GLOBALS['davconfig']['group.file'], 'w');
  foreach ($groupdata as $groupline) {
    list ($group, $users) = explode(":", $groupline);
    $group = trim($group);
    $users = explode(" ", trim($users));
    foreach ($users as $key => $user) {
      $users[$key] = trim($user);
    }
    if (in_array($username, $users)) {
      $users = array_splice($users, array_search($username, $users));
    }
    fprintf($fh, "%s: %s\n", $group, implode(" ", $users));
  }
  fclose($fh);
}

/**
 * Delete the user with the given user id and its group assignments
 * from the database.
 *
 * @param int $uid user id
 */
function deleteUser($uid) {
  if  (!(is_numeric($uid) && array_key_exists($uid, $GLOBALS['namemap']))) {
    errorAsXml(sprintf(_("Invalid user id %s"), $uid));
  }
  removeDigest($GLOBALS['namemap'][$uid]['username']);
  removeFromGroups($GLOBALS['namemap'][$uid]['username']);
  unset($GLOBALS['namemap'][$uid]);
  updateNameMap();
}

if ($_GET) {
  if ($_GET['method']) {
    switch ($_GET['method']) {
    case 'getuserdata':
      if ($_GET['uid']) {
        print getUserData($_GET['uid']);
      }
      break;
    default:
      errorAsXml(sprintf(_("Unexpected values %s!"), serialize($_GET)));
    }
  } else if (isset($_GET['language'])) {
    header("Content-Type: text/html; charset=UTF-8");
    $smarty->assign("users", $namemap);
    $smarty->display("users.html");
  } else {
    invalidCall();
  }
} elseif ($_POST) {
  if ($_POST['method']) {
    switch ($_POST['method']) {
    case 'submituser':
      if ($_POST['uid']) {
        updateUser($_POST);
        print getUserData($_POST['uid']);
      } else {
        print getUserData(insertUser($_POST));
      }
      break;
    case 'deleteuser':
      if ($_POST['uid']) {
        deleteUser($_POST['uid']);
        print getDeletedUserData($_POST['uid']);
      } else {
        errorAsXml(sprintf(_("Unexpected values %s!"), serialize($_POST)));
      }
      break;
    default:
      errorAsXml(sprintf(_("Unexpected values %s!"), serialize($_POST)));
    }
  } else {
    invalidCall();
  }
} else {
  header("Content-Type: text/html; charset=UTF-8");
  $smarty->assign("users", $namemap);
  $smarty->display("users.html");
}
?>