Skip to content
Snippets Groups Projects
Forked from Stud.IP / Stud.IP
3358 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
functions.php 59.91 KiB
<?php
# Lifter002: DONE - not applicable
# Lifter003: TEST
# Lifter007: TODO
# Lifter010: DONE - not applicable
/**
 * functions.php
 *
 * The Stud.IP-Core functions. Look to the descriptions to get further details
 *
 * 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.
 *
 * @author      Cornelis Kater <ckater@gwdg.de>
 * @author      Suchi & Berg GmbH <info@data-quest.de>
 * @author      Ralf Stockmann <rstockm@gwdg.de>
 * @author      André Noack <andre.noack@gmx.net>
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 * @access      public
 * @package     studip_cores
 * @modulegroup library
 * @module      functions.php
 */

// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
// functions.php
// Stud.IP Kernfunktionen
// Copyright (C) 2002 Cornelis Kater <ckater@gwdg.de>, Suchi & Berg GmbH <info@data-quest.de>,
// Ralf Stockmann <rstockm@gwdg.de>, André Noack André Noack <andre.noack@gmx.net>
// +---------------------------------------------------------------------------+
// 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 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
// +---------------------------------------------------------------------------+


require_once 'lib/object.inc.php';
require_once 'lib/user_visible.inc.php';

/**
 * returns an array containing name and type of the passed objeact
 * denoted by $range_id
 *
 * @global array $SEM_TYPE
 * @global array $INST_TYPE
 * @global array $SEM_TYPE_MISC_NAME
 *
 * @param string $range_id    the id of the object
 * @param string $object_type the type of the object
 *
 * @return array  an array containing name and type of the object
 */
function get_object_name($range_id, $object_type)
{
    global $SEM_TYPE,$INST_TYPE, $SEM_TYPE_MISC_NAME;

    if ($object_type == "sem") {
        $query = "SELECT status, Name FROM seminare WHERE Seminar_id = ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$range_id]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);

        if ($SEM_TYPE[$row['status']]['name'] == $SEM_TYPE_MISC_NAME) {
            $type = _('Veranstaltung');
        } else {
            $type = $SEM_TYPE[$row['status']]['name'];
        }
        if (!$type) {
            $type = _('Veranstaltung');
        }
        $name = $row['Name'];
    } else if ($object_type == 'inst' || $object_type == 'fak') {
        $query = "SELECT type, Name FROM Institute WHERE Institut_id = ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$range_id]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);

        $type = $INST_TYPE[$row['type']]['name'];
        if (!$type) {
            $type = _('Einrichtung');
        }
        $name = $row['Name'];
    }

    return compact('name', 'type');
}

/**
 * Returns a sorm object for a given range_id
 *
 * @param string the range_id
 * @return SimpleORMap Course/Institute/User/Statusgruppen/
 */
function get_object_by_range_id($range_id) {
    $possible_sorms = "Course Institute User";
    foreach(words($possible_sorms) as $sorm) {
        if ($object = $sorm::find($range_id)) {
            return $object;
        }
    }
    return false;
}

/**
 * This function checks, if there is an open Veranstaltung or Einrichtung
 *
 * @throws CheckObjectException
 *
 * @return void
 */
function checkObject()
{
    if (!Context::get()) {
        throw new CheckObjectException(_('Sie haben kein Objekt gewählt.'));
    }
}


/**
 * This function checks, if given module
 * is allowed for this stud.ip-object.
 *
 * @throws CheckObjectException
 * @param string $module the module to check for
 * @param bool $is_plugin_name is the module name old style ( "wiki","scm") or new style (the name of the plugin)
 * @return StudipModule
 */
function checkObjectModule($module, $is_plugin_name = false)
{
    if ($context = Context::get()) {
        if (!$is_plugin_name) {
            $module_name = "Core" . ucfirst($module);
        } else {
            $module_name = $module;
        }

        $studip_module = PluginManager::getInstance()->getPlugin($module_name);
        if (!$studip_module || !$studip_module->isActivated($context->getId())) {
            throw new CheckObjectException(sprintf(_('Das Inhaltselement "%s" ist für dieses Objekt leider nicht verfügbar.'), ucfirst($module)));
        }
        return $studip_module;
    }
}

/**
 * This function closes a opened Veranstaltung or Einrichtung
 *
 * @return void
 */
function closeObject()
{
    Context::close();
}

/**
 * This function determines the type of the passed id
 *
 * The function recognizes the following types at the moment:
 * Einrichtungen, Veranstaltungen, Statusgruppen and Fakultaeten
 *
 * @staticvar array $object_type_cache
 *
 * @param string $id         the id of the object
 * @param array  $check_only an array to narrow the search, may contain
 *                            'sem', 'inst', 'fak', 'group' or 'dokument' (optional)
 *
 * @return string  return "inst" (Einrichtung), "sem" (Veranstaltung),
 *                 "fak" (Fakultaeten), "group" (Statusgruppe), "dokument" (Dateien)
 *
 */
function get_object_type($id, $check_only = [])
{
    static $cache = null;

    // Nothing to check
    if (!$id) {
        return false;
    }

    // Id is global
    if ($id === 'studip') {
        return 'global';
    }

    // Initialize cache array
    if ($cache === null) {
        $cache = new StudipCachedArray('Studip/ObjectTypes');
    }

    // No cached entry available? Go ahead and determine type
    if (!isset($cache[$id])) {
        // Tests for specific types
        $tests = [
            "SELECT 'sem' FROM `seminare` WHERE `Seminar_id` = ?" => ['sem'],
            "SELECT IF(`Institut_id` = `fakultaets_id`, 'fak', 'inst') FROM `Institute` WHERE `Institut_id` = ?" => ['inst', 'fak'],
            "SELECT 'date' FROM `termine` WHERE `termin_id` = ?" => ['date'],
            "SELECT 'user' FROM `auth_user_md5` WHERE `user_id` = ?" => ['user'],
            "SELECT 'group' FROM `statusgruppen` WHERE `statusgruppe_id` = ?" => ['group'],
            "SELECT 'dokument' FROM `file_refs` WHERE `id` = ?" => ['dokument'],
            "SELECT 'range_tree' FROM `range_tree` WHERE `item_id` = ?" => ['range_tree'],
        ];

        // If we want to check only for a specific type, order the tests so that
        // these tests will be executed first
        if ($check_only) {
            uasort($tests, function ($a, $b) use ($check_only) {
                return count(array_intersect($b, $check_only)) - count(array_intersect($a, $check_only));
            });
        }

        // Actually determine type
        $type = null;
        foreach ($tests as $query => $types) {
            $type = DBManager::get()->fetchColumn($query, [$id]);
            if ($type) {
                break;
            }
        }

        // Store type
        $cache[$id] = $type ?? false;
    }

    return (!$check_only || in_array($cache[$id], $check_only)) ? $cache[$id] : false;
}

/**
 * This function calculates one of the group colors unique for the semester of
 * the passed timestamp
 *
 * It calculates a unique color number to create the initial entry for a new user in a seminar.
 * It will create a unique number for every semester and will start over, if the max. number
 * (7) is reached.
 *
 * @param integer $sem_start_time the timestamp of the start time from the Semester
 *
 * @return integer  the color number
 *
 */
function select_group($sem_start_time)
{
    //Farben Algorhytmus, erzeugt eindeutige Farbe fuer jedes Semester. Funktioniert ab 2001 die naechsten 1000 Jahre.....
    $year_of_millenium=date ("Y", $sem_start_time) % 1000;
    $index=$year_of_millenium * 2;
    if (date ("n", $sem_start_time) > 6)
        $index++;
    $group=($index % 7) + 1;

    return $group;
}

/**
 * The function shortens a string, but it uses the first 2/3 and the last 1/3
 *
 * The parts will be divided by a "[...]". The functions is to use like php's
 * mb_substr function.
 *
 * @param string  $what  the original string
 * @param integer $start start pos, 0 is the first pos
 * @param integer $end   end pos
 *
 * @return string
 *
 *
 */
function my_substr($what, $start, $end)
{
    $length=$end-$start;
    $what_length = mb_strlen($what);
    // adding 5 because: mb_strlen("[...]") == 5
    if ($what_length > $length + 5) {
        $what = mb_substr($what, $start, round(($length / 3) * 2))
              . "[...]" . mb_substr($what, $what_length - round($length / 3), $what_length);
    }
    return $what;
}

/**
 * Retrieves the fullname for a given user_id
 *
 * @param string $user_id   if omitted, current user_id is used
 * @param string $format    output format
 * @param bool   $htmlready if true, htmlReady is applied to all output-strings
 *
 * @return string
 */
function get_fullname($user_id = "", $format = "full" , $htmlready = false)
{
    static $cache;
    global $user, $_fullname_sql;

    if (!$user_id) {
        $user_id = $user->id;
    }

    if (User::findCurrent()->id === $user_id) {
        $fullname = User::findCurrent()->getFullName($format);
        return $htmlready ? htmlReady($fullname) : $fullname;
    }

    $hash = md5($user_id . $format);
    if (!isset($cache[$hash])) {
        $query = "SELECT {$_fullname_sql[$format]}
                  FROM auth_user_md5
                  LEFT JOIN user_info USING (user_id)
                  WHERE user_id = ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$user_id]);
        $cache[$hash] = $statement->fetchColumn() ?: _('unbekannt');
    }

    return $htmlready ? htmlReady($cache[$hash]) : $cache[$hash];
}

/**
 * Retrieves the fullname for a given username
 *
 * @param string $uname     if omitted, current user_id is used
 * @param string $format    output format
 * @param bool   $htmlready if true, htmlReady is applied to all output-strings
 *
 * @return       string
 */
function get_fullname_from_uname($uname = "", $format = "full", $htmlready = false)
{
    static $cache;
    global $auth, $_fullname_sql;

    if (!$uname) {
        $uname = $auth->auth['uname'];
    }

    $hash = md5($uname . $format);
    if (!isset($cache[$hash])) {
        $query = "SELECT {$_fullname_sql[$format]}
                  FROM auth_user_md5
                  LEFT JOIN user_info USING (user_id)
                  WHERE username = ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$uname]);
        $cache[$hash] = $statement->fetchColumn() ?: _('unbekannt');
    }

    return $htmlready ? htmlReady($cache[$hash]) : $cache[$hash];
}

/**
 * Retrieves the username for a given user_id
 *
 * @global object $auth
 * @staticvar array $cache
 *
 * @param string $user_id if omitted, current username will be returned
 *
 * @return string
 *
 */
function get_username($user_id = "")
{
    static $cache = [];
    global $auth;

    if (!$user_id || $user_id == $auth->auth['uid']) {
        return $auth->auth['uname'];
    }

    if (!isset($cache[$user_id])) {
        $query = "SELECT username FROM auth_user_md5 WHERE user_id = ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$user_id]);
        $cache[$user_id] = $statement->fetchColumn();
    }

    return $cache[$user_id];
}

/**
 * Retrieves the userid for a given username
 *
 * uses global $online array if user is online
 *
 * @global object $auth
 * @staticvar array $cache
 *
 * @param string $username if omitted, current user_id will be returned
 *
 * @return string
 */
function get_userid($username = "")
{
    static $cache = [];
    global $auth;

    if (!$username || $username == $auth->auth['uname']) {
        return $auth->auth['uid'];
    }

    // Read id from database if no cached version is available
    if (!isset($cache[$username])) {
        $query = "SELECT user_id FROM auth_user_md5 WHERE username = ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$username]);
        $cache[$username] = $statement->fetchColumn();
    }

    return $cache[$username];
}


/**
 * Return an array containing the nodes of the sem-tree-path
 *
 * @param string $seminar_id the seminar to get the path for
 * @param int    $depth      the depth
 * @param string $delimeter  a string to separate the path parts
 *
 * @return array
 */
function get_sem_tree_path($seminar_id, $depth = false, $delimeter = ">")
{
    $the_tree = TreeAbstract::GetInstance("StudipSemTree");
    $view = DbView::getView('sem_tree');
    $ret = null;
    $view->params[0] = $seminar_id;
    $rs = $view->get_query("view:SEMINAR_SEM_TREE_GET_IDS");
    while ($rs->next_record()){
        $ret[$rs->f('sem_tree_id')] = $the_tree->getShortPath($rs->f('sem_tree_id'), NULL, $delimeter, $depth ? $depth - 1 : 0);
    }
    return $ret;
}

/**
 * check_and_set_date
 *
 * Checks if given date is valid and sets field in array accordingly.
 * (E.g. $admin_admission_data['admission_enddate'])
 *
 * @param mixed $tag    day or placeholder for day
 * @param mixed $monat  month or placeholder for month
 * @param mixed $jahr   year or placeholder for year
 * @param mixed $stunde hours or placeholder for hours
 * @param mixed $minute minutes or placeholder for minutes
 * @param array &$arr   Reference to array to update. If NULL, only check is performed
 * @param mixed $field  Name of field in array to be set
 *
 * @return bool  true if date was valid, false else
 */
function check_and_set_date($tag, $monat, $jahr, $stunde, $minute, &$arr, $field)
{

    $check=TRUE; // everything ok?
    if (($jahr>0) && ($jahr<100))
        $jahr=$jahr+2000;

    if ($monat == _("mm")) $monat=0;
    if ($tag == _("tt")) $tag=0;
    if ($jahr == _("jjjj")) $jahr=0;
    //if ($stunde == _("hh")) $stunde=0;
    if ($minute == _("mm")) $minute=0;

    if (($monat) && ($tag) && ($jahr)) {
        if ($stunde==_("hh")) {
            $check=FALSE;
        }

        if ((!checkdate((int)$monat, (int)$tag, (int)$jahr) && ((int)$monat) && ((int)$tag) && ((int)$jahr))) {
            $check=FALSE;
        }

        if (($stunde > 24) || ($minute > 59)
            || ($stunde == 24 && $minute > 0) ) {
            $check=FALSE;
        }

        if ($stunde == 24) {
            $stunde = 23;
            $minute = 59;
        }

        if ($arr) {
            if ($check) {
                $arr[$field] = mktime((int)$stunde,(int)$minute, 0,$monat,$tag,$jahr);
            } else {
                $arr[$field] = -1;
            }
        }
    }
    return $check;
}


/**
 * gets an entry from the studip configuration table
 *
 * @param string $key the key for the config entry
 * @return string  the value
 * @deprecated since Stud.IP 5.0
 */
function get_config($key)
{
    return Config::get()->$key;
}

/**
 * reset the order-positions for the lecturers in the passed seminar,
 * starting at the passed position
 *
 * @param string $s_id     the seminar to work on
 * @param int    $position the position to start with
 *
 * @return void
 */
function re_sort_dozenten($s_id, $position)
{
    $query = "UPDATE seminar_user
              SET position = position - 1
              WHERE Seminar_id = ? AND status = 'dozent' AND position > ?";
    $statement = DBManager::get()->prepare($query);
    $statement->execute([$s_id, $position]);
}

/**
 * reset the order-positions for the tutors in the passed seminar,
 * starting at the passed position
 *
 * @param string $s_id     the seminar to work on
 * @param int    $position the position to start with
 *
 * @return void
 */
function re_sort_tutoren($s_id, $position)
{
    $query = "UPDATE seminar_user
              SET position = position - 1
              WHERE Seminar_id = ? AND status = 'tutor' AND position > ?";
    $statement = DBManager::get()->prepare($query);
    $statement->execute([$s_id, $position]);
}

/**
 * return the highest position-number increased by one for the
 * passed user-group in the passed seminar
 *
 * @param string $status     can be on of 'tutor', 'dozent', ...
 * @param string $seminar_id the seminar to work on
 *
 * @return int  the next available position
 */
function get_next_position($status, $seminar_id)
{
    $query = "SELECT MAX(position) + 1
              FROM seminar_user
              WHERE Seminar_id = ? AND status = ?";
    $statement = DBManager::get()->prepare($query);
    $statement->execute([$seminar_id, $status]);

   return $statement->fetchColumn() ?: 0;
}

/**
 * converts a string to a float, depending on the locale
 *
 * @param string $str the string to convert to float
 *
 * @return float the string casted to float
 */
function StringToFloat($str)
{
    $str = mb_substr((string)$str,0,13);
    $locale = localeconv();
    $from = ($locale["thousands_sep"] ? $locale["thousands_sep"] : ',');
    $to = ($locale["decimal_point"] ? $locale["decimal_point"] : '.');
    if(mb_strstr($str, $from)){
        $conv_str = str_replace($from, $to, $str);
        $my_float = (float)$conv_str;
        if ($conv_str === (string)$my_float) return $my_float;
    }
    return (float)$str;
}

/**
 * check which perms the currently logged in user had in the
 * passed archived seminar
 *
 * @global array $perm
 * @global object $auth
 * @staticvar array $archiv_perms
 *
 * @param string $seminar_id the seminar in the archive
 *
 * @return string the perm the user had
 */
function archiv_check_perm($seminar_id)
{
    static $archiv_perms;
    global $perm, $user;

    $u_id = $user->id;

    // root darf sowieso ueberall dran
    if ($perm->have_perm('root')) {
        return 'admin';
    }

    if (!is_array($archiv_perms)){
        $query = "SELECT seminar_id, status FROM archiv_user WHERE user_id = ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$u_id]);
        $archiv_perms = $statement->fetchGrouped(PDO::FETCH_COLUMN);

        if ($perm->have_perm("admin")){
            $query = "SELECT archiv.seminar_id, 'admin'
                      FROM user_inst
                      INNER JOIN archiv ON (heimat_inst_id = institut_id)
                      WHERE user_inst.user_id = ? AND user_inst.inst_perms = 'admin'";
            $statement = DBManager::get()->prepare($query);
            $statement->execute([$u_id]);
            $temp_perms = $statement->fetchGrouped(PDO::FETCH_COLUMN);

            $archiv_perms = array_merge($archiv_perms, $temp_perms);
        }
        if ($perm->is_fak_admin()){
            $query = "SELECT archiv.seminar_id, 'admin'
                      FROM user_inst
                      INNER JOIN Institute ON (user_inst.institut_id = Institute.fakultaets_id)
                      INNER JOIN archiv ON (archiv.heimat_inst_id = Institute.institut_id)
                      WHERE user_inst.user_id = ? AND user_inst.inst_perms = 'admin'";
            $statement = DBManager::get()->prepare($query);
            $statement->execute([$u_id]);
            $temp_perms = $statement->fetchGrouped(PDO::FETCH_COLUMN);

            $archiv_perms = array_merge($archiv_perms, $temp_perms);
        }
    }
    return $archiv_perms[$seminar_id];
}

/**
 * retrieve a list of all online users
 *
 * @global object $user
 * @global array  $_fullname_sql
 *
 * @param int    $active_time filter: the time in minutes until last life-sign
 * @param string $name_format format the fullname shall have
 *
 * @return array
 */
function get_users_online($active_time = 5, $name_format = 'full_rev')
{
    if (!isset($GLOBALS['_fullname_sql'][$name_format])) {
        $sql_fullname = array_keys($GLOBALS['_fullname_sql']);
        $name_format = reset($sql_fullname);
    }

    $query = "SELECT a.username AS temp, a.username, {$GLOBALS['_fullname_sql'][$name_format]} AS name,
                     ABS(CAST(UNIX_TIMESTAMP() AS SIGNED) - CAST(last_lifesign AS SIGNED)) AS last_action,
                     a.user_id, IF(owner_id IS NOT NULL, 1, 0) AS is_buddy, " . get_vis_query('a', 'online') . " AS is_visible,
                     a.visible
              FROM user_online uo
              JOIN auth_user_md5 a ON (a.user_id = uo.user_id)
              LEFT JOIN user_info ON (user_info.user_id = uo.user_id)
              LEFT JOIN user_visibility ON (user_visibility.user_id = uo.user_id)
              LEFT JOIN contact ON (owner_id = ? AND contact.user_id = a.user_id)
              WHERE last_lifesign > ? AND uo.user_id <> ?
              ORDER BY {$GLOBALS['_fullname_sql'][$name_format]} ASC";
    $statement = DBManager::get()->prepare($query);
    $statement->execute([
        $GLOBALS['user']->id,
        time() - $active_time * 60,
        $GLOBALS['user']->id,
    ]);
    $online = $statement->fetchGrouped();

    // measure users online
    if ($active_time === 10) {
        Metrics::gauge('core.users_online', sizeof($online));
    }

    return $online;
}

/**
 * get the number of currently online users
 *
 * @param int $active_time filter: the time in minutes until last life-sign
 *
 * @return int
 */
function get_users_online_count($active_time = 10)
{
    $cache = StudipCacheFactory::getCache();
    $online_count = $cache->read("online_count/{$active_time}");
    if ($online_count === false) {
        $query = "SELECT COUNT(*) FROM user_online
                  WHERE last_lifesign > ?";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([time() - $active_time * 60]);
        $online_count = $statement->fetchColumn();
        $cache->write("online_count/{$active_time}", $online_count, 180);
    }
    if ($GLOBALS['user']->id && $GLOBALS['user']->id !== 'nobody') {
        --$online_count;
    }
    return $online_count > 0 ? $online_count : 0;
}

/**
 * return a studip-ticket
 *
 * @return string a unique id referring to a newly created ticket
 */
function get_ticket()
{
    return Seminar_Session::get_ticket();
}

/**
 * check if the passed ticket is valid
 *
 * @param string $studipticket the ticket-id to check
 *
 * @return bool
 */
function check_ticket($studipticket)
{
    return Seminar_Session::check_ticket($studipticket);
}

/**
 * searches
 *
 * @global array $perm
 * @global object $user
 * @global array $_fullname_sql
 *
 * @param string $search_str  optional search-string
 * @param string $search_user optional user to search for
 * @param bool   $show_sem    if true, the seminar is added to the result
 *
 * @return array
 */
function search_range($search_str = false, $search_user = false, $show_sem = true)
{
    global $perm, $user, $_fullname_sql;

    // Helper function that obtains the correct name for an entity taking
    // in account whether the semesters should be displayed or not
    $formatName = function ($row) use ($show_sem) {
        $name = $row['Name'];
        if ($show_sem) {
            $name = sprintf('%s (%s%s)',
                            $name,
                            $row['startsem'],
                            $row['startsem'] != $row['endsem'] ? ' - ' . $row['endsem'] : '');
        }
        return $name;
    };

    $search_result = [];
    $show_sem_sql1 = ", s.start_time, (SELECT semester_data.name FROM semester_data WHERE s.start_time >= semester_data.`beginn` AND s.start_time <= semester_data.`ende` LIMIT 1) AS startsem, IF(semester_courses.semester_id IS NULL, '"._("unbegrenzt")."', (SELECT semester_data.name FROM semester_data LEFT JOIN semester_courses USING (semester_id) WHERE semester_courses.course_id = s.Seminar_id ORDER BY semester_data.`beginn` DESC LIMIT 1)) AS endsem ";
    $show_sem_sql2 = "LEFT JOIN semester_courses ON (semester_courses.course_id = s.Seminar_id) ";


    if ($search_str && $perm->have_perm('root')) {
        if ($search_user) {
            $query = "SELECT user_id, CONCAT({$_fullname_sql['full']}, ' (', username, ')') AS name
                      FROM auth_user_md5 AS a
                      LEFT JOIN user_info USING (user_id)
                      WHERE CONCAT(Vorname, ' ', Nachname, ' ', username) LIKE CONCAT('%', ?, '%')
                      ORDER BY Nachname, Vorname";
            $statement = DBManager::get()->prepare($query);
            $statement->execute([$search_str]);
            while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
                $search_result[$row['user_id']] = [
                    'type' => 'user',
                    'name' => $row['name'],
                ];
            }
        }

        $_hidden = _('(versteckt)');
        $query = "SELECT Seminar_id, IF(s.visible = 0, CONCAT(s.Name, ' {$_hidden}'), s.Name) AS Name %s
                  FROM seminare AS s %s
                  WHERE s.Name LIKE CONCAT('%%', ?, '%%')
                  GROUP BY s.Seminar_id
                  ORDER BY start_time DESC, Name";
        $query = $show_sem
               ? sprintf($query, $show_sem_sql1, $show_sem_sql2)
               : sprintf($query, '', '');
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$search_str]);
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            $search_result[$row['Seminar_id']] = [
                'type'      => 'sem',
                'name'      => $formatName($row),
                'starttime' => $row['start_time'],
                'startsem'  => $row['startsem'],
            ];
        }

        $query = "SELECT Institut_id, Name, IF(Institut_id = fakultaets_id, 'fak', 'inst') AS type
                  FROM Institute
                  WHERE Name LIKE CONCAT('%', ?, '%')
                  ORDER BY Name";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$search_str]);
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            $search_result[$row['Institut_id']] = [
                'type' => $row['type'],
                'name' => $row['Name'],
            ];
        }
    } elseif ($search_str && $perm->have_perm('admin')) {
        $_hidden = _('(versteckt)');
        $query = "SELECT s.Seminar_id, IF(s.visible = 0, CONCAT(s.Name, ' {$_hidden}'), s.Name) AS Name %s
                  FROM user_inst AS a
                  JOIN seminare AS s USING (Institut_id) %s
                  WHERE a.user_id = ? AND a.inst_perms = 'admin' AND s.Name LIKE CONCAT('%%', ?, '%%')
                  ORDER BY start_time";
        $query = $show_sem
               ? sprintf($query, $show_sem_sql1, $show_sem_sql2)
               : sprintf($query, '', '');
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$user->id, $search_str]);
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            $search_result[$row['Seminar_id']] = [
                'type'      => 'sem',
                'name'      => $formatName($row),
                'starttime' => $row['start_time'],
                'startsem'  => $row['startsem'],
            ];
        }

        $query = "SELECT b.Institut_id, b.Name
                  FROM user_inst AS a
                  JOIN Institute AS b USING (Institut_id)
                  WHERE a.user_id = ? AND a.inst_perms = 'admin'
                    AND a.institut_id != b.fakultaets_id AND b.Name LIKE CONCAT('%', ?, '%')
                  ORDER BY Name";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$user->id, $search_str]);
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            $search_result[$row['Institut_id']] = [
                'type' => 'inst',
                'name' => $row['Name'],
            ];
        }
        if ($perm->is_fak_admin()) {
            $_hidden = _('(versteckt)');
            $query = "SELECT s.Seminar_id, IF(s.visible = 0, CONCAT(s.Name, ' {$_hidden}'), s.Name) AS Name %s
                      FROM user_inst AS a
                      JOIN Institute AS b ON (a.Institut_id = b.Institut_id AND b.Institut_id = b.fakultaets_id)
                      JOIN Institute AS c ON (c.fakultaets_id = b.Institut_id AND c.fakultaets_id != c.Institut_id)
                      JOIN seminare AS s ON (s.Institut_id = c.Institut_id) %s
                      WHERE a.user_id = ? AND a.inst_perms = 'admin'
                        AND s.Name LIKE CONCAT('%%', ?, '%%')
                      ORDER BY start_time DESC, Name";
            $query = $show_sem
                   ? sprintf($query, $show_sem_sql1, $show_sem_sql2)
                   : sprintf($query, '', '');
            $statement = DBManager::get()->prepare($query);
            $statement->execute([$user->id, $search_str]);
            while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
                $search_result[$row['Seminar_id']] = [
                    'type'      => 'sem',
                    'name'      => $formatName($row),
                    'starttime' => $row['start_time'],
                    'startsem'  => $row['startsem'],
                ];
            }

            $query = "SELECT c.Institut_id, c.Name
                      FROM user_inst AS a
                      JOIN Institute AS b ON (a.Institut_id = b.Institut_id AND b.Institut_id = b.fakultaets_id)
                      JOIN Institute AS c ON (c.fakultaets_id = b.institut_id AND c.fakultaets_id != c.institut_id)
                      WHERE a.user_id = ? AND a.inst_perms = 'admin'
                        AND c.Name LIKE CONCAT('%', ?, '%')
                      ORDER BY Name";
            $statement = DBManager::get()->prepare($query);
            $statement->execute([$user->id, $search_str]);
            while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
                $search_result[$row['Institut_id']] = [
                    'type' => 'inst',
                    'name' => $row['Name'],
                ];
            }

            $query = "SELECT b.Institut_id, b.Name
                      FROM user_inst AS a
                      JOIN Institute AS b ON (a.Institut_id = b.Institut_id AND b.Institut_id = b.fakultaets_id)
                      WHERE a.user_id = ? AND a.inst_perms = 'admin'
                        AND b.Name LIKE CONCAT('%', ?, '%')
                      ORDER BY Name";
            $statement = DBManager::get()->prepare($query);
            $statement->execute([$user->id, $search_str]);
            while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
                $search_result[$row['Institut_id']] = [
                    'type' => 'inst',
                    'name' => $row['Name'],
                ];
            }
        }
    } elseif ($perm->have_perm('tutor') || $perm->have_perm('autor')) {
        // autors my also have evaluations and news in studygroups with proper rights
        $_hidden = _('(versteckt)');
        $query = "SELECT s.Seminar_id, IF(s.visible = 0, CONCAT(s.Name, ' {$_hidden}'), s.Name) AS Name %s
                  FROM seminar_user AS a
                  JOIN seminare AS s USING (Seminar_id) %s
                  WHERE a.user_id = ? AND a.status IN ('tutor', 'dozent')
                  ORDER BY start_time DESC, Name";
        $query = $show_sem
               ? sprintf($query, $show_sem_sql1, $show_sem_sql2)
               : sprintf($query, '', '');
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$user->id]);
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            $search_result[$row['Seminar_id']] = [
                'type'      => 'sem',
                'name'      => $formatName($row),
                'starttime' => $row['start_time'],
                'startsem'  => $row['startsem'],
            ];
        }

        $query = "SELECT Institut_id, b.Name,
                         IF (Institut_id = fakultaets_id, 'fak', 'inst') AS type
                  FROM user_inst AS a
                  JOIN Institute AS b USING (Institut_id)
                  WHERE a.user_id = ? AND a.inst_perms IN ('dozent','tutor')
                  ORDER BY Name";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$user->id]);
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            $search_result[$row['Institut_id']] = [
                'name' => $row['Name'],
                'type' => $row['type'],
            ];
        }
    }

    if (Config::get()->DEPUTIES_ENABLE) {
        $_hidden = _('(versteckt)');
        $_deputy = _('Vertretung');
        $query = "SELECT s.Seminar_id,
                         CONCAT(IF(s.visible = 0, CONCAT(s.Name, ' {$_hidden}'), s.Name), ' [{$_deputy}]') AS Name %s
                  FROM seminare AS s
                  JOIN deputies AS d ON (s.Seminar_id = d.range_id) %s
                  WHERE d.user_id = ?
                  ORDER BY s.start_time DESC, Name";
        $query = $show_sem
               ? sprintf($query, $show_sem_sql1, $show_sem_sql2)
               : sprintf($query, '', '');
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$user->id]);
        while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
            $search_result[$row['Seminar_id']] = [
                'type'      => 'sem',
                'name'      => $formatName($row),
                'starttime' => $row['start_time'],
                'startsem'  => $row['startsem'],
            ];
        }
        if (Deputy::isEditActivated()) {
            $query = "SELECT a.user_id, a.username, 'user' AS type,
                             CONCAT({$_fullname_sql['full']}, ' (', username, ')') AS name
                      FROM auth_user_md5 AS a
                      JOIN user_info USING (user_id)
                      JOIN deputies AS d ON (a.user_id = d.range_id)
                      WHERE d.user_id = ?
                      ORDER BY name ASC";
            $statement = DBManager::get()->prepare($query);
            $statement->execute([
                $user->id
            ]);
            while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
                $search_result[$row['user_id']] = $row;
            }
        }
    }

    return $search_result ?: null;
}

/**
 * format_help_url($keyword)
 * returns URL for given help keyword
 *
 * @param string $keyword the help-keyword
 *
 * @return string the help-url
 */
function format_help_url($keyword)
{
    // all help urls need short language tag (de, en)
    $lang = 'de';
    if (!empty($_SESSION['_language'])) {
        [$lang] = explode('_', $_SESSION['_language']);
    }

    // determine major Stud.IP version from SOFTWARE_VERSION.
    preg_match('/^\d+/', $GLOBALS['SOFTWARE_VERSION'], $v);
    $version = $v[0];

    $help_query = sprintf('https://hilfe.studip.de/help/%s/%s/%s',
                          $version, $lang, $keyword);
    return $help_query;
}

/**
 * Splits a string by space characters and returns these words as an array.
 * If an array is given, returns the array itself.
 *
 * @param mixed   $what what to split
 * @return array  the words of the string as array
 */
function words($what) {
    return is_array($what) ? $what : preg_split('/ /', $what, -1, PREG_SPLIT_NO_EMPTY);
}

/**
 * Encodes a string or array from UTF-8 to Stud.IP encoding (WINDOWS-1252/ISO-8859-1 with numeric HTML-ENTITIES)
 *
 * @param mixed $data a string in UTF-8 or an array with all strings encoded in utf-8
 *
 * @return string  the string in WINDOWS-1252/HTML-ENTITIES
 */
function legacy_studip_utf8decode($data)
{
    if (is_array($data)) {
        $new_data = [];
        foreach ($data as $key => $value) {
            $key = legacy_studip_utf8decode($key);
            $new_data[$key] = legacy_studip_utf8decode($value);
        }
        return $new_data;
    }

    if (!preg_match('/[\200-\377]/', $data)) {
        return $data;
    } else {
        $windows1252 = [
            "\x80" => '&#8364;',
            "\x81" => '&#65533;',
            "\x82" => '&#8218;',
            "\x83" => '&#402;',
            "\x84" => '&#8222;',
            "\x85" => '&#8230;',
            "\x86" => '&#8224;',
            "\x87" => '&#8225;',
            "\x88" => '&#710;',
            "\x89" => '&#8240;',
            "\x8A" => '&#352;',
            "\x8B" => '&#8249;',
            "\x8C" => '&#338;',
            "\x8D" => '&#65533;',
            "\x8E" => '&#381;',
            "\x8F" => '&#65533;',
            "\x90" => '&#65533;',
            "\x91" => '&#8216;',
            "\x92" => '&#8217;',
            "\x93" => '&#8220;',
            "\x94" => '&#8221;',
            "\x95" => '&#8226;',
            "\x96" => '&#8211;',
            "\x97" => '&#8212;',
            "\x98" => '&#732;',
            "\x99" => '&#8482;',
            "\x9A" => '&#353;',
            "\x9B" => '&#8250;',
            "\x9C" => '&#339;',
            "\x9D" => '&#65533;',
            "\x9E" => '&#382;',
            "\x9F" => '&#376;'];
        return str_replace(
            array_values($windows1252),
            array_keys($windows1252),
            utf8_decode(mb_encode_numericentity(
                $data,
                [0x100, 0xffff, 0, 0xffff],
                'UTF-8'
            ))
        );
    }
}

/**
 * Special stud.ip version of json_decode() that also converts the data
 * from utf8 and creates an associative array by default (this differs
 * from the default behavior of json_decode() !).
 *
 * @param String $json
 * @param bool   $assoc
 * @param int    $depth
 * @param int    $options
 * @deprecated since Stud.IP 5.0
 */
function studip_json_decode($json, $assoc = true, $depth = 512, $options = 0)
{
    $data = json_decode($json, $assoc, $depth, $options);

    return $data;
}

/**
 * Special stud.ip version of json_decode() that also converts the data
 * to utf8.
 *
 * @param mixed $data
 * @param int   $options
 * @param int   $depth
 * @deprecated since Stud.IP 5.0
 */
function studip_json_encode($data, $options = 0)
{
    $json = json_encode($data, $options);

    return $json;
}

/**
 * Encode an HTTP header parameter (e.g. filename for 'Content-Disposition').
 *
 * @param string $name  parameter name
 * @param string $value parameter value
 *
 * @return string encoded header text (using RFC 2616 or 5987 encoding)
 */
function encode_header_parameter($name, $value)
{
    if (preg_match('/[\200-\377]/', $value)) {
        // use RFC 5987 encoding (ext-parameter)
        return $name . "*=UTF-8''" . rawurlencode($value);
    } else {
        // use RFC 2616 encoding (quoted-string)
        return $name . '="' . addslashes($value) . '"';
    }
}

/**
 * Get the title used for the given status ('dozent', 'tutor' etc.) for the
 * specified SEM_TYPE. Alternative titles can be defined in the config.inc.php.
 *
 * @global array $SEM_TYPE
 * @global array $DEFAULT_TITLE_FOR_STATUS
 *
 * @param string $type     status ('dozent', 'tutor', 'autor', 'user' or 'accepted')
 * @param int    $count    count, this determines singular or plural form of title
 * @param int    $sem_type sem_type of course (defaults to type of current course)
 *
 * @return string  translated title for status
 */
function get_title_for_status($type, $count, $sem_type = NULL)
{
    global $SEM_CLASS, $SEM_TYPE, $DEFAULT_TITLE_FOR_STATUS;

    if (is_null($sem_type)) {
        $sem_type = Context::getArtNum();
    }

    $atype = 'title_'.$type;
    $index = $count == 1 ? 0 : 1;
    $class_index = $count == 1 ? $atype : $atype . '_plural';

    $title = $SEM_CLASS[$SEM_TYPE[$sem_type]['class']][$class_index] ??
             $DEFAULT_TITLE_FOR_STATUS[$type][$index] ?? _('unbekannt');

    return $title;
}

/**
 * Test whether the given URL refers to some page or resource of
 * this Stud.IP installation.
 *
 * @param string $url url to check
 *
 * @return mixed
 */
function is_internal_url($url)
{
    if (preg_match('%^[a-z]+:%', $url)) {
        return mb_strpos($url, $GLOBALS['ABSOLUTE_URI_STUDIP']) === 0;
    }

    if ($url[0] === '/') {
        return mb_strpos($url, $GLOBALS['CANONICAL_RELATIVE_PATH_STUDIP']) === 0;
    }

    return true;
}

/**
 * Return the list of SEM_TYPES that represent study groups in this
 * Stud.IP installation.
 *
 * @return array  list of SEM_TYPES used for study groups
 */
function studygroup_sem_types()
{
    $result = [];

    foreach ($GLOBALS['SEM_TYPE'] as $id => $sem_type) {
        if ($GLOBALS['SEM_CLASS'][$sem_type['class']]['studygroup_mode']) {
            $result[] = $id;
        }
    }

    return $result;
}

/**
 * generates form fields for the submitted multidimensional array
 *
 * @param string $variable the name of the array, which is filled with the data
 * @param mixed  $data     the data-array
 * @param mixed  $parent   leave this entry as is
 *
 * @return string the inputs of type hidden as html
 */
function addHiddenFields($variable, $data, $parent = [])
{
    $ret = "";
    if (is_array($data)) {
        foreach($data as $key => $value) {
            if (is_array($value)) {
                $ret .= addHiddenFields($variable, $value, array_merge($parent, [$key]));
            } else {
                $ret.= '<input type="hidden" name="'. htmlReady($variable .'['. implode('][', array_merge($parent, [$key])) .']').'" value="'. htmlReady($value) .'">' ."\n";
            }
        }
    } else {
        $ret.= '<input type="hidden" name="'. htmlReady($variable) .'" value="'. htmlReady($data) .'">' ."\n";
    }

    return $ret;
}

/**
 * Returns a new array that is a one-dimensional flattening of this
 * array (recursively). That is, for every element that is an array,
 * extract its elements into the new array.
 *
 * @param array $ary the array to be flattened
 * @return array the flattened array
 */
function array_flatten($ary)
{
    $i = 0;
    while ($i < sizeof($ary)) {
        if (is_array($ary[$i])) {
            array_splice($ary, $i, 1, $ary[$i]);
        } else {
            $i++;
        }
    }
    return $ary;
}

/**
 * Displays "relative time" - a textual representation between now and a
 * certain timestamp, e.g. "3 hours ago".
 *
 * @param int  $timestamp        Timestamp to relate to.
 * @param bool $verbose          Display long or short texts (optional)
 * @param int  $displayed_levels How many levels shall be displayed
 * @param int  $tolerance        Defines a tolerance area of seconds around
 *                               now (How many seconds must have passed until
 *                               the function won't return "now")
 * @return String Textual representation of the difference between the passed
 *                timestamp and now
 */
function reltime($timestamp, $verbose = true, $displayed_levels = 1, $tolerance = 5)
{
    if ($verbose) {
        $glue = [', ', _(' und ')];
        $levels = [
            [60, _('%u Sekunde'), _('%u Sekunden')],
            [60, _('%u Minute'),  _('%u Minuten')],
            [24, _('%u Stunde'),  _('%u Stunden')],
            [30, _('%u Tag'),     _('%u Tagen')],
            [12, _('%u Monat'),   _('%u Monaten')],
            [99, _('%u Jahr'),    _('%u Jahren')],
        ];
    } else {
        $glue = ['', ''];
        $levels = [
            [60, _('%us'),   _('%us')],
            [60, _('%umin'), _('%umin')],
            [24, _('%uh'),   _('%uh')],
            [30, _('%ud'),   _('%ud')],
            [12, _('%uM'),   _('%uM')],
            [99, _('%uy'),   _('%uy')],
        ];
    }

    $now   = time();
    $diff  = abs($timestamp - $now);

    if ($diff < $tolerance) {
        return _('jetzt');
    }

    $chunks = [];
    for ($i = 0; $i < count($levels) && $diff > 0; $i++) {
        $remainder = $diff % $levels[$i][0];
        if ($remainder > 0) {
            $chunks[] = sprintf(ngettext($levels[$i][1], $levels[$i][2], $remainder), $remainder);
        }
        $diff = floor($diff / $levels[$i][0]);
        if ($diff === 0) {
            break;
        }
    }

    $chunks = array_reverse($chunks);
    $chunks = array_slice($chunks, 0, $displayed_levels);
    if (count($chunks) == 1) {
        $result = $chunks[0];
    } else {
        $result = $chunks[0] . $glue[1] . implode($glue[0], array_slice($chunks, 1));
    }
    if ($verbose) {
        $result = sprintf($timestamp < $now ? _('vor %s') : _('in %s'), $result);
    }
    return $result;
}

/**
 * Displays a filesize in a (shortened) human readable form including the
 * according units. For instance, 1234567 would be displayed as "1 MB" or
 * 12345 would be displayed as "12 kB".
 * The function can display the units in a short or a long form ("1 b" vs.
 * "1 Byte").
 * Optionally, more than one unit part can be displayed. For instance, 1234567
 * could also be displayed as "1 MB, 234 kB, 567 b".
 *
 * @param int    $size             The raw filesize as integer
 * @param bool   $verbose          Use short or long unit names
 * @param int    $displayed_levels How many unit parts should be displayed
 * @param String $glue             Text used to glue the different unit parts
 *                                 together
 * @return String The filesize in human readable form.
 * @todo Allow "1,3 MB"
 */
function relsize($size, $verbose = true, $displayed_levels = 1, $glue = ', ', $truncate = false)
{
    $units = [
        'B' => 'Byte',
        'kB' => 'Kilobyte',
        'MB' => 'Megabyte',
        'GB' => 'Gigabyte',
        'TB' => 'Terabyte',
        'PB' => 'Petabyte',
        'EB' => 'Exabyte',
        'ZB' => 'Zettabyte',
        'YB' => 'Yottabyte',
    ];

    $result = [];
    foreach ($units as $short => $long) {
        $remainder = $size % 1024;

        $template = sprintf('%%.1f %s%%s', $verbose ? $long : $short);
        $result[$template] = $remainder;

        $size = floor($size / 1024);
        if ($size == 0) {
            break;
        }
    }

    if ($displayed_levels == 1 && count($result) >=2 && !$truncate) {
        $result = array_slice($result, -2);

        $fraction = array_shift($result);
        $template = key($result);
        $size     = array_pop($result);

        $result = [
            $template => $size + $fraction / 1024,
        ];
    } elseif ($displayed_levels > 0) {
        $result = array_slice($result, -$displayed_levels);
    }

    $display = [];
    foreach ($result as $template => $size) {
        if ($truncate || $size - floor($size) < 0.1) {
            $template = str_replace('%.1f', '%u', $template);
            $size     = (int)$size;
        }
        $display[] = sprintf($template, $size, ($verbose && $size !== 1) ? 's' : '');
    }
    return implode($glue, array_reverse($display));
}

/**
 * extracts route
 *
 * @param string $route           route (optional, uses REQUEST_URI otherwise)
 *
 * @return  string  route
 */
function get_route($route = '')
{
    $route = mb_substr(parse_url($route ?: $_SERVER['REQUEST_URI'], PHP_URL_PATH), mb_strlen($GLOBALS['CANONICAL_RELATIVE_PATH_STUDIP']));
    if (mb_strpos($route, 'plugins.php/') !== false) {
        $trails = explode('plugins.php/', $route);
        $pieces = explode('/', $trails[1]);
        $route = 'plugins.php/' . $pieces[0] . ($pieces[1] ? '/' . $pieces[1] : '') . ($pieces[2] ? '/' . $pieces[2] : '');
    } elseif (mb_strpos($route, 'dispatch.php/') !== false) {
        $trails = explode('dispatch.php/', $route);
        $dispatcher = new StudipDispatcher();
        $pieces = explode('/', $trails[1]);
        $trail = '';
        foreach ($pieces as $index => $piece) {
            $trail .= ($trail ? '/' : '') . $piece;
            if ($dispatcher->file_exists($trail . '.php')) {
                $route = 'dispatch.php/' . $trail . (isset($pieces[$index+1]) ? '/' . $pieces[$index+1] : '');
            }
        }
    }
    while (mb_substr($route, mb_strlen($route)-6, 6) == '/index') {
        $route = mb_substr($route, 0, mb_strlen($route)-6);
    }
    return $route;
}

/**
 * compares actual route to requested route
 *
 * @param string $requested_route         requested route (for help content or tour)
 * @param string $current_route           current route (optional)
 *
 * @return  boolean  result
 */
function match_route($requested_route, $current_route = '')
{
    if (!$current_route) {
        $current_route = get_route();
    }
    $route_parts = explode('?', $requested_route);
    // if base routes don't match, return false without further checks
    if (!fnmatch($route_parts[0], $current_route)) {
        return false;
    }
    // if no parameters given and base routes do match, return true
    if (empty($route_parts[1])) {
        return true;
    }
    // extract vars and check if they are set accordingly
    $vars = [];
    parse_str($route_parts[1], $vars);
    if (!count($vars)) {
        return false;
    }
    foreach ($vars as $name => $value) {
        if (@$_REQUEST[$name] != $value) {
            return false;
        }
    }
    return true;
}

function studip_default_exception_handler($exception) {
    require_once 'lib/visual.inc.php';

    // send exception to metrics backend
    if (class_exists('Metrics')) {
        $exception_class = mb_strtolower(
            preg_replace(
                '/(?<=\w)([A-Z])/',
                '_\\1',
                get_class($exception)));
        Metrics::increment('core.exception.' . $exception_class);
    }

    while (ob_get_level()) {
        ob_end_clean();
    }
    $layout = 'layouts/base.php';
    if ($exception instanceof AccessDeniedException) {
        PageLayout::setTitle(_('Zugriff verweigert'));

        $status = 403;
        $template = 'access_denied_exception';
    } else if ($exception instanceof CheckObjectException) {
        $status = 403;
        $template = 'check_object_exception';
    } elseif ($exception instanceof LoginException) {
        $GLOBALS['auth']->login_if(true);
    } else {
        if ($exception instanceOf Trails_Exception) {
            $status = $exception->getCode();
        } else {
            $status = 500;
        }
        error_log($exception->__toString());
        $template = 'unhandled_exception';
    }

    header('HTTP/1.1 ' . $status . ' ' . $exception->getMessage());

    // ajax requests return JSON instead
    // re-use the http status code determined above
    if (!strcasecmp($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '', 'xmlhttprequest')) {
        header('Content-Type: application/json; charset=UTF-8');
        $template = 'json_exception';
        $layout = null;
    }


    try {
        $args = compact('exception', 'status');
        ob_start();
        echo $GLOBALS['template_factory']->render($template, $args, $layout);
    } catch (Exception $e) {
        ob_end_clean();
        echo 'Error: ' . htmlReady($e->getMessage());
    }
    exit;
}

/**
 * Converts a string to camelCase.
 *
 * @param String $string  The string that should be converted
 * @param bool   $ucfirst Uppercase the very first character as well
 *                        (optional, defaults to false)
 * @return String containing the converted input string
 */
function strtocamelcase($string, $ucfirst = false) {
    $string = mb_strtolower($string);
    $chunks = preg_split('/\W+/', $string);
    $chunks = array_map('ucfirst', $chunks);

    if (!$ucfirst && count($chunks) > 0) {
        $chunks[0] = mb_strtolower($chunks[0]);
    }

    return implode($chunks);
}

/**
 * Converts a string to snake_case.
 *
 * @param String $string  The string that should be converted
 * @return String containing the converted input string
 */
function strtosnakecase($string) {
    $string = preg_replace('/\W+/', '_', $string);
    $string = preg_replace('/(?<!^)[A-Z]/', '_$0', $string);
    $string = mb_strtolower($string);
    return $string;
}

/**
 * Converts a string to kebab-case.
 *
 * @param String $string  The string that should be converted
 * @return String containing the converted input string
 */
function strtokebabcase($string) {
    $string = preg_replace('/\W+/', '-', $string);
    $string = preg_replace('/(?<!^)[A-Z]/', '-$0', $string);
    $string = mb_strtolower($string);
    return $string;
}

/**
 * fetch number of rows for a table
 * for innodb this is not exact, but much faster than count(*)
 *
 * @param string $table  name of database table
 * @return int number of rows
 */
function count_table_rows($table) {
    $stat = DbManager::get()->fetchOne("SHOW TABLE STATUS LIKE ?", [$table]);
    return (int)$stat['Rows'];
}

/**
 * get the file path relative to the STUDIP_BASE_PATH
 *
 * @param string path of the file
 * @return string relative path of the file
 */
function studip_relative_path($filepath)
{
    return str_replace($GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR, '', $filepath);
}


/**
 * converts a given array to a csv format
 *
 * @param array $data the data to convert, each row should be an array
 * @param string $filename full path to a file to write to, if omitted the csv content is returned
 * @param array $caption assoc array with captions, is written to the first line, $data is filtered by keys
 * @param string $delimiter sets the field delimiter (one character only)
 * @param string $enclosure sets the field enclosure (one character only)
 * @param string $eol sets the end of line format
 * @return mixed if $filename is given the number of written bytes, else the csv content as string
 */
function array_to_csv($data, $filename = null, $caption = null, $delimiter = ';' , $enclosure = '"', $eol = "\r\n", $add_bom = true )
{
    $fp = fopen('php://temp', 'r+');
    $fp2 = fopen('php://temp', 'r+');
    if ($add_bom) {
        fwrite($fp2, "\xEF\xBB\xBF");
    }
    if (is_array($caption)) {
        fputcsv($fp, array_values($caption), $delimiter, $enclosure);
        rewind($fp);
        $csv = stream_get_contents($fp);
        if ($eol != PHP_EOL) {
            $csv = trim($csv);
            $csv .= $eol;
        }
        fwrite($fp2, $csv);
        ftruncate($fp, 0);
        rewind($fp);
    }
    foreach ($data as $row) {
        if (is_array($caption)) {
            $fields = [];
            foreach(array_keys($caption) as $fieldname) {
                $fields[] = $row[$fieldname];
            }
        } else {
            $fields = $row;
        }
        fputcsv($fp, $fields, $delimiter, $enclosure);
        rewind($fp);
        $csv = stream_get_contents($fp);
        if ($eol != PHP_EOL) {
            $csv = trim($csv);
            $csv .= $eol;
        }
        fwrite($fp2, $csv);
        ftruncate($fp, 0);
        rewind($fp);
    }
    fclose($fp);
    rewind($fp2);
    if ($filename === null) {
        return stream_get_contents($fp2);
    } else {
        return file_put_contents($filename, $fp2);
    }
}


/**
* Delete a file, or a folder and its contents
*
* @author      Aidan Lister <aidan@php.net>
* @version     1.0
* @param       string   $dirname    The directory to delete
* @return      bool     Returns true on success, false on failure
*/
function rmdirr($dirname){
    // Simple delete for a file
    if (is_file($dirname)) {
        return @unlink($dirname);
    } else if (!is_dir($dirname)) {
        return false;
    }

    // Loop through the folder
    $dir = dir($dirname);
    while (false !== ($entry = $dir->read())) {
        // Skip pointers
        if ($entry == '.' || $entry == '..') {
            continue;
        }

        // Deep delete directories
        if (is_dir("$dirname/$entry") && !is_link("$dirname/$entry")) {
            rmdirr("$dirname/$entry");
        } else {
            @unlink("$dirname/$entry");
        }
    }
    // Clean up
    $dir->close();
    return @rmdir($dirname);
}


/**
 * Determines an appropriate MIME type for a file based on the
 * extension of the file name.
 *
 * @param string $filename      file name to check
 */
function get_mime_type($filename)
{
    static $mime_types = [
        // archive types
        'gz'   => 'application/x-gzip',
        'tgz'  => 'application/x-gzip',
        'bz2'  => 'application/x-bzip2',
        'zip'  => 'application/zip',
        // document types
        'txt'  => 'text/plain',
        'css'  => 'text/css',
        'csv'  => 'text/csv',
        'rtf'  => 'application/rtf',
        'pdf'  => 'application/pdf',
        'doc'  => 'application/msword',
        'xls'  => 'application/ms-excel',
        'ppt'  => 'application/ms-powerpoint',
        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'odp'  => 'application/vnd.oasis.opendocument.presentation',
        'ods'  => 'application/vnd.oasis.opendocument.spreadsheet',
        'odt'  => 'application/vnd.oasis.opendocument.text',
        // image types
        'gif'  => 'image/gif',
        'jpeg' => 'image/jpeg',
        'jpg'  => 'image/jpeg',
        'jpe'  => 'image/jpeg',
        'png'  => 'image/png',
        'bmp'  => 'image/x-ms-bmp',
        'avif' => 'image/avif',
        'avifs' => 'image/avif-sequence',
        'heic' => 'image/heic',
        'heif' => 'image/heif',
        'webp' => 'image/webp',
        'apng' => 'image/apng',
        // audio types
        'mp3'  => 'audio/mp3',
        'oga'  => 'audio/ogg',
        'wav'  => 'audio/wave',
        // video types
        'mpeg' => 'video/mpeg',
        'mpg'  => 'video/mpeg',
        'mpe'  => 'video/mpeg',
        'qt'   => 'video/quicktime',
        'mov'  => 'video/quicktime',
        'avi'  => 'video/x-msvideo',
        'flv'  => 'video/x-flv',
        'ogg'  => 'application/ogg',
        'ogv'  => 'video/ogg',
        'mp4'  => 'video/mp4',
        'webm' => 'video/webm',
    ];

    $extension = mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    if (isset($mime_types[$extension])) {
        return $mime_types[$extension];
    } else {
        return 'application/octet-stream';
    }
}


function readfile_chunked($filename, $start = null, $end = null) {
    if (isset($start) && $start < $end) {
        $chunksize = 1024 * 1024; // how many bytes per chunk
        $bytes = 0;
        $handle = fopen($filename, 'rb');
        if ($handle === false) {
            return false;
        }
        fseek($handle, $start);
        while (!feof($handle) && ($p = ftell($handle)) <= $end) {
            if ($p + $chunksize > $end) {
                $chunksize = $end - $p + 1;
            }
            $buffer = fread($handle, $chunksize);
            $bytes += strlen($buffer);
            echo $buffer;
        }
        fclose($handle);
        return $bytes; // return num. bytes delivered like readfile() does.
    } else {
        try {
            $context = get_default_http_stream_context($filename);
        } catch (InvalidArgumentException $e) {
            return readfile($filename);
        }
        return readfile($filename, false, $context);
    }
}

/**
 * @param string $url
 * @return resource
 */
function get_default_http_stream_context($url = '')
{
    $proxy = Config::get()->HTTP_PROXY;
    if ($url) {
        $purl = parse_url($url);
        if (!isset($purl['scheme']) || !in_array($purl['scheme'], ['http', 'https'])) {
            return stream_context_get_default();
        }
        $host = $purl['host'];
        $whitelist = array_filter(array_map('trim', explode(',', Config::get()->HTTP_PROXY_IGNORE)));

        foreach ($whitelist as $whitehost) {
            if (fnmatch($whitehost, $host)) {
                $proxy = '';
                break;
            }
        }
    }
    if ($proxy) {
        $opts = ['http' => ['proxy' => 'tcp://' . $proxy]];
    } else {
        $opts = [];
    }
    return stream_context_get_default($opts);
}

/**
 * Encodes an uri just like encodeURI() in Javascript would do.
 *
 * encodeURI() escapes all characters except:
 *
 *     A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #
 *
 * @param string $uri
 * @return string
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
 */
function encodeURI(string $uri): string
{
    $replacements = [
        '%21' => '!',
        '%23' => '#',
        '%24' => '$',
        '%26' => '&',
        '%27' => "'",
        '%28' => '(',
        '%29' => ')',
        '%2A' => '*',
        '%2B' => '+',
        '%2C' => ',',
        '%3B' => ';',
        '%2F' => '/',
        '%3A' => ':',
        '%3D' => '=',
        '%3F' => '?',
        '%40' => '@',
    ];
    return strtr(rawurlencode($uri), $replacements);
}