<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
  Copyright (C) 2003-2010  Cajus Pollmeier
  Copyright (C) 2011-2018  FusionDirectory

  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 St, Fifth Floor, Boston, MA 02110-1301, USA.
*/

/*!
 * \file functions.inc
 *  Common functions and named definitions.
 */

/* Define common locations and variables */
require_once ('variables.inc');

/* Include required files */
require_once (CACHE_DIR.'/'.CLASS_CACHE);
require_once ('functions_debug.inc');
require_once ('accept-to-gettext.inc');

/* Define constants for debugging */
define ('DEBUG_TRACE',    1); /*! Debug level for tracing of common actions (save, check, etc.) */
define ('DEBUG_LDAP',     2); /*! Debug level for LDAP queries */
define ('DEBUG_DB',       4); /*! Debug level for database operations */
define ('DEBUG_SHELL',    8); /*! Debug level for shell commands */
define ('DEBUG_POST',     16); /*! Debug level for POST content */
define ('DEBUG_SESSION',  32); /*! Debug level for SESSION content */
define ('DEBUG_CONFIG',   64); /*! Debug level for CONFIG information */
define ('DEBUG_ACL',      128); /*! Debug level for ACL infos */
define ('DEBUG_SI',       256); /*! Debug level for communication with Argonaut */
define ('DEBUG_MAIL',     512); /*! Debug level for all about mail (mailAccounts, imap, sieve etc.) */
define ('DEBUG_FAI',      1024); /* FAI (incomplete) */

/* Define shadow states */
define ('POSIX_ACCOUNT_EXPIRED', 1);
define ('POSIX_WARN_ABOUT_EXPIRATION', 2);
define ('POSIX_FORCE_PASSWORD_CHANGE', 4);
define ('POSIX_DISALLOW_PASSWORD_CHANGE', 8);

/* Rewrite german 'umlauts' and spanish 'accents'
   to get better results */
$REWRITE = array( "ä" => "ae",
    "ö" => "oe",
    "ü" => "ue",
    "Ä" => "Ae",
    "Ö" => "Oe",
    "Ü" => "Ue",
    "ß" => "ss",
    "á" => "a",
    "é" => "e",
    "í" => "i",
    "ó" => "o",
    "ú" => "u",
    "Á" => "A",
    "É" => "E",
    "Í" => "I",
    "Ó" => "O",
    "Ú" => "U",
    "ñ" => "ny",
    "Ñ" => "Ny" );


/*!
 * \brief Does autoloading for classes used in FusionDirectory.
 *
 *  Takes the list generated by 'fusiondirectory-setup' and loads the
 *  file containing the requested class.
 *
 *  \param array $class_name list of class name
 */

function __fusiondirectory_autoload($class_name)
{
  global $class_mapping, $BASE_DIR, $config;

  if ($class_mapping === NULL) {
    if (isset($config) && is_object($config) &&
        $config->get_cfg_value('displayerrors') == 'TRUE') {
      list($trace,) = html_trace();
      echo $trace;
      echo "<br/>\n";
    }
    echo sprintf(_("Fatal error: no class locations defined - please run '%s' to fix this"), "<b>fusiondirectory-setup --update-cache</b>");
    exit;
  }

  /* Do not try to autoload smarty classes */
  if (strpos($class_name, 'Smarty_') === 0) {
    return;
  }

  if (isset($class_mapping["$class_name"])) {
    require_once($BASE_DIR.'/'.$class_mapping["$class_name"]);
  } else {
    @DEBUG (DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $class_name, 'Could not load');
    if (isset($config) && is_object($config) &&
        $config->get_cfg_value('displayerrors') == 'TRUE') {
      list($trace,) = html_trace();
      echo $trace;
      echo "<br/>\n";
    }
    echo sprintf(_("Fatal error: cannot instantiate class '%s' - try running '%s' to fix this"), $class_name, "<b>fusiondirectory-setup --update-cache</b>");
    exit;
  }
}
spl_autoload_register('__fusiondirectory_autoload');


/*!
 * \brief Checks if a class is available.
 *
 * \param string $name The subject of the test
 *
 * \return boolean Return TRUE if successfull FALSE otherwise
 */
function class_available($name)
{
  global $class_mapping;
  return isset($class_mapping[$name]);
}


/*!
 * \brief Check if plugin is available
 *
 * Checks if a given plugin is available and readable.
 *
 * \param string $plugin the subject of the check
 *
 * \return boolean Return TRUE if successfull FALSE otherwise
 */
function plugin_available($plugin)
{
  global $class_mapping, $BASE_DIR;

  if (!isset($class_mapping[$plugin])) {
    return FALSE;
  } else {
    return is_readable($BASE_DIR.'/'.$class_mapping[$plugin]);
  }
}

/*!
 * \brief Loads plist and load it in config object
 */
function load_plist ($ldap_available = TRUE)
{
  global $config, $plist;
  if (!session::global_is_set('plist')) {
    /* Initially load all classes */
    load_all_classes();

    $plist = new pluglist();
    session::global_set('plist', $plist);
    $config->loadPlist($plist);
    if ($ldap_available) {
      $config->get_departments();
      $config->make_idepartments();
    }
  }
  return session::global_get('plist');
}

/*!
 * \brief Debug level action
 *
 * Print a DEBUG level if specified debug level of the level matches the
 * the configured debug level.
 *
 * \param int $level The log level of the message (should use the constants,
 * defined in functions.in (DEBUG_TRACE, DEBUG_LDAP, etc.)
 *
 * \param int $line Define the line of the logged action (using __LINE__ is common)
 *
 * \param string $function Define the function where the logged action happened in
 * (using __FUNCTION__ is common)
 *
 * \param string $file Define the file where the logged action happend in
 * (using __FILE__ is common)
 *
 * \param mixed $data The data to log. Can be a message or an array, which is printed
 * with print_a
 *
 * \param string $info Optional: Additional information
 */
function DEBUG($level, $line, $function, $file, $data, $info = '')
{
  static $first = TRUE;
  if (($_SERVER['REQUEST_METHOD'] == 'POST') && preg_match('/index.php$/', $_SERVER['REQUEST_URI'])) {
    return;
  }
  if (session::global_get('DEBUGLEVEL') & $level) {
    if ($first) {
      echo '<div id="debug-handling" class="notice">'.
            '<img src="geticon.php?context=status&amp;icon=dialog-information&amp;size=22" alt="Information" style="vertical-align:middle;margin-right:.2em;"/>'.
            'There is some debug output '.
            '<button onClick="javascript:$$(\'div.debug_div\').each(function (a) { a.toggle(); });">Toggle</button>'.
          '</div>';
      $first = FALSE;
    }
    $output = "DEBUG[$level] ";
    if ($function != '') {
      $output .= "($file:$function():$line) - $info: ";
    } else {
      $output .= "($file:$line) - $info: ";
    }
    echo '<div class="debug_div">';
    echo $output;
    if (is_array($data)) {
      print_a($data);
    } else {
      echo "'$data'";
    }
    echo "</div>\n";
  }
}

/*!
 * \brief Return themed path for specified base file
 *
 *  Depending on its parameters, this function returns the full
 *  path of a template file. First match wins while searching
 *  in this order:
 *
 *  - load theme depending file
 *  - load global theme depending file
 *  - load default theme file
 *  - load global default theme file
 *
 * \param  string $filename The base file name
 *
 * \param  boolean $plugin Flag to take the plugin directory as search base
 *
 * \param  string $path User specified path to take as search base
 *
 * \return string Full path to the template file
 */
function get_template_path($filename = '', $plugin = FALSE, $path = '')
{
  global $config, $BASE_DIR;
  $default_theme = 'breezy';

  /* Set theme */
  if (isset ($config)) {
    $theme = $config->get_cfg_value('theme', $default_theme);
  } else {
    $theme = $default_theme;
  }

  /* Return path for empty filename */
  if ($filename == '') {
    return "themes/$theme/";
  }

  /* Return plugin dir or root directory? */
  if ($plugin) {
    if ($path == '') {
      $path = session::global_get('plugin_dir');
      $nf   = preg_replace('!^'.$BASE_DIR.'/!', '', preg_replace('/^\.\.\//', '', $path));
    } else {
      $nf = preg_replace('!^'.$BASE_DIR.'/!', '', $path);
    }
    $paths = array(
      "$BASE_DIR/ihtml/themes/$theme/$nf/$filename",
      "$BASE_DIR/ihtml/themes/$default_theme/$nf/$filename",
      "$BASE_DIR/ihtml/themes/default/$nf/$filename",
      $path."/$filename"
    );
  } else {
    $paths = array(
      "themes/$theme/$filename",
      "$BASE_DIR/ihtml/themes/$theme/$filename",
      "themes/$default_theme/$filename",
      "$BASE_DIR/ihtml/themes/$default_theme/$filename",
      "themes/default/$filename",
      "$BASE_DIR/ihtml/themes/default/$filename",
      $filename
    );
  }

  foreach ($paths as $path) {
    if (file_exists($path)) {
      return $path;
    }
  }

  return end($paths);
}


/*!
 * \brief Remove multiple entries from an array
 *
 * Removes every element that is in $needles from the
 * array given as $haystack
 *
 * \param array $needles array of the entries to remove
 *
 * \param array $haystack original array to remove the entries from
 */
function array_remove_entries(array $needles, array $haystack)
{
  return array_values(array_diff($haystack, $needles));
}


/*!
 * \brief Remove multiple entries from an array (case-insensitive)
 *
 * Removes every element that is in $needles from the
 * array given as $haystack but case insensitive
 *
 * \param array $needles array of the entries to remove
 *
 * \param array $haystack original array to remove the entries from
 */
function array_remove_entries_ics(array $needles, array $haystack)
{
  // strcasecmp will work, because we only compare ASCII values here
  return array_values(array_udiff($haystack, $needles, 'strcasecmp'));
}

/*!
 * \brief Merge to array but remove duplicate entries (case-insensitive)
 *
 * Merges two arrays and removes duplicate entries. Triggers
 * an error if first or second parametre is not an array.
 *
 * \param array $ar1 first array
 *
 * \param array $ar2 second array
 *
 * \return array
 */
function array_merge_unique($ar1, $ar2)
{
  if (!is_array($ar1) || !is_array($ar2)) {
    trigger_error('Specified parameter(s) are not valid arrays.');
  } else {
    return array_values(array_unique(array_merge($ar1, $ar2)));
  }
}


/*!
 * \brief Generate a system log info
 *
 * Creates a syslog message, containing user information.
 *
 * \param string $message the message to log
 */
function fusiondirectory_log ($message)
{
  global $ui;

  /* Preset to something reasonable */
  $username = '[unauthenticated]';

  /* Replace username if object is present */
  if (isset($ui)) {
    if ($ui->uid != '') {
      $username = '['.$ui->uid.']';
    } else {
      $username = '[unknown]';
    }
  }

  syslog(LOG_INFO, "FusionDirectory $username: $message");
}


/*!
 * \brief Initialize a LDAP connection
 *
 * Initializes a LDAP connection.
 *
 * \param string $server The server we are connecting to
 *
 * \param string $base The base of our ldap tree
 *
 * \param string $binddn Default: empty
 *
 * \param string $pass Default: empty
 *
 * \return LDAP object
 */
function ldap_init ($server, $base, $binddn = '', $pass = '')
{
  global $config;

  $ldap = new LDAP ($binddn, $pass, $server,
      isset($config->current['LDAPFOLLOWREFERRALS']) && $config->current['LDAPFOLLOWREFERRALS'] == 'TRUE',
      isset($config->current['LDAPTLS']) && $config->current['LDAPTLS'] == 'TRUE');

  /* Sadly we've no proper return values here. Use the error message instead. */
  if (!$ldap->success()) {
    msg_dialog::display(_('Fatal error'),
        sprintf(_("FATAL: Error when connecting the LDAP. Server said '%s'."), $ldap->get_error()),
        FATAL_ERROR_DIALOG);
    exit();
  }

  /* Preset connection base to $base and return to caller */
  $ldap->cd ($base);
  return $ldap;
}

/*!
 * \brief Get user from LDAP directory
 *
 * Search the user by login or other fields authorized by the configuration
 *
 * \param string $username The username or email to check
 *
 * \return userinfo instance on SUCCESS, FALSE if not found, string error on error
 */
function ldap_get_user ($username)
{
  global $config;

  /* look through the entire ldap */
  $ldap = $config->get_ldap_link();
  if (!$ldap->success()) {
    msg_dialog::display(_('LDAP error'),
        msgPool::ldaperror($ldap->get_error(), '', LDAP_AUTH).'<br/><br/>'.session::get('errors'),
        FATAL_ERROR_DIALOG);
    exit();
  }

  $allowed_attributes = array('uid','mail');
  $verify_attr = array();
  $tmp = explode(',', $config->get_cfg_value('loginAttribute'));
  foreach ($tmp as $attr) {
    if (in_array($attr, $allowed_attributes)) {
      $verify_attr[] = $attr;
    }
  }

  if (count($verify_attr) == 0) {
    $verify_attr = array('uid');
  }
  $tmp    = $verify_attr;
  $tmp[]  = 'uid';
  $filter = '';
  foreach ($verify_attr as $attr) {
    $filter .= '('.$attr.'='.$username.')';
  }
  $filter = '(&(|'.$filter.')(objectClass=inetOrgPerson))';
  $ldap->cd($config->current['BASE']);
  $ldap->search($filter, $tmp);

  /* get results, only a count of 1 is valid */
  if ($ldap->count() == 0) {
    /* user not found */
    return FALSE;
  } elseif ($ldap->count() != 1) {
    /* found more than one matching id */
    return _('Login (uid) is not unique inside the LDAP tree. Please contact your administrator.');
  }

  /* LDAP schema is not case sensitive. Perform additional check. */
  $attrs = $ldap->fetch();
  $success = FALSE;
  foreach ($verify_attr as $attr) {
    if (isset($attrs[$attr][0]) && $attrs[$attr][0] == $username) {
      $success = TRUE;
    }
  }
  $ldap->disconnect();
  if (!$success) {
    return FALSE;
  }

  return new userinfo($attrs['dn']);
}

/*!
 * \brief Verify user login against LDAP directory
 *
 * Checks if the specified username is in the LDAP and verifies if the
 * password is correct by binding to the LDAP with the given credentials.
 *
 * \param string $username The username to check
 *
 * \param string $password The password to check
 *
 * \return TRUE on SUCCESS, NULL or FALSE on error
 */
function ldap_login_user ($username, $password)
{
  global $config;

  $ui = ldap_get_user($username);

  if ($ui === FALSE) {
    return NULL;
  } elseif (is_string($ui)) {
    msg_dialog::display(_('Internal error'), $ui, FATAL_ERROR_DIALOG);
    return NULL;
  }

  /* password check, bind as user with supplied password  */
  $ldapObj = new LDAP($ui->dn, $password, $config->current['SERVER'],
    isset($config->current['LDAPFOLLOWREFERRALS']) &&
    $config->current['LDAPFOLLOWREFERRALS'] == 'TRUE',
    isset($config->current['LDAPTLS'])
    && $config->current['LDAPTLS'] == 'TRUE'
  );
  $ldap = new ldapMultiplexer($ldapObj);
  if (!$ldap->success()) {
    return NULL;
  }
  if (class_available('ppolicyAccount')) {
    $ldap->cd($config->current['BASE']);
    $ldap->search('(objectClass=*)', array(), 'one');
    if (!$ldap->success()) {
      msg_dialog::display(
        _('Authentication error'),
        _('It seems your user password has expired. Please use <a href="recovery.php">password recovery</a> to change it.'),
        ERROR_DIALOG
      );
      return NULL;
    }
  }

  /* Username is set, load subtreeACL's now */
  $ui->loadACL();

  return $ui;
}

/*!
 *  \brief Add a lock for object(s)
 *
 * Adds a lock by the specified user for one ore multiple objects.
 * If the lock for that object already exists, an error is triggered.
 *
 * \param array $object The object or array of objects to lock
 *
 * \param string $user  The user who shall own the lock
 */
function add_lock($object, $user)
{
  global $config;

  /* Remember which entries were opened as read only, because we
      don't need to remove any locks for them later.
   */
  if (!session::global_is_set('LOCK_CACHE')) {
    session::global_set('LOCK_CACHE', array(''));
  }
  if (is_array($object)) {
    foreach ($object as $obj) {
      add_lock($obj, $user);
    }
    return;
  }

  $cache = &session::global_get_ref('LOCK_CACHE');
  if (isset($_POST['open_readonly'])) {
    $cache['READ_ONLY'][$object] = TRUE;
    return;
  }
  if (isset($cache['READ_ONLY'][$object])) {
    unset($cache['READ_ONLY'][$object]);
  }

  /* Just a sanity check... */
  if ($object == '' || $user == '') {
    msg_dialog::display(_('Internal error'), _('Error while adding a lock. Contact the developers!'), ERROR_DIALOG);
    return;
  }

  /* Check for existing entries in lock area */
  $ldap = $config->get_ldap_link();
  $ldap->cd(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
  $ldap->search('(&(objectClass=fdLockEntry)(fdUserDn='.ldap_escape_f($user).')(fdObjectDn='.base64_encode($object).'))',
      array('fdUserDn'));
  if (!$ldap->success()) {
    msg_dialog::display(_('Configuration error'), sprintf(_('Cannot create locking information in LDAP tree. Please contact your administrator!').'<br><br>'._('LDAP server returned: %s'), '<br><br><i>'.$ldap->get_error().'</i>'), ERROR_DIALOG);
    return;
  }

  /* Add lock if none present */
  if ($ldap->count() == 0) {
    $attrs  = array();
    $name   = md5($object);
    $ldap->cd('cn='.$name.','.get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
    $attrs = array(
      'objectClass'     => 'fdLockEntry',
      'fdUserDn'        => $user,
      'fdObjectDn'      => base64_encode($object),
      'cn'              => $name,
      'fdLockTimestamp' => LdapGeneralizedTime::toString(new DateTime('now')),
    );
    $ldap->add($attrs);
    if (!$ldap->success()) {
      msg_dialog::display(_('LDAP error'), msgPool::ldaperror($ldap->get_error(), "cn=$name,".get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE'], 0), LDAP_ERROR);
      return;
    }
  }
}


/*!
 * \brief Remove a lock for object(s)
 *
 * Remove a lock for object(s)
 *
 * \param mixed $object object or array of objects for which a lock shall be removed
 */
function del_lock ($object)
{
  global $config;

  if (is_array($object)) {
    foreach ($object as $obj) {
      del_lock($obj);
    }
    return;
  }

  /* Sanity check */
  if ($object == '') {
    return;
  }

  /* If this object was opened in read only mode then
      skip removing the lock entry, there wasn't any lock created.
    */
  if (session::global_is_set('LOCK_CACHE')) {
    $cache = &session::global_get_ref('LOCK_CACHE');
    if (isset($cache['READ_ONLY'][$object])) {
      unset($cache['READ_ONLY'][$object]);
      return;
    }
  }

  /* Check for existance and remove the entry */
  $ldap = $config->get_ldap_link();
  $ldap->cd(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
  $ldap->search('(&(objectClass=fdLockEntry)(fdObjectDn='.base64_encode($object).'))', array('fdObjectDn'));
  $attrs = $ldap->fetch();
  if (!$ldap->success()) {
    msg_dialog::display(_('LDAP error'), msgPool::ldaperror($ldap->get_error(), $ldap->getDN(), LDAP_DEL, ERROR_DIALOG));
    return;
  } elseif (!empty($attrs['dn'])) {
    $ldap->rmdir($attrs['dn']);
  }
}


/*!
 * \brief Remove all locks owned by a specific userdn
 *
 * For a given userdn remove all existing locks. This is usually
 * called on logout.
 *
 * \param string $userdn the subject whose locks shall be deleted
 */
function del_user_locks($userdn)
{
  global $config;

  /* Get LDAP ressources */
  $ldap = $config->get_ldap_link();
  $ldap->cd(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);

  /* Remove all objects of this user, drop errors silently in this case. */
  $ldap->search('(&(objectClass=fdLockEntry)(fdUserDn='.ldap_escape_f($userdn).'))', array('fdUserDn'));
  while ($attrs = $ldap->fetch()) {
    $ldap->rmdir($attrs['dn']);
  }
}


/*!
 * \brief Get a lock for a specific object
 *
 * Searches for a lock on a given object.
 *
 * \param string $object subject whose locks are to be searched
 *
 * \return string Returns the dn of the user who owns the lock or '' if no lock is found
 * or FALSE if an error occured.
 */
function get_lock($object)
{
  /* Sanity check */
  if ($object == '') {
    msg_dialog::display(_('Internal error'), _('Error while adding a lock. Contact the developers!'), ERROR_DIALOG);
    return FALSE;
  }

  /* Allow readonly access, the plugin constructor will restrict the acls */
  if (isset($_POST['open_readonly'])) {
    return '';
  }

  $locks = get_locks($object);
  if ($locks === FALSE) {
    return FALSE;
  } elseif (empty($locks)) {
    return '';
  } else {
    return $locks[0]['user'];
  }
}


/*!
 * \brief Get locks for objects
 *
 * Similar as get_lock(), but for multiple objects.
 *
 * \param mixed $objects Array of dns for which a lock will be searched or dn of a single object
 *
 * \param boolean $allow_readonly TRUE if readonly access should be permitted,
 * FALSE if not (default).
 *
 * \return A numbered array containing all found locks as an array with key 'object'
 * and key 'user', or FALSE if an error occured.
 */
function get_locks($objects, $allow_readonly = FALSE)
{
  global $config;

  if (is_array($objects) && (count($objects) == 1)) {
    $objects = reset($objects);
  }
  if (is_array($objects)) {
    if ($allow_readonly) {
      trigger_error('Read only is not possible for several objects');
    }
    $filter = '(&(objectClass=fdLockEntry)(|';
    foreach ($objects as $obj) {
      $filter .= '(fdObjectDn='.base64_encode($obj).')';
    }
    $filter .= '))';
  } else {
    if ($allow_readonly && isset($_POST['open_readonly'])) {
      /* If readonly is allowed and asked and there is only one object, bypass lock detection */
      return array();
    }
    $filter = '(&(objectClass=fdLockEntry)(fdObjectDn='.base64_encode($objects).'))';
  }

  /* Get LDAP link, check for presence of the lock entry */
  $ldap = $config->get_ldap_link();
  $ldap->cd(get_ou('lockRDN').get_ou('fusiondirectoryRDN').$config->current['BASE']);
  $ldap->search($filter, array('fdUserDn','fdObjectDn', 'fdLockTimestamp'));
  if (!$ldap->success()) {
    msg_dialog::display(_('LDAP error'), msgPool::ldaperror($ldap->get_error(), '', LDAP_SEARCH), LDAP_ERROR);
    return FALSE;
  }

  $locks = array();
  while ($attrs = $ldap->fetch()) {
    $locks[] = array(
      'dn'        => $attrs['dn'],
      'object'    => base64_decode($attrs['fdObjectDn'][0]),
      'user'      => $attrs['fdUserDn'][0],
      'timestamp' => LdapGeneralizedTime::fromString($attrs['fdLockTimestamp'][0]),
    );
  }

  if (!is_array($objects) && (count($locks) > 1)) {
    /* Hmm. We're removing broken LDAP information here and issue a warning. */
    msg_dialog::display(_('Warning'), _('Found multiple locks for object to be locked. This should not happen - cleaning up multiple references.'), WARNING_DIALOG);

    /* Clean up these references now... */
    foreach ($locks as $lock) {
      $ldap->rmdir($lock['dn']);
    }

    return FALSE;
  }

  return $locks;
}

/*!
 * \brief Return the current userinfo object
 *
 * \return return the current userinfo object
 */
function &get_userinfo()
{
  global $ui;

  return $ui;
}

/*!
 * \brief Get global smarty object
 *
 * \return return the global smarty object
 */
function &get_smarty()
{
  global $smarty;

  return $smarty;
}

/*!
 * \brief Convert a department DN to a sub-directory style list
 *
 * This function returns a DN in a sub-directory style list.
 * Examples:
 * - ou=1.1.1,ou=limux becomes limux/1.1.1
 * - cn=bla,ou=foo,dc=local becomes foo/bla or foo/bla/local, depending
 * on the value for $base.
 *
 * If the specified DN contains a basedn which either matches
 * the specified base or $config->current['BASE'] it is stripped.
 *
 * \param string $dn the subject for the conversion
 *
 * \param string $base the base dn, default: $config->current['BASE']
 *
 * \return a string in the form as described above
 */
function convert_department_dn($dn, $base = NULL)
{
  global $config;

  if ($base == NULL) {
    $base = $config->current['BASE'];
  }

  /* Build a sub-directory style list of the tree level
     specified in $dn */
  $dn = preg_replace('/'.preg_quote($base, '/')."$/i", '', $dn);
  if (empty($dn)) {
    return '/';
  }

  $dep = '';
  foreach (explode(',', $dn) as $rdn) {
    $dep = preg_replace("/^[^=]+=/", '', $rdn).'/'.$dep;
  }

  /* Return and remove accidently trailing slashes */
  return trim($dep, '/');
}

/*! \brief Get the OU of a certain RDN
 *
 * Given a certain RDN name (ogroupRDN, applicationRDN etc.) this
 * function returns either a configured OU or the default
 * for the given RDN.
 *
 * Example:
 * \code
 * # Determine LDAP base where systems are stored
 * $base = get_ou('systemRDN') . $config->current['BASE'];
 * $ldap->cd($base);
 * \endcode
 *
 * \param $name the rdn of the ou you are trying to find
 *
 * \return the ou associated the the RDN or nothing
 *
 */
function get_ou($name)
{
  global $config;

  $map = array(
    'fusiondirectoryRDN'      => 'ou=fusiondirectory,',
    'lockRDN'                 => 'ou=locks,',
    'recoveryTokenRDN'        => 'ou=recovery,',
    'reminderTokenRDN'        => 'ou=reminder,',
    'roleRDN'                 => 'ou=roles,',
    'ogroupRDN'               => 'ou=groups,',
    'applicationRDN'          => 'ou=apps,',
    'systemRDN'               => 'ou=systems,',
    'serverRDN'               => 'ou=servers,ou=systems,',
    'terminalRDN'             => 'ou=terminals,ou=systems,',
    'workstationRDN'          => 'ou=workstations,ou=systems,',
    'printerRDN'              => 'ou=printers,ou=systems,',
    'phoneRDN'                => 'ou=phones,ou=systems,',
    'componentRDN'            => 'ou=netdevices,ou=systems,',
    'mobilePhoneRDN'          => 'ou=mobile,ou=systems,',

    'inventoryRDN'            => 'ou=inventory,',

    'ipmiRDN'                 => 'ou=ipmi,',

    'faxBlocklistRDN'     => 'ou=gofax,ou=systems,',
    'aclRoleRDN'          => 'ou=aclroles,',
    'phoneMacroRDN'       => 'ou=macros,ou=asterisk,ou=configs,ou=systems,',
    'phoneConferenceRDN'  => 'ou=conferences,ou=asterisk,ou=configs,ou=systems,',

    'faiBaseRDN'      => 'ou=fai,ou=configs,ou=systems,',
    'faiScriptRDN'    => 'ou=scripts,',
    'faiHookRDN'      => 'ou=hooks,',
    'faiTemplateRDN'  => 'ou=templates,',
    'faiVariableRDN'  => 'ou=variables,',
    'faiProfileRDN'   => 'ou=profiles,',
    'faiPackageRDN'   => 'ou=packages,',
    'faiPartitionRDN' => 'ou=disk,',

    'debconfRDN'      => 'ou=debconf,',

    'supannStructuresRDN' => 'ou=structures,',

    'sudoRDN'     => 'ou=sudoers,',

    'netgroupRDN' => 'ou=netgroup,',

    'deviceRDN'   => 'ou=devices,',

    'aliasRDN'    => 'ou=alias,',

    'dsaRDN'    => 'ou=dsa,',

    'mimetypeRDN' => 'ou=mime,'
  );

  /* Preset ou... */
  if ($config->get_cfg_value($name, '_not_set_') != '_not_set_') {
    $ou = $config->get_cfg_value($name);
  } elseif (isset($map[$name])) {
    $ou = $map[$name];
    return $ou;
  } else {
    return NULL;
  }

  if ($ou != '') {
    if (!preg_match('/^[^=]+=[^=]+/', $ou)) {
      $ou = "ou=$ou";
    } else {
      $ou = "$ou";
    }

    if (preg_match('/'.preg_quote($config->current['BASE'], '/').'$/', $ou)) {
      return $ou;
    } else {
      if (preg_match('/,$/', $ou)) {
        return $ou;
      } else {
        return "$ou,";
      }
    }
  } else {
    return '';
  }
}

/*!
 * \brief Get the OU for users
 *
 * Function for getting the userRDN
 *
 * \return the ou of the userRDN
 */
function get_people_ou()
{
  return get_ou('userRDN');
}

/*! \brief Return a base from a given user DN
 *
 * \code
 * get_base_from_people('cn=Max Muster,dc=local')
 * # Result is 'dc=local'
 * \endcode
 *
 * \param string $dn
 *
 * \return the base from the dn
 */
function get_base_from_people($dn)
{
  global $config;

  $pattern  = "/^[^,]+,".preg_quote(get_people_ou(), '/')."/i";
  $base     = preg_replace($pattern, '', $dn);

  /* Set to base, if we're not on a correct subtree */
  if (!isset($config->idepartments[$base])) {
    $base = $config->current['BASE'];
  }

  return $base;
}


/*!
 * \brief Check if strict naming rules are configured
 *
 * Return TRUE or FALSE depending on weither strictNamingRules
 * are configured or not.
 *
 * \return Returns TRUE if strictNamingRules is set to TRUE or if the
 * config object is not available, otherwise FALSE.
 */
function strict_uid_mode()
{
  global $config;

  if (isset($config)) {
    return ($config->get_cfg_value('strictNamingRules') == 'TRUE');
  }
  return TRUE;
}

/*!
 * \brief Generate a lock message
 *
 * This message shows a warning to the user, that a certain object is locked
 * and presents some choices how the user can proceed. By default this
 * is 'Cancel' or 'Edit anyway', but depending on the function call
 * its possible to allow readonly access, too.
 *
 * Example usage:
 * \code
 * if ($locks = get_locks($this->dn)) {
 *   return gen_locked_message($locks, $this->dn, TRUE);
 * }
 * \endcode
 *
 * \param string $locks the locks as returned by get_locks
 *
 * \param string $dn the locked DN
 *
 * \param boolean $allow_readonly TRUE if readonly access should be permitted,
 * FALSE if not (default).
 *
 *
 */
function gen_locked_message($locks, $dn, $allow_readonly = FALSE)
{
  session::set('dn', $dn);
  $remove = FALSE;

  /* Save variables from LOCK_VARS_TO_USE in session - for further editing */
  if ( session::is_set('LOCK_VARS_TO_USE') && count(session::get('LOCK_VARS_TO_USE'))) {

    $LOCK_VARS_USED_GET       = array();
    $LOCK_VARS_USED_POST      = array();
    $LOCK_VARS_USED_REQUEST   = array();
    $LOCK_VARS_TO_USE         = session::get('LOCK_VARS_TO_USE');

    foreach ($LOCK_VARS_TO_USE as $name) {

      if (empty($name)) {
        continue;
      }

      foreach ($_POST as $Pname => $Pvalue) {
        if (preg_match($name, $Pname)) {
          $LOCK_VARS_USED_POST[$Pname] = $_POST[$Pname];
        }
      }

      foreach ($_GET as $Pname => $Pvalue) {
        if (preg_match($name, $Pname)) {
          $LOCK_VARS_USED_GET[$Pname] = $_GET[$Pname];
        }
      }

      foreach ($_REQUEST as $Pname => $Pvalue) {
        if (preg_match($name, $Pname)) {
          $LOCK_VARS_USED_REQUEST[$Pname] = $_REQUEST[$Pname];
        }
      }
    }
    session::set('LOCK_VARS_TO_USE',        array());
    session::set('LOCK_VARS_USED_GET',      $LOCK_VARS_USED_GET);
    session::set('LOCK_VARS_USED_POST',     $LOCK_VARS_USED_POST);
    session::set('LOCK_VARS_USED_REQUEST',  $LOCK_VARS_USED_REQUEST);
  }

  /* Prepare and show template */
  $smarty = get_smarty();
  $smarty->assign('allow_readonly', $allow_readonly);
  if (is_array($dn)) {
    $msg = '<pre>';
    foreach ($dn as $sub_dn) {
      $msg .= "\n".$sub_dn.', ';
    }
    $msg = preg_replace("/, $/", "</pre>", $msg);
  } else {
    $msg = $dn;
  }

  $smarty->assign('dn', $msg);
  if ($remove) {
    $smarty->assign('action', _('Continue anyway'));
  } else {
    $smarty->assign('action', _('Edit anyway'));
  }
  $smarty->assign('message', sprintf(_("You're going to edit the LDAP entry/entries %s"), "<b>".$msg."</b>", ""));
  $smarty->assign('locks', $locks);

  return $smarty->fetch(get_template_path('islocked.tpl'));
}


/*!
 * \brief Return a string/HTML representation of an array
 *
 * This returns a string representation of a given value.
 * It can be used to dump arrays, where every value is printed
 * on its own line. The output is targetted at HTML output, it uses
 * '<br>' for line breaks. If the value is already a string its
 * returned unchanged.
 *
 * \param mixed $value Whatever needs to be printed.
 *
 * \return string $value in html form.
 */
function to_string ($value)
{
  /* If this is an array, generate a text blob */
  if (is_array($value)) {
    $ret = '';
    foreach ($value as $line) {
      $ret .= $line."<br>\n";
    }
    return $ret;
  } else {
    return $value;
  }
}

/*! \brief Function to rewrite some problematic characters
 *
 * This function takes a string and replaces all possibly characters in it
 * with less problematic characters, as defined in $REWRITE.
 *
 * \param string $s the string to rewrite
 *
 * \return string $s the result of the rewrite
 */
function rewrite($s)
{
  global $REWRITE;

  foreach ($REWRITE as $key => $val) {
    $s = str_replace("$key", "$val", $s);
  }

  return $s;
}


/*!
 * \brief Return the base of a given DN
 *
 * \param string $dn a DN
 * \param string $ou an ou to remove from the base
 *
 * \return base of the given DN
 */
function dn2base($dn, $ou = NULL)
{
  if ($ou === NULL) {
    if (get_people_ou() != '') {
      $dn = preg_replace('/,'.get_people_ou().'/i', ',', $dn);
    }
    if (get_ou('groupRDN') != '') {
      $dn = preg_replace('/,'.get_ou('groupRDN').'/i', ',', $dn);
    }
  } else {
    $dn = preg_replace("/,$ou/i", ',', $dn);
  }

  return preg_replace ('/^[^,]+,/i', '', $dn);
}

/*!
 * \brief Check if a given command exists and is executable
 *
 * Test if a given cmdline contains an executable command. Strips
 * arguments from the given cmdline.
 *
 * \param string $cmdline the cmdline to check
 *
 * \return TRUE if command exists and is executable, otherwise FALSE.
 */
function check_command($cmdline)
{
  $cmd = preg_replace("/ .*$/", '', $cmdline);

  /* Check if command exists in filesystem */
  if (!file_exists($cmd)) {
    return FALSE;
  }

  /* Check if command is executable */
  if (!is_executable($cmd)) {
    return FALSE;
  }

  return TRUE;
}

/*!
 * \brief Print plugin HTML header
 *
 * \param string $image the path of the image to be used next to the headline
 *
 * \param string $headline the headline
 *
 * \param string $info additional information to print
 *
 * \return the $display variable
 */
function print_header($image, $headline, $info = '')
{
  $smarty = get_smarty();
  $smarty->assign('headline', $headline);
  $smarty->assign('headline_image', $image);
  $display = '';

  if ($info != '') {
    $display .= '<div class="pluginfo">'."\n";
    $display .= "$info";
    $display .= "</div>\n";
    $display .= "<div></div>\n";
  }
  return $display;
}

/*!
 * \brief Generate HTML for the 'Back' button
 *
 * \return the back button html code
 */
function back_to_main()
{
  return '<br><p class="plugbottom"><input type=submit name="password_back" value="'.
    msgPool::backButton().'"></p><input type="hidden" name="ignore">';
}

/*!
 * \brief Put netmask in n.n.n.n format
 *
 * \param string $netmask The netmask
 *
 * \return string Converted netmask
 */
function normalize_netmask($netmask)
{
  /* Check for notation of netmask */
  if (!preg_match('/^([0-9]+\.){3}[0-9]+$/', $netmask)) {
    $num      = (int)($netmask);
    $netmask  = "";

    for ($byte = 0; $byte < 4; $byte++) {
      $result = 0;

      for ($i = 7; $i >= 0; $i--) {
        if ($num-- > 0) {
          $result += pow(2, $i);
        }
      }

      $netmask .= $result.".";
    }

    return preg_replace('/\.$/', '', $netmask);
  }

  return $netmask;
}


/*!
 * \brief Return the number of set bits in the netmask
 *
 * For a given subnetmask (for example 255.255.255.0) this returns
 * the number of set bits.
 *
 * Example:
 * \code
 * $bits = netmask_to_bits('255.255.255.0') # Returns 24
 * $bits = netmask_to_bits('255.255.254.0') # Returns 23
 * \endcode
 *
 * Be aware of the fact that the function does not check
 * if the given subnet mask is actually valid. For example:
 * Bad examples:
 * \code
 * $bits = netmask_to_bits('255.0.0.255') # Returns 16
 * $bits = netmask_to_bits('255.255.0.255') # Returns 24
 * \endcode
 *
 * \param $netmask given netmask
 *
 * \return the number of bits in the netmask
 */
function netmask_to_bits($netmask)
{
  $nm = explode('.', $netmask, 4);

  $res = 0;
  for ($n = 0; $n < 4; $n++) {
    $start  = 255;

    for ($i = 0; $i < 8; $i++) {
      if ($start == (int)($nm[$n])) {
        $res += 8 - $i;
        break;
      }
      $start -= pow(2, $i);
    }
  }

  return $res;
}


/*!
 * \brief Recursion helper for gen_uids()
 */
function _recurse_gen_uids($rule, array $variables)
{
  $result = array();

  if (!count($variables)) {
    return array($rule);
  }

  reset($variables);
  $key  = key($variables);
  $val  = current($variables);
  unset($variables[$key]);

  foreach ($val as $possibility) {
    $nrule  = str_replace("{$key}", $possibility, $rule);
    $result = array_merge($result, _recurse_gen_uids($nrule, $variables));
  }

  return $result;
}


/*!
 * \brief Generate a list of uid proposals based on a rule
 *
 *  Unroll given rule string by filling in attributes and replacing
 *  all keywords.
 *
 * \param string $rule The rule string from fusiondirectory.conf.
 *
 * \param array $attributes A dictionary of attribute/value mappings
 *
 * \return array List of valid not used uids
 */
function gen_uids($rule, $attributes)
{
  global $config;

  // Attributes should be arrays
  foreach ($attributes as $name => $value) {
      $attributes[$name] = array($value);
  }

  /* Search for keys and fill the variables array with all
     possible values for that key. */
  $stripped   = $rule;
  $variables  = array();

  for ($pos = 0; preg_match('/%([^%]+)%/', $stripped, $m, PREG_OFFSET_CAPTURE, $pos); ) {
    $variables[$pos]  = templateHandling::parseMask($m[1][0], $attributes);
    $replace          = '{'.$pos.'}';
    $stripped         = substr_replace($stripped, $replace, $m[0][1], strlen($m[0][0]));
    $pos              = $m[0][1] + strlen($replace);
  }

  /* Recurse through all possible combinations */
  $proposed = _recurse_gen_uids($stripped, $variables);

  /* Get list of used ID's */
  $ldap = $config->get_ldap_link();
  $ldap->cd($config->current['BASE']);

  /* Remove used uids and watch out for id tags */
  $ret = array();
  foreach ($proposed as $uid) {
    /* Check for id tag and modify uid if needed */
    if (preg_match('/\{id(:|!)(\d+)}/', $uid, $m)) {
      $size = $m[2];

      $start = ($m[1] == ":" ? 0 : -1);
      for ($i = $start, $p = pow(10, $size) - 1; $i < $p; $i++) {
        if ($i == -1) {
          $number = "";
        } else {
          $number = sprintf("%0".$size."d", $i + 1);
        }
        $res = preg_replace('/{id(:|!)\d+}/', $number, $uid);

        $ldap->search('(uid='.ldap_escape_f(preg_replace('/[{}]/', '', $res)).')', array('dn'));
        if ($ldap->count() == 0) {
          $uid = $res;
          break;
        }
      }

      /* Remove link if nothing has been found */
      $uid = preg_replace('/{id(:|!)\d+}/', '', $uid);
    }

    if (preg_match('/\{id#\d+}/', $uid)) {
      $size = preg_replace('/^.*{id#(\d+)}.*$/', '\\1', $uid);

      while (TRUE) {
        $number = sprintf("%0".$size."d", random_int(0, pow(10, $size) - 1));
        $res    = preg_replace('/{id#(\d+)}/', $number, $uid);
        $ldap->search('(uid='.ldap_escape_f(preg_replace('/[{}]/', '', $res)).')', array('dn'));
        if ($ldap->count() == 0) {
          $uid = $res;
          break;
        }
      }

      /* Remove link if nothing has been found */
      $uid = preg_replace('/{id#\d+}/', '', $uid);
    }

    /* Don't assign used ones */
    $ldap->search('(uid='.ldap_escape_f(preg_replace('/[{}]/', '', $uid)).')', array('dn'));
    if ($ldap->count() == 0) {
      /* Add uid, but remove {} first. These are invalid anyway. */
      $uid = preg_replace('/[{}]/', '', $uid);
      if ($uid != '') {
        $ret[] = $uid;
      }
    }
  }

  return array_unique($ret);
}


/*!
 * \brief Convert various data sizes to bytes
 *
 * Given a certain value in the format n(g|m|k), where n
 * is a value and (g|m|k) stands for Gigabyte, Megabyte and Kilobyte
 * this function returns the byte value.
 *
 * \param string $value a value in the above specified format
 *
 * \return a byte value or the original value if specified string is simply
 * a numeric value
 */
function to_byte($value)
{
  $value = strtolower(trim($value));

  if (!is_numeric(substr($value, -1))) {

    switch (substr($value, -1)) {
      case 'g':
        $mult = 1073741824;
        break;
      case 'm':
        $mult = 1048576;
        break;
      case 'k':
        $mult = 1024;
        break;
    }

    return $mult * (int)substr($value, 0, -1);
  } else {
    return $value;
  }
}

/*!
 * \brief Convert a size in bytes to a human readable version
 *
 * \param float $bytes size in bytes
 *
 * \param int $precision number of digits after comma, default is 2
 *
 * \return Returns something like '9.77KiB' for arguments (10000, 2)
 */
function humanReadableSize ($bytes, $precision = 2)
{
  $format = array(
    _('%sB'),
    _('%sKiB'),
    _('%sMiB'),
    _('%sGiB'),
    _('%sTiB'),
    _('%sPiB'),
    _('%sEiB'),
    _('%sZiB'),
    _('%sYiB')
  );
  if ($bytes == 0) {
    return sprintf($format[0], '0');
  }
  $base = log($bytes) / log(1024);

  return sprintf($format[floor($base)], round(pow(1024, $base - floor($base)), $precision));
}


/*!
 * \brief Check if a value exists in an array (case-insensitive)
 *
 * This is just as http://php.net/in_array except that the comparison
 * is case-insensitive.
 *
 * \param string $value needle
 *
 * \param array $items haystack
 *
 * \return Return TRUE is value is found, FALSE if not.
 */
function in_array_ics($value, array $items)
{
  return preg_grep('/^'.preg_quote($value, '/').'$/i', $items);
}


/*! \brief Generate a clickable alphabet */
function generate_alphabet($count = 10)
{
  $characters = _("*ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
  $alphabet   = "";
  $c          = 0;

  /* Fill cells with charaters */
  for ($i = 0, $l = mb_strlen($characters, 'UTF8'); $i < $l; $i++) {
    if ($c == 0) {
      $alphabet .= "<tr>";
    }

    $ch = mb_substr($characters, $i, 1, "UTF8");
    $alphabet .= "<td><a class=\"alphaselect\" href=\"main.php?plug=".
      validate($_GET['plug'])."&amp;search=".$ch."\">&nbsp;".$ch."&nbsp;</a></td>";

    if ($c++ == $count) {
      $alphabet .= "</tr>";
      $c        = 0;
    }
  }

  /* Fill remaining cells */
  while ($c++ <= $count) {
    $alphabet .= "<td>&nbsp;</td>";
  }

  return $alphabet;
}


/*!
 * \brief Removes malicious characters from a (POST) string.
 *
 * \param string $string the string to check for malicious caracters
 *
 * \return string with caracters removed
 */
function validate($string)
{
  return strip_tags(str_replace('\0', '', $string));
}


/*! \brief Recursively delete a path in the file system
 *
 * Will delete the given path and all its files recursively.
 * Can also follow links if told so.
 *
 * \param string $path
 *
 * \param boolean $followLinks TRUE to follow links, FALSE (default)
 * for not following links
 */
function rmdirRecursive($path, $followLinks = FALSE)
{
  $dir = opendir($path);
  while ($entry = readdir($dir)) {
    if (is_file($path."/".$entry) || ((!$followLinks) && is_link($path."/".$entry))) {
      unlink($path."/".$entry);
    } elseif (is_dir($path."/".$entry) && ($entry != '.') && ($entry != '..')) {
      rmdirRecursive($path."/".$entry);
    }
  }
  closedir($dir);
  return rmdir($path);
}


/*!
 * \brief Get directory content information
 *
 * Returns the content of a directory as an array in an
 * ascending sorted manner.
 *
 * \param string $path
 *
 * \param boolean $sort_desc weither to sort the content descending.
 *
 * \return array content of directory in ascending sorted manner.
 */
function scan_directory($path, $sort_desc = FALSE)
{
  $ret = FALSE;

  /* is this a dir ? */
  if (is_dir($path)) {

    /* is this path a readable one */
    if (is_readable($path)) {

      /* Get contents and write it into an array */
      $ret = array();

      $dir = opendir($path);

      /* Is this a correct result ?*/
      if ($dir) {
        while ($fp = readdir($dir)) {
          $ret[] = $fp;
        }
      }
    }
  }
  /* Sort array ascending , like scandir */
  sort($ret);

  /* Sort descending if parameter is sort_desc is set */
  if ($sort_desc) {
    $ret = array_reverse($ret);
  }

  return $ret;
}


/*!
 * \brief Clean the smarty compile dir
 *
 * \param string $directory smarty compile dir
 */
function clean_smarty_compile_dir($directory)
{
  if (is_dir($directory) && is_readable($directory)) {
    // Set revision filename to REVISION
    $revision_file = $directory."/REVISION";

    /* Is there a stamp containing the current revision? */
    if (file_exists($revision_file)) {
      // check for "$config->...['CONFIG']/revision" and the
      // contents should match the revision number
      if (!compare_revision($revision_file, FD_VERSION)) {
        // If revision differs, clean compile directory
        foreach (scan_directory($directory) as $file) {
          if (($file == ".") || ($file == "..")) {
            continue;
          }
          if (is_file($directory."/".$file)) {
            // delete file
            if (!unlink($directory."/".$file)) {
              msg_dialog::display(_("Internal error"), sprintf(_("File '%s' could not be deleted. Try fusiondirectory-setup --check-directories to fix permissions."), $directory."/".$file), ERROR_DIALOG);
            }
          }
        }
      } else {
        // Revision matches, nothing to do
      }
    }
    /* If the file does not exists or has just been deleted */
    if (!file_exists($revision_file)) {
      // create revision file
      create_revision($revision_file, FD_VERSION);
    }
  }
}

/*!
 * \brief Create the revision file
 *
 * Create the revision file in FusionDirectory spool dir
 *
 * \param string $revision_file the name of the revision file
 *
 * \param string $revision the version of FusionDirectory
 *
 * \return TRUE if successfully created FALSE otherwise
 */
function create_revision($revision_file, $revision)
{
  $result = FALSE;

  if (is_dir(dirname($revision_file)) && is_writable(dirname($revision_file))) {
    if ($fh = fopen($revision_file, 'w')) {
      if (fwrite($fh, $revision)) {
        $result = TRUE;
      }
      fclose($fh);
    }
  } else {
    msg_dialog::display(_('Internal error'), _('Cannot write to revision file!'), ERROR_DIALOG);
  }

  return $result;
}

/*!
 * \brief Compare the revision file
 *
 * Create the revision file in FusionDirectory spool dir
 *
 * \param string $revision_file the name of the revision file
 * \param string $revision the version of FusionDirectory
 *
 * \return TRUE if revision match FALSE otherwise
 */
function compare_revision($revision_file, $revision)
{
  // FALSE means revision differs
  $result = FALSE;

  if (file_exists($revision_file) && is_readable($revision_file)) {
    // Open file
    if ($fh = fopen($revision_file, "r")) {
      // Compare File contents with current revision
      if ($revision == fread($fh, filesize($revision_file))) {
        $result = TRUE;
      }
      // Close file
      fclose($fh);
    } else {
      msg_dialog::display(_('Internal error'), _('Cannot read revision file!'), ERROR_DIALOG);
    }
  }

  return $result;
}


/*!
 * \brief Lookup a key in an array case-insensitive
 *
 * Given an associative array this can lookup the value of
 * a certain key, regardless of the case.
 *
 * \code
 * $items = array ('FOO' => 'blub', 'bar' => 'blub');
 * array_key_ics('foo', $items); # Returns 'blub'
 * array_key_ics('BAR', $items); # Returns 'blub'
 * \endcode
 *
 * \param string $ikey needle
 *
 * \param array $items haystack
 *
 * \return return key or empty result
 */
function array_key_ics($ikey, array $items)
{
  $tmp  = array_change_key_case($items, CASE_LOWER);
  $ikey = strtolower($ikey);
  if (isset($tmp[$ikey])) {
    return $tmp[$ikey];
  }

  return '';
}


/*!
 * \brief Determine if two arrays are different
 *
 * \param array $src The source
 *
 * \param array $dst The destination
 *
 * \return boolean TRUE or FALSE
 */
function array_differs($src, $dst)
{
  /* If the count is differing, the arrays differ */
  if (count ($src) != count ($dst)) {
    return TRUE;
  }

  return (count(array_diff($src, $dst)) != 0);
}

/*!
 * \brief Determine if two arrays are different using recursion for sublevels
 *
 * \param array $src The source
 *
 * \param array $dst The destination
 *
 * \return boolean TRUE or FALSE
 */
function array_differs_recursive($src, $dst)
{
  if (is_array($src)) {
    if (!is_array($dst)) {
      return TRUE;
    }
    if (count($src) != count($dst)) {
      return TRUE;
    }
    foreach ($src as $key => $value) {
      if (!isset($dst[$key])) {
        return TRUE;
      }
      if (array_differs_recursive($dst[$key], $value)) {
        return TRUE;
      }
    }
    return FALSE;
  }
  return ((string)$src != (string)$dst);
}

/*!
 * \brief Escape all LDAP filter relevant characters
 *
 * \param string $input string where we should add \ before special caracters
 *
 */
function normalizeLdap($input)
{
  trigger_error('deprecated, use ldap_escape_f');
  return addcslashes($input, '*()\\/');
}

/*!
 * \brief Check if LDAP schema matches the requirements
 *
 * \param string $cfg A config object
 */
function check_schema($cfg)
{
  $checks = array();

  /* Get objectclasses */
  $ldapObj = new LDAP($cfg['admin'], $cfg['password'], $cfg['connection'], FALSE, $cfg['tls']);
  $ldap = new ldapMultiplexer($ldapObj);
  $objectclasses = $ldap->get_objectclasses(TRUE);
  if (count($objectclasses) == 0) {
    msg_dialog::display(_('LDAP warning'), _('Cannot get schema information from server. No schema check possible!'), WARNING_DIALOG);
    return $checks;
  }

  /* This is the default block used for each entry.
   *  to avoid unset indexes.
   */
  $def_check = array(
    'SCHEMA_FILE'       => '',
    'CLASSES_REQUIRED'  => array(),
    'STATUS'            => FALSE,
    'IS_MUST_HAVE'      => FALSE,
    'MSG'               => '',
    'INFO'              => ''
  );

  /* FusionDirectory core schemas */

  /* core-fd */
  $checks['core-fd'] = $def_check;

  $checks['core-fd']['SCHEMA_FILE']       = 'core-fd.schema';
  $checks['core-fd']['CLASSES_REQUIRED']  = array('fdLockEntry');
  $checks['core-fd']['IS_MUST_HAVE']      = TRUE;
  $checks['core-fd']['INFO']              = _('Main FusionDirectory schema');

  /* core-fd-conf */
  $checks['core-fd-conf'] = $def_check;

  $checks['core-fd-conf']['SCHEMA_FILE']      = 'core-fd-conf.schema';
  $checks['core-fd-conf']['CLASSES_REQUIRED'] = array('fusionDirectoryConf');
  $checks['core-fd-conf']['IS_MUST_HAVE']     = TRUE;
  $checks['core-fd-conf']['INFO']             = _('Schema used to store FusionDirectory configuration');

  /* ldapns */
  $checks['ldapns'] = $def_check;

  $checks['ldapns']['SCHEMA_FILE']      = 'ldapns.schema';
  $checks['ldapns']['CLASSES_REQUIRED'] = array('hostObject');
  $checks['ldapns']['IS_MUST_HAVE']     = FALSE;
  $checks['ldapns']['INFO']             = _('Used to store trust mode information in users or groups.');

  /* template-fd */
  $checks['template-fd'] = $def_check;

  $checks['template-fd']['SCHEMA_FILE']      = 'template-fd.schema';
  $checks['template-fd']['CLASSES_REQUIRED'] = array('fdTemplate');
  $checks['template-fd']['IS_MUST_HAVE']     = FALSE;
  $checks['template-fd']['INFO']             = _('Used to store templates.');

  if (class_available('posixAccount')) {
    /* nis */
    $checks['nis'] = $def_check;

    $checks['nis']['SCHEMA_FILE']       = 'nis.schema';
    $checks['nis']['CLASSES_REQUIRED']  = array('posixAccount');
    $checks['nis']['IS_MUST_HAVE']      = FALSE;
    $checks['nis']['INFO']              = _('Used to store POSIX information.');
  }

  foreach ($checks as $name => $value) {
    foreach ($value['CLASSES_REQUIRED'] as $class) {
      if (!isset($objectclasses[$class])) {
        $checks[$name]['STATUS'] = FALSE;
        if ($value['IS_MUST_HAVE']) {
          $checks[$name]['MSG']    = sprintf(_('Missing required object class "%s"!'), $class);
        } else {
          $checks[$name]['MSG']    = sprintf(_('Missing optional object class "%s"!'), $class);
        }
      } else {
        $checks[$name]['STATUS']  = TRUE;
        $checks[$name]['MSG']     = sprintf(_('Class(es) available'));
      }
    }
  }

  $checks['posixGroup'] = $def_check;

  $checks['posixGroup']['SCHEMA_FILE']      = 'nis.schema';
  $checks['posixGroup']['CLASSES_REQUIRED'] = array('posixGroup');
  $checks['posixGroup']['STATUS']           = TRUE;
  $checks['posixGroup']['MSG']              = '';
  $checks['posixGroup']['INFO']             = '';

  if (isset($objectclasses['posixGroup'])) {
    $checks['posixGroup']['IS_MUST_HAVE']     = TRUE;

    /* Depending on mixed groups plugin installation status, we need different schema configurations */
    if (class_available('mixedGroup') && isset($objectclasses['posixGroup']['STRUCTURAL'])) {
      $checks['posixGroup']['STATUS'] = FALSE;
      $checks['posixGroup']['MSG']    = _('You have installed the mixed groups plugin, but your schema configuration does not support this.');
      $checks['posixGroup']['INFO']   = _('In order to use mixed groups the objectClass "posixGroup" must be AUXILIARY');
    } elseif (!class_available('mixedGroup') && !isset($objectclasses['posixGroup']['STRUCTURAL'])) {
      $checks['posixGroup']['STATUS'] = FALSE;
      $checks['posixGroup']['MSG']    = _('Your schema is configured to support mixed groups, but this plugin is not present.');
      $checks['posixGroup']['INFO']   = _('The objectClass "posixGroup" must be STRUCTURAL');
    }
  }

  return $checks;
}


/*!
 * \brief Returns contents of the given POST variable and check magic quotes settings
 *
 * Depending on the magic quotes settings this returns a stripclashed'ed version of
 * a certain POST variable.
 *
 * \param string $name the POST var to return ($_POST[$name])
 *
 * \return string
 */
function get_post($name)
{
  if (!isset($_POST[$name])) {
    trigger_error("Requested POST value (".$name.") does not exists, you should add a check to prevent this message.");
    return FALSE;
  }

  return validate($_POST[$name]);
}

/*!
 * \brief Return class name in correct case
 */
function get_correct_class_name($cls)
{
  global $class_mapping;
  if (isset($class_mapping) && is_array($class_mapping)) {
    foreach (array_keys($class_mapping) as $class) {
      if (preg_match("/^".$cls."$/i", $class)) {
        return $class;
      }
    }
  }
  return FALSE;
}


/*!
 * \brief Change the password of a given DN
 *
 * Change the password of a given DN with the specified hash.
 *
 * \param string $dn the DN whose password shall be changed
 *
 * \param string $password the password
 *
 * \param string $hash which hash to use to encrypt it, default is empty
 * for reusing existing hash method for this password (or use the default one).
 *
 * \return boolean TRUE on success and an error strings array on failure.
 */
function change_password ($dn, $password, $hash = "")
{
  $userTabs = objects::open($dn, 'user');
  $userTab  = $userTabs->getBaseObject();
  $userTab->userPassword = array(
    $hash,
    $password,
    $password,
    $userTab->userPassword,
    $userTab->attributesAccess['userPassword']->isLocked()
  );
  $userTabs->save_object();
  $error = $userTabs->save();
  if (!empty($error)) {
    return $error;
  }

  return TRUE;
}

/* Lock or unlock samba account */
function lock_samba_account($mode, array $attrs)
{
  global $config;
  if (!isset($attrs['sambaNTPassword'][0])) {
    return array();
  }
  $modify = array('sambaNTPassword' => $attrs['sambaNTPassword'][0]);
  if ($config->get_cfg_value("sambaGenLMPassword", "FALSE") == "TRUE") {
    $modify['sambaLMPassword'] = $attrs['sambaLMPassword'][0];
  } else {
    $modify['sambaLMPassword'] = array();
  }
  foreach ($modify as &$pwd) {
    if (is_array($pwd)) {
      continue;
    }
    if ($mode == 'LOCK') {
      /* Lock entry */
      if (!preg_match('/^\!/', $pwd)) {
        $pwd = '!'.$pwd;
      }
    } else {
      /* Unlock entry */
      $pwd = preg_replace("/^\!/",  "",   $pwd);
    }
  }
  unset($pwd);
  return $modify;
}

/* Lock or unlock ssh account */
function lock_ssh_account($mode, array $attrs, &$modify)
{
  if (!isset($attrs['sshPublicKey'])) {
    return;
  }
  $modify['sshPublicKey'] = array();
  for ($i = 0; $i < $attrs['sshPublicKey']['count']; ++$i) {
    if ($mode == 'LOCK') {
      $modify['sshPublicKey'][] = preg_replace('/^/', 'disabled-', $attrs['sshPublicKey'][$i]);
    } else {
      $modify['sshPublicKey'][] = preg_replace('/^disabled-/', '', $attrs['sshPublicKey'][$i]);
    }
  }
}


/*!
 * \brief Get the Change Sequence Number of a certain DN
 *
 * To verify if a given object has been changed outside of FusionDirectory
 * in the meanwhile, this function can be used to get the entryCSN
 * from the LDAP directory. It uses the attribute as configured
 * in modificationDetectionAttribute
 *
 * \param string $dn The dn you want to check
 *
 * \return either the result or "" in any other case
 */
function getEntryCSN($dn)
{
  global $config;
  if (empty($dn) || !is_object($config)) {
    return '';
  }

  /* Get attribute that we should use as serial number */
  $attr = $config->get_cfg_value('modificationDetectionAttribute');
  if ($attr != '') {
    $ldap = $config->get_ldap_link();
    $ldap->cat($dn, array($attr));
    $csn = $ldap->fetch();
    if (isset($csn[$attr][0])) {
      return $csn[$attr][0];
    }
  }
  return '';
}

/*!
 * \brief  Initialize a file download with given content, name and data type.
 *
 * \param  string $data The content to send.
 *
 * \param  string $name The name of the file.
 *
 * \param  string $type The content identifier, default value is "application/octet-stream";
 */
function send_binary_content($data, $name, $type = "application/octet-stream")
{
  header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
  header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
  header("Cache-Control: no-cache");
  header("Pragma: no-cache");
  header("Cache-Control: post-check=0, pre-check=0");
  header("Content-type: ".$type);

  /* Strip name if it is a complete path */
  if (preg_match ("/\//", $name)) {
    $name = basename($name);
  }

  /* force download dialog */
  header('Content-Disposition: attachment; filename="'.$name.'"');

  echo $data;
  exit();
}


function reverse_html_entities($str, $type = ENT_QUOTES, $charset = "UTF-8")
{
  if (is_string($str)) {
    return htmlentities($str, $type, $charset);
  } elseif (is_array($str)) {
    foreach ($str as $name => $value) {
      $str[$name] = reverse_html_entities($value, $type, $charset);
    }
  }
  return $str;
}


/*!
 * \brief Encode special string characters
 *
 * Encode the special caracters so we can use the string in
 * HTML output, without breaking quotes.
 *
 * \param string $str The String we want to encode.
 *
 * \return string The encoded String
 */
function xmlentities($str)
{
  if (is_string($str)) {
    return htmlspecialchars($str, ENT_QUOTES);
  } elseif (is_array($str)) {
    foreach ($str as $name => $value) {
      $str[$name] = xmlentities($value);
    }
  }
  return $str;
}

/*!
 *  \brief Returns a random char
 */
function get_random_char ()
{
  $randno = rand (0, 63);
  if ($randno < 12) {
    // Digits, '/' and '.'
    return chr($randno + 46);
  } elseif ($randno < 38) {
    // Uppercase
    return chr($randno + 53);
  } else {
    // Lowercase
    return chr($randno + 59);
  }
}

/*!
 * \brief  Decrypt a string with RIJNDAEL_128
 *
 * \param string $input The string to decrypt.
 *
 * \param String $password The password used
 */
function cred_decrypt($input, $password)
{
  /************************* Inspired by Crypt/CBC.pm *******************************/
  $input = pack('H*', $input);
  if (substr($input, 0, 8) != 'Salted__') {
    throw new FusionDirectoryException("Invalid hash header: expected 'Salted__', found '".substr($input, 0, 8)."'");
  }
  $salt   = substr($input, 8, 8);
  $input  = substr($input, 16);

  $key_len  = 32;
  $iv_len   = openssl_cipher_iv_length('aes-256-cbc');

  $data = '';
  $d    = '';
  while (strlen($data) < $key_len + $iv_len) {
    $d = md5($d . $password . $salt, TRUE);
    $data .= $d;
  }
  $key  = substr($data, 0, $key_len);
  $iv   = substr($data, $key_len, $iv_len);

  return openssl_decrypt($input, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
}


function get_object_info()
{
  return session::get('objectinfo');
}


function set_object_info($str = "")
{
  session::set('objectinfo', $str);
}

/*!
 * \brief Test if an ip is the network range
 *
 * \param string $ip The ip address to test.
 *
 * \param string $net The network to test
 *
 * \param string $mask The netmask of the network
 */
function isIpInNet($ip, $net, $mask)
{
   // Move to long ints
   $ip    = ip2long($ip);
   $net   = ip2long($net);
   $mask  = ip2long($mask);

   // Mask given IP with mask. If it returns "net", we're in...
   return (($ip & $mask) == $net);
}

/*!
 * \brief Expands an IP v6
 */
function expandIPv6 ($ip)
{
  $hex  = unpack('H*hex', inet_pton($ip));
  $ip   = substr(preg_replace('/([A-f0-9]{4})/', "$1:", $hex['hex']), 0, -1);

  return $ip;
}

/* Mark the occurance of a string with a span */
function mark($needle, $haystack)
{
  $result = '';

  while (preg_match('/^(.*)('.preg_quote($needle).')(.*)$/i', $haystack, $matches)) {
    $result   .= $matches[1].'<span class="mark">'.$matches[2].'</span>';
    $haystack = $matches[3];
  }

  return $result.$haystack;
}

function reset_errors()
{
  session::set('errors', '');
  session::set('errorsAlreadyPosted', array());
  session::set('LastError', '');
}

function load_all_classes()
{
  global $BASE_DIR, $class_list, $class_mapping;
  /* Initially load all classes */
  $class_list = get_declared_classes();
  foreach ($class_mapping as $class => $path) {
    if (!in_array($class, $class_list)) {
      if (is_readable("$BASE_DIR/$path")) {
        require_once("$BASE_DIR/$path");
      } else {
        msg_dialog::display(_('Fatal error'),
            sprintf(_("Cannot locate file '%s' - please run '%s' to fix this"),
              "$BASE_DIR/$path", '<b>fusiondirectory-setup</b>'), FATAL_ERROR_DIALOG);
        exit;
      }
    }
  }
}

if (!function_exists('ldap_escape')) {
  /* This bloc is for PHP<5.6 */
  define('LDAP_ESCAPE_FILTER', 0x01);
  define('LDAP_ESCAPE_DN',     0x02);

  /* PHP version of ldap_escape for PHP<5.6 */
  function ldap_escape($subject, $ignore = '', $flags = 0)
  {
    static $charMaps = array(
      LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"),
      LDAP_ESCAPE_DN     => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'),
    );

    // Pre-process the char maps on first call
    if (!isset($charMaps[0])) {
      $charMaps[0] = array();
      for ($i = 0; $i < 256; $i++) {
        $charMaps[0][chr($i)] = sprintf('\\%02x', $i);
      }

      for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_FILTER]); $i < $l; $i++) {
        $chr = $charMaps[LDAP_ESCAPE_FILTER][$i];
        unset($charMaps[LDAP_ESCAPE_FILTER][$i]);
        $charMaps[LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr];
      }

      for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_DN]); $i < $l; $i++) {
        $chr = $charMaps[LDAP_ESCAPE_DN][$i];
        unset($charMaps[LDAP_ESCAPE_DN][$i]);
        $charMaps[LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr];
      }
    }

    // Create the base char map to escape
    $flags = (int)$flags;
    $charMap = array();
    if ($flags & LDAP_ESCAPE_FILTER) {
      $charMap += $charMaps[LDAP_ESCAPE_FILTER];
    }
    if ($flags & LDAP_ESCAPE_DN) {
      $charMap += $charMaps[LDAP_ESCAPE_DN];
    }
    if (!$charMap) {
      $charMap = $charMaps[0];
    }

    // Remove any chars to ignore from the list
    $ignore = (string)$ignore;
    for ($i = 0, $l = strlen($ignore); $i < $l; $i++) {
      unset($charMap[$ignore[$i]]);
    }

    // Do the main replacement
    $result = strtr($subject, $charMap);

    // Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed
    if ($flags & LDAP_ESCAPE_DN) {
      if ($result[0] === ' ') {
        $result = '\\20' . substr($result, 1);
      }
      if ($result[strlen($result) - 1] === ' ') {
        $result = substr($result, 0, -1) . '\\20';
      }
    }

    return $result;
  }
}

if (!function_exists('random_int')) {
  // PHP<7, we fallback on openssl_random_pseudo_bytes
  function random_int($min, $max)
  {
    $range = $max - $min;
    if ($range <= 0) {
      throw new Exception('Invalid range passed to random_int');
    }

    $log      = log($range, 2);
    // length in bytes
    $nbBytes  = (int) ($log / 8) + 1;
    // length in bits
    $nbBits   = (int) $log + 1;
    // set all lower bits to 1
    $filter   = pow(2, $nbBits) - 1;
    if ($filter >= PHP_INT_MAX) {
      $filter = PHP_INT_MAX;
    }
    do {
      $randomBytes = openssl_random_pseudo_bytes($nbBytes, $strong);
      if (!$strong || ($randomBytes === FALSE)) {
        throw new Exception('Failed to get random bytes');
      }
      $rnd = hexdec(bin2hex($randomBytes));
      // discard irrelevant bits
      $rnd = $rnd & $filter;
    } while ($rnd > $range);
    return $min + $rnd;
  }
}

function ldap_escape_f($str, $ignore = '')
{
  return ldap_escape($str, $ignore, LDAP_ESCAPE_FILTER);
}

function ldap_escape_dn($str, $ignore = '')
{
  return ldap_escape($str, $ignore, LDAP_ESCAPE_DN);
}

function mail_utf8($to, $from_user, $from_email, $subject, $message, $replyto_user = NULL, $replyto_email = NULL, $type = 'plain')
{
  $subject = "=?UTF-8?B?".base64_encode($subject)."?=";

  if ($replyto_user === NULL) {
    $replyto_user = $from_user;
  }

  if ($replyto_email === NULL) {
    $replyto_email = $from_email;
  }

  if ($from_user) {
    $from_user = "=?UTF-8?B?".base64_encode($from_user)."?=";
    $headers = "From: $from_user <$from_email>\r\n";
  } else {
    $headers = "From: <$from_email>\r\n";
  }

  if ($replyto_email) {
    if ($replyto_user) {
      $replyto_user = "=?UTF-8?B?".base64_encode($replyto_user)."?=";
      $headers .= "Reply-To: $replyto_user <$replyto_email>\r\n";
    } else {
      $headers .= "Reply-To: <$replyto_email>\r\n";
    }
  }

  $headers .= "MIME-Version: 1.0" . "\r\n" .
              "Content-type: text/$type; charset=UTF-8" . "\r\n";

  $additional_parameters = "-f".$from_email;

  return mail($to, $subject, $message, $headers, $additional_parameters);
}

/* Calls fopen, gives errors as an array if any, file handle if successful */
function fopenWithErrorHandling()
{
  $args   = func_get_args();
  $errors = array();
  set_error_handler(
    function ($errno, $errstr, $errfile, $errline, $errcontext) use (&$errors)
    {
      $errors[] = $errstr;
    }
  );
  $fh = @call_user_func_array('fopen', $args);
  restore_error_handler();
  if ($fh !== FALSE) {
    return $fh;
  }
  return $errors;
}
?>
