<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
  Copyright (C) 2003-2010  Cajus Pollmeier
  Copyright (C) 2011-2016  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');

/*!
 * \brief Does autoloading for classes used in FusionDirectory.
 *
 *  Takes the list generated by 'fusiondirectory-configuration-manager' 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-configuration-manager --update-cache</b>");
    exit;
  }

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

  if (strpos($class_name, 'FusionDirectory\\') === 0) {
    $class_name = preg_replace('/^.+\\\\([^\\\\]+)$/', '\\1', "$class_name");
  }

  if (isset($class_mapping["$class_name"])) {
    require_once($BASE_DIR.'/'.$class_mapping["$class_name"]);
  } else {
    logging::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-configuration-manager --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 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 = '')
{
  logging::debug($level, $line, $function, $file, $data, $info);
}

/*!
 * \brief Return HTML safe copyright notice
 */
function copynotice ()
{
  return sprintf(htmlescape(_('%s 2002-%d %sThe FusionDirectory team, %s%s')), '&copy;', date('Y'), '<a href="http://www.fusiondirectory.org">', FD_VERSION, '</a>');
}

/*!
 * \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::get('plugin_dir');
      $nf   = preg_replace('!^'.$BASE_DIR.'/!', '', preg_replace('/^\.\.\//', '', $path));
    } else {
      $nf = preg_replace('!^'.$BASE_DIR.'/!', '', $path);
    }
    $paths = [
      "$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 = [
      "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);
}


/**
 * 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
 *
 * @return array<int,mixed>
 */
function array_remove_entries (array $needles, array $haystack): array
{
  return array_values(array_udiff($haystack, $needles, 'array_cmp_recursive'));
}


/**
 * 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<int|string|bool|null|float|double|object> $needles array of the entries to remove
 *
 * @param array<int|string|bool|null|float|double|object> $haystack original array to remove the entries from
 *
 * @return array<int,int|string|bool|null|float|double|object>
 */
function array_remove_entries_ics (array $needles, array $haystack): array
{
  // strcasecmp will work, because we only compare ASCII values here
  return array_values(array_udiff($haystack, $needles, 'strcasecmp'));
}

/**
 * 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<int|string|bool|null|float|double|object> $ar1 first array
 *
 * @param array<int|string|bool|null|float|double|object> $ar2 second array
 *
 * @return array<int,int|string|bool|null|float|double|object>
 */
function array_merge_unique (array $ar1, array $ar2): array
{
  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 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 = [
    '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])) {
    return $map[$name];
  } 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 */
  $departmentInfo = $config->getDepartmentInfo();
  if (!isset($departmentInfo[$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 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)
{
  /* Rewrite german 'umlauts' and spanish 'accents'
   to get better results */
  static $REWRITE = [
    'ä' => 'ae',
    'ö' => 'oe',
    'ü' => 'ue',
    'Ä' => 'Ae',
    'Ö' => 'Oe',
    'Ü' => 'Ue',
    'ß' => 'ss',
    'á' => 'a',
    'é' => 'e',
    'í' => 'i',
    'ó' => 'o',
    'ú' => 'u',
    'Á' => 'A',
    'É' => 'E',
    'Í' => 'I',
    'Ó' => 'O',
    'Ú' => 'U',
    'ñ' => 'ny',
    'Ñ' => 'Ny',
  ];

  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 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 += 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 -= 2 ** $i;
    }
  }

  return $res;
}

/*!
 * \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;
      default:
        return $value;
    }

    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 = [
    _('%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(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 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 ? */
  /* is this path a readable one */
  if (is_dir($path) && is_readable($path)) {
    /* Get contents and write it into an array */
    $ret = [];

    $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) && !unlink($directory.'/'.$file)) {
            $error = new FusionDirectoryError(
              htmlescape(sprintf(
                _('File "%s" could not be deleted. Try "fusiondirectory-configuration-manager --check-directories" to fix permissions.'),
                $directory."/".$file
              ))
            );
            $error->display();
          }
        }
      } 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))) {
    $fh = fopenWithErrorHandling($revision_file, 'w');
    if (is_array($fh)) {
      $error = new FusionDirectoryError(
        htmlescape(_('Cannot write to revision file:')).'<br/>'.
        implode(
          '<br/>',
          array_map('htmlescape', $fh)
        )
      );
      $error->display();
      return $result;
    } else {
      if (fwrite($fh, $revision)) {
        $result = TRUE;
      }
      fclose($fh);
    }
  }

  if (!$result) {
    $error = new FusionDirectoryError(htmlescape(_('Cannot write to revision file!')));
    $error->display();
  }

  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
    $fh = fopenWithErrorHandling($revision_file, 'r');
    if (is_array($fh)) {
      $error = new FusionDirectoryError(
        htmlescape(_('Cannot read revision file:')).'<br/>'.
        implode(
          '<br/>',
          array_map('htmlescape', $fh)
        )
      );
      $error->display();
    } else {
      // Compare File contents with current revision
      if ($revision == fread($fh, filesize($revision_file))) {
        $result = TRUE;
      }
      // Close file
      fclose($fh);
    }
  }

  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 '';
}


/**
 * Determine if two arrays are different
 *
 * @param array<int|string|bool|null|float|double|object> $src The source
 *
 * @param array<int|string|bool|null|float|double|object> $dst The destination
 *
 * @return bool TRUE or FALSE
 */
function array_differs (array $src, array $dst): bool
{
  /* If the count is differing, the arrays differ */
  if (count($src) != count($dst)) {
    return TRUE;
  }

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

/**
 * Determine if two arrays are different using recursion for sublevels
 *
 * @param mixed $src The source
 *
 * @param mixed $dst The destination
 *
 * @return bool TRUE or FALSE
 */
function array_differs_recursive ($src, $dst): bool
{
  return (array_cmp_recursive($src, $dst) !== 0);
}

/**
 * Determine if two arrays are different using recursion for sublevels
 *
 * @param mixed $src The source
 *
 * @param mixed $dst The destination
 *
 * @return int negative, 0 or positive if $src is <, = or > $dst
 */
function array_cmp_recursive ($src, $dst): int
{
  if (is_array($src)) {
    if (!is_array($dst)) {
      return 1;
    }
    if (count($src) != count($dst)) {
      return count($src) - count($dst);
    }
    foreach ($src as $key => $value) {
      if (!isset($dst[$key])) {
        return 1;
      }
      if (($cmp = array_cmp_recursive($dst[$key], $value)) !== 0) {
        return $cmp;
      }
    }
    return 0;
  }
  return strcmp($src, $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 (array $cfg)
{
  $checks = [];

  /* 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) {
    $warning = new FusionDirectoryWarning(htmlescape(_('Cannot get schema information from server. No schema check possible!')));
    $warning->display();
    return $checks;
  }

  /* This is the default block used for each entry.
   *  to avoid unset indexes.
   */
  $def_check = [
    'SCHEMA_FILE'       => '',
    'CLASSES_REQUIRED'  => [],
    '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']  = ['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'] = ['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'] = ['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'] = ['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']  = ['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'] = ['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 = [
    $hash,
    $password,
    $password,
    $userTab->userPassword,
    $userTab->attributesAccess['userPassword']->isLocked()
  ];
  $userTabs->update();
  $error = $userTabs->save();
  if (!empty($error)) {
    return $error;
  }

  return TRUE;
}

/*!
 * \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 (string $dn): string
{
  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, [$attr]);
    $attrs = $ldap->fetch();
    if (isset($attrs[$attr][0])) {
      return $attrs[$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();
}

/*!
 * \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);
}

/*!
 * \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));
  return substr(preg_replace('/([A-f0-9]{4})/', "$1:", $hex['hex']), 0, -1);
}

/* 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('errorsAlreadyPosted', []);
}

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 {
        throw new FatalError(
          sprintf(
            htmlescape(_('Cannot locate file "%s" - please run "%s" to fix this')),
            htmlescape("$BASE_DIR/$path"),
            '<b>fusiondirectory-configuration-manager --update-cache</b>'
          )
        );
      }
    }
  }
}

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)
{
  $errors = [];
  set_error_handler(
    function (int $errno, string $errstr, string $errfile, int $errline, array $errcontext) use (&$errors): bool
    {
      $errors[] = $errstr;

      return TRUE;
    }
  );
  $fh = @fopen(...$args);
  restore_error_handler();
  if ($fh !== FALSE) {
    return $fh;
  }
  return $errors;
}

// Check to see if it exists in case PHP has this function later
if (!function_exists('mb_substr_replace')) {
  // Same parameters as substr_replace with the extra encoding parameter
  function mb_substr_replace (string $string, string $replacement, $start, $length = NULL, $encoding = NULL)
  {
    if ($encoding === NULL) {
      $encoding = mb_internal_encoding();
    }
    if ($length === NULL) {
      return  mb_substr($string, 0, $start, $encoding).
              $replacement;
    } else {
      return  mb_substr($string, 0, $start, $encoding).
              $replacement.
              mb_substr($string, $start + $length, NULL, $encoding);
    }
  }
}