Skip to content
Snippets Groups Projects
visual.inc.php 25.3 KiB
Newer Older
<?php
# Lifter002: TODO
# Lifter007: TODO
# Lifter003: TODO
# Lifter010: TODO


// Wrapper for formatted content (defined as a constant since it is used
// in the unit test defined in tests/unit/lib/VisualTest.php as well).
define('FORMATTED_CONTENT_WRAPPER', '<div class="formatted-content ck-content">%s</div>');

//// Functions for processing marked-up text (Stud.IP markup, HTML, JS).

use Studip\Markup;

function htmlReady($what, $trim=TRUE, $br=FALSE, $double_encode=true) {
    return Markup::htmlReady($what, $trim, $br, $double_encode);
}

/**
 * Prepare text for wysiwyg (if enabled), otherwise convert special
 * characters using htmlReady.
 *
 * @param  string  $text  The text.
 * @param  boolean $trim  Trim text before applying markup rules, if TRUE.
 * @param  boolean $br    Replace newlines by <br>, if TRUE and wysiwyg editor disabled.
 * @param  boolean $double_encode  Encode existing HTML entities, if TRUE and wysiwyg editor disabled.
 * @return string         The converted string.
 */
function wysiwygReady($what, $trim=TRUE, $br=FALSE, $double_encode=true) {
    return Markup::wysiwygReady($what, $trim, $br, $double_encode);
}

function jsReady ($what, $target = 'script-single') {
    switch ($target) {

    case "script-single" :
        return addcslashes($what, "\\'\n\r");
    break;

    case "script-double" :
        return addcslashes($what, "\\\"\n\r");
    break;

    case "inline-single" :
        return htmlReady(addcslashes($what, "\\'\n\r"), false, false, true);
    break;

    case "inline-double" :
        return htmlReady(addcslashes($what, "\\\"\n\r"), false, false, true);
    break;

    }
    return addslashes($what);
}

/**
 * Quote a piece of text, optionally include the author's name.
 *
 * @param string $text Text that is to be quoted.
 * @param string $author Name of the text's author (optional).
 *
 * @return string The quoted text.
 */
function quotes_encode($text, $author = '')
{
    // If quoting is changed update these functions:
    // - StudipFormat::markupQuote
    //   lib/classes/StudipFormat.php
    // - quotes_encode lib/visual.inc.php
    // - STUDIP.Forum.citeEntry > quote
    //   public/plugins_packages/core/Forum/javascript/forum.js
    // - studipQuotePlugin > insertStudipQuote
    //   public/assets/javascripts/ckeditor/plugins/studip-quote/plugin.js

    $text = Markup::markupToHtml($text);
        $title = sprintf(_('%s hat geschrieben:'), htmlReady($author));
        $text = '<div class="author">' . $title . '</div>' . $text;
    $text = sprintf('<blockquote>%s</blockquote><p>&nbsp;</p>', $text);
    return Markup::markAsHtml($text);
}

/**
 * Common function to get all special Stud.IP formattings.
 *
 * @access public
 * @param string  $text  Marked-up text.
 * @param boolean $trim  Trim leading and trailing whitespace, if TRUE.
 * @return string        HTML code computed by applying markup-rules.
 */
function formatReady($text, $trim = true)
{
    $formatted = Markup::apply(new StudipFormat(), $text, $trim);

    return $formatted !== '' ? sprintf(FORMATTED_CONTENT_WRAPPER, $formatted) : '';
}

/**
 * Simplified version of formatReady that handles link formatting only.
 *
 * @param  string $text   Marked-up text.
 * @param  bool   $nl2br  Convert newlines to <br>.
 * @return string         Marked-up text with markup-links converted to
 *                        HTML-links.
 */
function formatLinks($text, $nl2br = true) {
    $link_markup_rule = StudipCoreFormat::getStudipMarkup('links');
    $email_markup_rule = StudipCoreFormat::getStudipMarkup('emails');
    $markup = new TextFormat();
    $markup->addMarkup(
        'links',
        $link_markup_rule['start'],
        $link_markup_rule['end'] ?? '',
        $link_markup_rule['callback']
    );
    $markup->addMarkup(
        'emails',
        $email_markup_rule['start'],
        $email_markup_rule['end'] ?? '',
        $email_markup_rule['callback']
    );
    return $markup->format(htmlReady($text, true, $nl2br));
}

/**
 * Special version of formatReady for wiki-webs.
 *
 * @access public
 * @param  string $what  Marked-up text.
 * @param  string $trim  Trim leading and trailing whitespace, if TRUE.
 * @param  string $range_id the id of the course or institute
 * @param  string $page_id the id of the page
 * @return string        HTML code computed by applying markup-rules.
 */
function wikiReady($text, $trim=TRUE, $range_id = null, $page_id = null) {
    $formatted = Markup::apply(new WikiFormat($range_id, $page_id), $text, $trim);

    return $formatted !== '' ? sprintf(FORMATTED_CONTENT_WRAPPER, $formatted) : '';
}

/**
 * Special version of formatReady for blubber, which includes hashtags
 *
 * @access public
 * @param  string $what  Marked-up text.
 * @param  string $trim  Trim leading and trailing whitespace, if TRUE.
 * @return string        HTML code computed by applying markup-rules.
 */
function blubberReady($text, $trim=TRUE) {
    $formatted = Markup::apply(new BlubberFormat(), $text, $trim);

    return $formatted !== '' ? sprintf(FORMATTED_CONTENT_WRAPPER, $formatted) : '';
}

////////////////////////////////////////////////////////////////////////////////

/**
* decodes html entities to normal characters
*
* @access   public
* @param    string
* @return   string
*/
function decodeHTML ($string) {
    return html_entity_decode($string, ENT_QUOTES);
}

/**
* formats a ~~~~ wiki signature with username and timestamp
* @param string
* @param unix timestamp
*/
function preg_call_format_signature($username, $timestamp) {
    $fullname = get_fullname_from_uname($username);
    $date = strftime('%x, %X', $timestamp);
    return '<span style="font-size: 75%">-- <a href="'.URLHelper::getLink('dispatch.php/profile', ['username' => $username]).'">'.htmlReady($fullname).'</a> '.htmlReady($date).'</span>';
}


/**
* removes all characters used by quick-format-syntax
*
* @access   public
* @param    string
* @return   string
*/
function kill_format ($text) {
    if (Markup::isHtml($text)) {
        $is_fallback = Markup::isHtmlFallback($text);
        $text = Markup::removeHtml($text);

        if (!$is_fallback) {
            // pure HTML - no Stud.IP markup to remove
            return $text;
        }
    }

    // remove Stud.IP markup
    $text = preg_replace("'\n?\r\n?'", "\n", $text);
    // wir wandeln [code] einfach in [pre][nop] um und sind ein Problem los ... :-)
    $text = preg_replace_callback("|(\[/?code\])|isU", function ($a) {
        return $a[0] === '[code]' ? '[pre][nop]' : '[/nop][/pre]';
    }, $text);

    $pattern = [
                    "'(^|\n)\!{1,4}(.+)$'m",      // Ueberschriften
                    "'(\n|\A)(-|=)+ (.+)$'m",     // Aufzaehlungslisten
                    "'%%(\S|\S.*?\S)%%'s",        // ML-kursiv
                    "'\*\*(\S|\S.*?\S)\*\*'s",    // ML-fett
                    "'__(\S|\S.*?\S)__'s",        // ML-unterstrichen
                    "'##(\S|\S.*?\S)##'s",        // ML-diktengleich
                    "'\+\+(((\+\+)*)(\S|\S.*?\S)?\\2)\+\+'s",  // ML-groesser
                    "'--(((--)*)(\S|\S.*?\S)?\\2)--'s",        // ML-kleiner
                    "'>>(\S|\S.*?\S)>>'is",  // ML-hochgestellt
                    "'<<(\S|\S.*?\S)<<'is",  // ML-tiefgestellt
                    "'{-(.+?)-}'is" ,        // durchgestrichen
                    "'\n\n  (((\n\n)  )*(.+?))(\Z|\n\n(?! ))'s",  // Absatz eingerueckt
                    "'(?<=\n|^)--+(\d?)(\n|$|(?=<))'m", // Trennlinie
                    "'\[pre\](.+?)\[/pre\]'is" ,        // praeformatierter Text
                    "'\[nop\].+\[/nop\]'isU",
                    //"'\[.+?\](((http\://|https\://|ftp\://)?([^/\s]+)(.[^/\s]+){2,})|([-a-z0-9_]+(\.[_a-z0-9-]+)*@([a-z0-9-]+(\.[a-z0-9-]+)+)))'i",
                    "'\[(.+?)\](((http\://|https\://|ftp\://)?([^/\s]+)(\.[^/\s]+){2,}(/[^\s]*)?)|([-a-z0-9_]+(\.[_a-z0-9-]+)*@([a-z0-9-]+(\.[a-z0-9-]+)+)))'i",
            //      "'\[quote=.+?quote\]'is",    // quoting

                    ];
    $replace = [
                    "\\1\\2", "\\1\\3",
                    "\\1", "\\1", "\\1", "\\1", "\\1", "\\1",
                    "\\1", "\\1", "\\1", "\n\\1\n", "", "\\1",'[nop] [/nop]',
                    //"\\2",
                    '$1 ($2)',
                     //"",
                      '$1$2'];
    $callback = function ($c) {
        return function ($m) use ($c) {
            return $m[1] . mb_substr(str_replace($c, ' ', $m[2]), 0, -1);
        };
    };
    $pattern_callback = [
        "'(^|\s)%(?!%)(\S+%)+'" => $callback('%'),     // SL-kursiv
        "'(^|\s)\*(?!\*)(\S+\*)+'" => $callback('*') ,  // SL-fett
        "'(^|\s)_(?!_)(\S+_)+'" => $callback('_'),     // SL-unterstrichen
        "'(^|\s)#(?!#)(\S+#)+'" => $callback('#'),     // SL-diktengleich
        "'(^|\s)\+(?!\+)(\S+\+)+'" => $callback('+'),  // SL-groesser
        "'(^|\s)-(?!-)(\S+-)+'" => $callback('-'),     // SL-kleiner
        "'(^|\s)>(?!>)(\S+>)+'" => $callback('>'),     // SL-hochgestellt
        "'(^|\s)<(?!<)(\S+<)+'" => $callback('<'),     // SL-tiefgestellt);
    ];

    if (preg_match_all("'\[nop\](.+)\[/nop\]'isU", $text, $matches)) {
        $text = preg_replace($pattern, $replace, $text);
        $text = preg_replace_callback_array($pattern_callback, $text);
        $text = explode("[nop] [/nop]", $text);
        $i = 0;
        $all = '';
        foreach ($text as $w) {
            $all .= $w . ($matches[1][$i++] ?? '');
        }

        return $all;
    }
    $text = preg_replace($pattern, $replace, $text);
    $text = preg_replace_callback_array($pattern_callback, $text);
    return $text;
}

function isURL($url) {
    return filter_var($url, FILTER_VALIDATE_URL) !== false;
}

function isLinkIntern($url) {
    $pum = @parse_url(TransformInternalLinks($url));

    if (!$pum) {
        return false;
    }

    // If given $url is pointing to an anchor, it should be internal
    if (count($pum) === 1 && isset($pum['fragment'])) {
        return true;
    }

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
    return in_array($pum['scheme'] ?? null, ['https', 'http', null], true)
        && in_array($pum['host'] ?? null, [$_SERVER['SERVER_NAME'] ?? null, null], true)
        && in_array($pum['port'] ?? null, [$_SERVER['SERVER_PORT'] ?? null, null], true)
        && mb_strpos($pum['path'] ?? '', $GLOBALS['CANONICAL_RELATIVE_PATH_STUDIP']) === 0;
}

/**
* convert links with 'umlauten' to punycode
*
* @access   public
* @param    string  link to convert
* @param    boolean  for mailadr = true and for other link = false
* @return   string  link in punycode
*/
function idna_link($link, $mail = false) {
    if (!Config::get()->CONVERT_IDNA_URL) {
        return $link;
    }
    $pu = @parse_url($link);
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
    if (isset($pu['host']) && preg_match('/&\w+;/i', $pu['host'])) { //umlaute?  (html-coded)
        $IDN = new Algo26\IdnaConvert\ToIdn();
        $out = false;
        if ($mail){
            if (preg_match('#^([^@]*)@(.*)$#i',$link, $matches)) {
                $out = $IDN->convert(decodeHTML($matches[2], ENT_NOQUOTES)); // false by error
                $out = $out ? $matches[1] . '@' . htmlReady($out) : $link;
            }
        } elseif (preg_match('#^([^/]*)//([^/?]*)(((/|\?).*$)|$)#i',$link, $matches)) {
            $out = $IDN->convert(decodeHTML($matches[2], ENT_NOQUOTES)); // false by error
            $out = $out ? $matches[1].'//'.htmlReady($out).$matches[3] : $link;
        }
        return $out ?: $link;
    }
    return $link;
}

/**
 * create symbols from the shorts
 *
 * This functions converts the short, locatet in the config.inc
 * into the assigned pictures. A tooltip which shows the symbol
 * code is given, too.
 *
 * @access   public
 *
 * @param        string  the text to convert
 *
 * @return       string  convertet text
*/
function symbol ($text = '')
{
    if (!$text) {
        return $text;
    }

    $patterns = [];
    $replaces = [];
    //symbols in short notation
    foreach ($GLOBALS['SYMBOL_SHORT'] as $key => $value) {
        $patterns[] = "'" . preg_quote($key) . "'m";
        $replaces[] = $value;
    }

    return preg_replace($patterns, $replaces, $text);
}

//Beschneidungsfunktion fuer alle printhead Ausgaben
function mila ($titel, $size = 60) {
    global $auth;

    if (!empty($auth->auth['jscript']) && $size == 60) {
        //hier wird die maximale Laenge berechnet, nach der Abgeschnitten wird (JS dynamisch)
        if (mb_strlen($titel) > $auth->auth['xres'] / 13) {
            $titel = mb_substr($titel, 0, $auth->auth['xres'] / 13) . '... ';
        }
    } elseif (mb_strlen ($titel) >$size) {
        $titel = mb_substr($titel, 0, $size) . '... ';
    }
    return $titel;
}

/**
 * Ausgabe der Aufklapp-Kopfzeile
 *
 * @param $breite
 * @param $left
 * @param $link
 * @param $open
 * @param $new
 * @param $icon
 * @param $titel
 * @param $zusatz
 * @param $timestmp
 * @param $printout
 * @param $index
 * @param $indikator
 * @param $css_class
 */
function printhead($breite, $left, $link, $open, $new, $icon, $titel, $zusatz,
                   $timestmp = 0, $printout = TRUE, $index = "", $indikator = "age",
                   $css_class = NULL)
{
    global $user;

    // Verzweigung was der Pfeil anzeigen soll
    if ($indikator == "viewcount") {
        if ($index == "0") {
            $timecolor = "#BBBBBB";
        } else {
            $tmp = $index;
            if ($tmp > 68)
                $tmp = 68;
            $tmp = 68-$tmp;
            $green = dechex(255 - $tmp);
            $other = dechex(119 + ($tmp/1.5));
            $timecolor= "#" . $other . $green . $other;
        }
    } elseif ($indikator == "rating") {
        if ($index == "?") {
            $timecolor = "#BBBBBB";
        } else {
            $tmp = (ABS(1-$index))*10*3;
            $green = dechex(255 - $tmp);
            $other = dechex(0);
            $red = dechex(255);
            $timecolor= "#" . $red . $green . $other;
        }
    } elseif ($indikator == "score") {
        if ($index == "0") {
            $timecolor = "#BBBBBB";
        } else {
            if ($index > 68)
                $tmp = 68;
            else
                $tmp = $index;
            $tmpb = 68-$tmp;
            $blue = dechex(255 - $tmpb);
            $other = dechex(119 + ($tmpb/1.5));
            $timecolor= "#" . $other . $other . $blue;
        }
    } else {
        if ($timestmp == 0)
            $timecolor = "#BBBBBB";
        else {
            if ($new == TRUE)
                $timecolor = "#FF0000";
            else {
                $timediff = (int) log((time() - $timestmp) / 86400 + 1) * 15;
                if ($timediff >= 68)
                    $timediff = 68;
                $red = dechex(255 - $timediff);
                $other = dechex(119 + $timediff);
                $timecolor= "#" . $red . $other . $other;
            }
        }
    }

    //TODO: überarbeiten -> valides html und/oder template draus machen...
    $class = "printhead";
    $class2 = "printhead2";
    $class3 = "printhead3";

    if ($css_class) {
        $class = $class2 = $class3 = $css_class;
    }

    if ($open == "close") {
        $print = "<td bgcolor=\"".$timecolor."\" class=\"".$class2."\" nowrap=\"nowrap\" width=\"1%\"";
        $print .= " align=\"left\" valign=\"top\">";
    }
    else {
        $print = "<td bgcolor=\"".$timecolor."\" class=\"".$class3."\" nowrap=\"nowrap\" width=\"1%\"";
        $print .= " align=\"left\" valign=\"top\">";
    }

    if ($link)
        $print .= "<a href=\"".$link."\">";

    if ($open == "open")
        $titel = "<b>" . $titel . "</b>";

    $img = $open === 'close'
         ? 'forumgrau2.png'
         : 'forumgraurunt2.png';
    $attr = [];

    if ($link) {
        // TODO [tlx] What is addon used for? This seems to lead to invalid html
        //      so i will ditch it for now in the output
        $addon = $index
               ? " ($indikator: $index)"
               : '';

        $attr = $open === 'close'
              ? tooltip2(_('Objekt aufklappen'))
              : tooltip2(_('Objekt zuklappen'));
    }

    $print .= Assets::img($img, $attr) . " ";
    if ($link) {
        $print .= "</a> ";
    }
    $print .= "</td><td class=\"".$class."\" nowrap=\"nowrap\" width=\"1%\" valign=\"bottom\"> $icon &nbsp; </td>";
    $print .= "<td class=\"".$class."\" align=\"left\" width=\"20%\" nowrap=\"nowrap\" valign=\"bottom\"> ";
    $print .= $titel."</td><td align=\"right\" nowrap=\"nowrap\" class=\"".$class."\" width=\"99%\" valign=\"bottom\">";
    $print .= $zusatz."</td>";


    if ($printout)
        echo $print;
    else
        return $print;
}

//Ausgabe des Contents einer aufgeklappten Kopfzeile
function printcontent ($breite, $write, $inhalt, $edit, $printout = true, $addon = '', $noTdTag = false) {

    $print = "";
    if ($noTdTag == false)
    {
        $print .= "<td class=\"printcontent\" width=\"22\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
        $print .= "</td><td class=\"printcontent\" width=\"$breite\" valign=\"bottom\"><br>";
    }

    $print .= $inhalt;

    if ($edit) {
        $print .= "<br><br><div align=\"center\">$edit</div>";
        if ($addon!="") {
            if (mb_substr($addon,0,5)=="open:") { // es wird der öffnen-Pfeil mit Link ausgegeben
                $print .= "</td><td valign=\"middle\" class=\"table_row_even\" nowrap><a href=\"".mb_substr($addon,5)."\">";
                $print .= Icon::create('arr_1left', 'clickable', ['title' => _('Bewertungsbereich öffnen')])->asImg();
                $print .= "</a>&nbsp;";
            } else {              // es wird erweiterter Inhalt ausgegeben
                $print .= "</td><td class=\"content_body_panel\" nowrap>";
                $print .= "<font size=\"-2\" color=\"#444444\">$addon";
            }
        }
    } else {
        $print .= "<br>";
    }

    if ($noTdTag == false)
    {
        $print .= "</td>";
    }

    if ($printout)
        echo $print;
    else
        return $print;
}

/**
 * Returns a given text as html tooltip
 *
 * title and alt attribute is default, with_popup means a JS alert box
 * activated on click
 *
 * @param        string  $text
 * @param        boolean $with_alt    return text with alt attribute
 * @param        boolean $with_popup  return text with JS alert box on click
 * @return       string
 */
function tooltip ($text, $with_alt = TRUE, $with_popup = FALSE) {
    return arrayToHtmlAttributes(tooltip2($text, $with_alt, $with_popup));
}

/**
 * Returns a given text as an array of html attributes used as tooltip
 *
 * title and alt attribute is default, with_popup means a JS alert box
 * activated on click
 *
 * @param        string  $text
 * @param        boolean $with_alt    return text with alt attribute
 * @param        boolean $with_popup  return text with JS alert box on click
 * @return       string
 */
function tooltip2($text, $with_alt = TRUE, $with_popup = FALSE) {

    $ret = [];

    if ($with_popup) {
        $ret['onClick'] = "alert('".JSReady($text, "alert")."');";
    }

    $text = preg_replace("/(\n\r|\r\n|\n|\r)/", " ", $text);
    $text = htmlReady($text);

    if ($with_alt) {
        $ret['alt'] = $text;
    }
    $ret['title'] = $text;

    return $ret;
}

/**
 * returns a html-snippet with an icon and a tooltip on it
 *
 * @param string $text tooltip text, html gets encoded
 * @param bool $important render icon in "important" style
 * @param bool $html tooltip text is HTML content
 */
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
function tooltipIcon($text, $important = false, $html = false): string
{
    if (!trim($text)) {
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        return '';
    }

    // render tooltip
    $template = $GLOBALS['template_factory']->open('shared/tooltip');
    return $template->render(compact('text', 'important', 'html'));
}

/**
 * returns a html-snippet with an icon and a tooltip on it
 *
 * @param string $text tooltip text, html is rendered as is
 * @param bool $important render icon in "important" style
 */
function tooltipHtmlIcon($text, $important = false)
{
    // render tooltip
    $html = true;
    $template = $GLOBALS['template_factory']->open('shared/tooltip');
    return $template->render(compact('text', 'important', 'html'));
}

/**
 * detects internal links in a given string and convert used domain to the domain
 * actually used (only necessary if more than one domain exists), relative URLs are
 * converted to absolute URLs
 *
 * @param    string  $str URL/Link to convert
 * @return   string  converted URL/Link
*/
function TransformInternalLinks($str){
    $str = trim($str);
    if (mb_strpos($str, 'http') !== 0) {
        if ($str[0] === '#' || preg_match('/^[a-z][a-z0-9+.-]*:/i', $str)) {
            return $str;
        }
        if ($str[0] === '/') {
            $str = mb_substr($str, mb_strlen($GLOBALS['CANONICAL_RELATIVE_PATH_STUDIP']));
        }
        $str = $GLOBALS['ABSOLUTE_URI_STUDIP'] . $str;
    }
Moritz Strohm's avatar
Moritz Strohm committed
    if (!empty($GLOBALS['STUDIP_DOMAINS']) && count($GLOBALS['STUDIP_DOMAINS']) > 1) {
        if (!isset($GLOBALS['TransformInternalLinks_domainData'])){
            $domain_data['domains'] = '';
            foreach ($GLOBALS['STUDIP_DOMAINS'] as $studip_domain) $domain_data['domains'] .= '|' . preg_quote($studip_domain);
            $domain_data['domains'] = preg_replace("'\|[^/|]*'", '$0[^/]*?', $domain_data['domains']);
            $domain_data['domains'] = mb_substr($domain_data['domains'], 1);
            $domain_data['user_domain'] = preg_replace("'^({$domain_data['domains']})(.*)$'i", "\\1", $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
            $domain_data['user_domain_scheme'] = 'http' . (($_SERVER['HTTPS'] || $_SERVER['SERVER_PORT'] == 443) ? 's' : '') . '://';
            $GLOBALS['TransformInternalLinks_domainData'] = $domain_data;
        }
        $domain_data = $GLOBALS['TransformInternalLinks_domainData'];
        return preg_replace("'https?\://({$domain_data['domains']})((/[^<\s]*[^\.\s<])*)'i", "{$domain_data['user_domain_scheme']}{$domain_data['user_domain']}\\2", $str);
    } else {
        return $str;
    }
}

/**
 * Displays the provided exception in a more readable fashion.
 *
 * @param Exception $exception The exception to be displayed
 * @param bool $as_html Indicates whether the exception shall be displayed as
 *                      plain text or html (optional, defaults to plain text)
 * @param bool $deep    Indicates whether any previous exception should be
 *                      included in the output (optional, defaults to false)
 * @return String The exception display either as plain text or html
 */
function display_exception($exception, $as_html = false, $deep = false) {
    $result  = '';
    $result .= sprintf("%s: %s\n", _('Typ'), get_class($exception));
    $result .= sprintf("%s: %s\n", _('Nachricht'), $exception->getMessage());
    $result .= sprintf("%s: %d\n", _('Code'), $exception->getCode());

    $trace = sprintf("  #$ %s(%u)\n", $exception->getFile(), $exception->getLine())
           . '  '  . str_replace("\n", "\n  ", $exception->getTraceAsString());
    $trace = str_replace($GLOBALS['STUDIP_BASE_PATH'] . '/', '', $trace);
    $result .= sprintf("%s:\n%s\n", _('Stack trace'), $trace);

    if ($deep && $exception->getPrevious()) {
        $result .= "\n";
        $result .= _('Vorherige Exception:') . "\n";
        $result .= display_exception($exception->getPrevious(), false, $deep);
    }

    return $as_html ? nl2br(htmlReady($result)) : $result;
}

/**
 * Returns the appropriate stud.ip icon for a given mime type.
 *
 * @param String $mime_type Mime type to get the icon for
 * @return String Icon path for the mime type
 */
//DEPRECATED: replaced by FileManager::getIconNameForMimeType
//TODO: test: lib/extern/modules/ExternModuleDownload.class.php
//TODO: test: lib/extern/modules/ExternModuleTemplateDownload.class.php
/*
function get_icon_for_mimetype($mime_type)
{

    $icons_application = [
        'file-pdf' => ['pdf'],
        'file-ppt' => ['powerpoint','presentation'],
        'file-excel' => ['excel', 'spreadsheet', 'csv'],
        'file-word' => ['word', 'wordprocessingml', 'opendocument.text', 'rtf'],
        'file-archive' => ['zip', 'rar', 'arj', '7z' ]
        ];
    list($type, $subtype) = explode('/', $mime_type);
    switch ($type) {
        case 'image':
            $ret = 'file-pic';
        break;
        case 'audio':
            $ret = 'file-audio';
            break;
        case 'video':
            $ret = 'file-video';
            break;
        case 'text':
            $ret = 'file-text';
            if (preg_match('/csv|comma-separated-values/i', $subtype)) {
                $ret = 'file-excel';
            }
            break;
        case 'application':
            $ret = 'file-generic';
            foreach($icons_application as $icon => $marker) {
                if (preg_match('/' . join('|', array_map('preg_quote', $marker)) . '/i', $subtype)) {
                    $ret = $icon;
                    break;
                }
            }
            break;
        default:
            $ret = 'file-generic';
    }

    return $ret;
}
*/

/**
 * Converts an array of attributes to an html attribute string.
 *
 * @param array $attributes Associative array of attributes
 * @return string
 * @since Stud.IP 4.1
 * @todo Nested attribute definitions?
 */
function arrayToHtmlAttributes(array $attributes) {
    // Filter empty attributes
    $attributes = array_filter($attributes, function ($value) {
        return isset($value) && $value !== false;
    });

    // Actual conversion
    $result = [];
    foreach ($attributes as $key => $value) {
        if ($value === true) {
            $result[] = htmlReady($key);
        } else {
            $result[] = sprintf('%s="%s"', htmlReady($key), htmlReady($value));
        }
    }
    return implode(' ', $result);
}