<?php
# Copyright (c) 2003-2005, Jannis Hermanns (on behalf the Serendipity Developer Team)
# All rights reserved.  See LICENSE file for licensing details

if (IN_serendipity !== true) {
    die ("Don't hack!");
}

/**
 * Check if an uploaded file is "evil"
 *
 * @access public
 * @param   string  Input filename
 * @return boolean
 */
function serendipity_isActiveFile($file) {
    if (preg_match('@^\.@', $file)) {
        return true;
    }

    $core = preg_match('@\.(php.*|[psj]html?|pht|aspx?|cgi|jsp|py|pl)$@i', $file);
    if ($core) {
        return true;
    }

    $eventData = false;
    serendipity_plugin_api::hook_event('backend_media_check', $eventData, $file);
    return $eventData;
}

/**
 * Gets a list of media items from our media database
 *
 * LONG
 *
 * @access public
 * @param   int     The offset to start fetching media files
 * @param   int     How many items to fetch
 * @param   int     The number (referenced variable) of fetched items
 * @param   string  The "ORDER BY" sql part when fetching items
 * @param   string  Order by DESC or ASC
 * @param   string  Only fetch files from a specific directory
 * @param   string  Only fetch specific filenames (including check for realname match)
 * @param   string  Only fetch media with specific keyword
 * @param   array   An array of restricting filter sets
 * @param   boolean Apply strict directory checks, or include subdirectories?
 * @return  array   Resultset of images
 */
function serendipity_fetchImagesFromDatabase($start=0, $limit=0, &$total=null, $order = false, $ordermode = false, $directory = '', $filename = '', $keywords = '', $filter = array(), $hideSubdirFiles = false) {
    global $serendipity;

    $cond = array(
        'joinparts' => array(),
        'parts'     => array(),
    );

    $orderfields = serendipity_getImageFields();
    if (empty($order) || !isset($orderfields[$order])) {
        $order = 'i.date';
    }

    if (!is_array($filter)) {
        $filter = array();
    }

    if (empty($ordermode) || ($ordermode != 'DESC' && $ordermode != 'ASC')) {
        $ordermode = 'DESC';
    }

    if ($order == 'name') {
        $order = 'realname ' . $ordermode . ', name';
    }

    if ($limit != 0) {
        $limitsql = serendipity_db_limit_sql(serendipity_db_limit($start, $limit));
    } else {
        $limitsql = '';
    }

    if ($hideSubdirFiles == false) {
        if (!empty($directory)) {
            $cond['parts']['directory'] = " AND i.path LIKE '" . serendipity_db_escape_string($directory) . "%'\n";
        }
        // if in root, having no directory set, the query fetches all files by default, meaning we are done
    } else {
        if (!empty($directory)) {
            $cond['parts']['directory'] = " AND i.path LIKE '" . serendipity_db_escape_string($directory) . "%'\n";
        } else {
            $cond['parts']['directory'] = " AND i.path = ''\n";
        }
    }

    if (!empty($filename)) {
        $cond['parts']['filename'] = " AND (i.name     like '%" . serendipity_db_escape_string($filename) . "%' OR
                  i.realname like '%" . serendipity_db_escape_string($filename) . "%')\n";
    }

    if (!is_array($keywords)) {
        if (!empty($keywords)) {
            $keywords = explode(';', $keywords);
        } else {
            $keywords = array();
        }
    }

    if (count($keywords) > 0) {
        $cond['parts']['keywords'] = " AND (mk.property IN ('" . serendipity_db_implode("', '", $keywords, 'string') . "'))\n";
        $cond['joinparts']['keywords'] = true;
    }
    foreach($filter AS $f => $fval) {
        if (! (isset($orderfields[$f]) || $f == "fileCategory") || empty($fval)) {
            continue;
        }
        
        $cond['parts']['filter'] = '';
        
        if (is_array($fval)) {
            if (empty($fval['from']) || empty($fval['to'])) {
                continue;
            }

            if ($orderfields[$f]['type'] == 'date') {
                $fval['from'] = serendipity_convertToTimestamp(trim($fval['from']));
                $fval['to']   = serendipity_convertToTimestamp(trim($fval['to']));
            }

            if (substr($f, 0, 3) === 'bp.') {
                $realf = substr($f, 3);
                $cond['parts']['filter'] .= " AND (bp2.property = '$realf' AND bp2.value >= " . (int)$fval['from'] . " AND bp2.value <= " . (int)$fval['to'] . ")\n";
            } else {
                $cond['parts']['filter'] .= " AND ($f >= " . (int)$fval['from'] . " AND $f <= " . (int)$fval['to'] . ")\n";
            }
        } elseif ($f == 'i.authorid') {
            $cond['parts']['filter'] .= " AND (
                                    (hp.property = 'authorid' AND hp.value = " . (int)$fval . ")
                                    OR
                                    (i.authorid = " . (int)$fval . ")
                                )\n";
            $cond['joinparts']['hiddenproperties'] = true;
        } elseif (($orderfields[$f]['type'] ?? null) == 'int') {
            if (substr($f, 0, 3) === 'bp.') {
                $realf = substr($f, 3);
                $cond['parts']['filter'] .= " AND (bp2.property = '$realf' AND bp2.value = '" . serendipity_db_escape_string(trim($fval)) . "')\n";
            } else {
                $cond['parts']['filter'] .= " AND ($f = '" . serendipity_db_escape_string(trim($fval)) . "')\n";
            }
        } elseif ($f == 'fileCategory') {
            switch ($fval) {
                case 'image':
                    $cond['parts']['filter'] .= " AND (i.mime LIKE 'image/%')\n";
                    break;
                case 'video':
                    $cond['parts']['filter'] .= " AND (i.mime LIKE 'video/%')\n";
                    break;
            }
        } else {
            if (substr($f, 0, 3) === 'bp.') {
                $realf = substr($f, 3);
                $cond['parts']['filter'] .= " AND (bp2.property = '$realf' AND bp2.value LIKE '%" . serendipity_db_escape_string(trim($fval)) . "%')\n";
            } else {
                $cond['parts']['filter'] .= " AND ($f LIKE '%" . serendipity_db_escape_string(trim($fval)) . "%')\n";
            }
        }
        $cond['joinparts']['filterproperties'] = true;
    }

    if (isset($serendipity['authorid']) && !serendipity_checkPermission('adminImagesViewOthers')) {
        $cond['parts']['authorid'] .= " AND (i.authorid = 0 OR i.authorid = " . (int)$serendipity['authorid'] . ")\n";
    }

    $cond['and']  = 'WHERE 1=1 ' . implode("\n", $cond['parts']);
    $cond['args'] = func_get_args();
    serendipity_plugin_api::hook_event('fetch_images_sql', $cond);
    serendipity_ACL_SQL($cond, false, 'directory');

    if ($cond['joinparts']['keywords'] ?? false) {
        $cond['joins'] .= "\n LEFT OUTER JOIN {$serendipity['dbPrefix']}mediaproperties AS mk
                                        ON (mk.mediaid = i.id AND mk.property_group = 'base_keyword')\n";
    }

    if (substr($order, 0, 3) === 'bp.') {
        $cond['orderproperty'] = substr($order, 3);
        $cond['orderkey']   = 'bp.value';
        $order              = 'bp.value';
        $cond['joinparts']['properties'] = true;
    } else {
        $cond['orderkey'] = "''";
    }

    if ($cond['joinparts']['properties'] ?? false) {
        $cond['joins'] .= "\n LEFT OUTER JOIN {$serendipity['dbPrefix']}mediaproperties AS bp
                                        ON (bp.mediaid = i.id AND bp.property_group = 'base_property' AND bp.property = '{$cond['orderproperty']}')\n";
    }

    if ($cond['joinparts']['filterproperties'] ?? false) {
        $cond['joins'] .= "\n LEFT OUTER JOIN {$serendipity['dbPrefix']}mediaproperties AS bp2
                                        ON (bp2.mediaid = i.id AND bp2.property_group = 'base_property')\n";
    }

    if ($cond['joinparts']['hiddenproperties'] ?? false) {
        $cond['joins'] .= "\n LEFT OUTER JOIN {$serendipity['dbPrefix']}mediaproperties AS hp
                                        ON (hp.mediaid = i.id AND hp.property_group = 'base_hidden')\n";
    }

    if ($serendipity['dbType'] == 'postgres' ||
        $serendipity['dbType'] == 'pdo-postgres') {
        $cond['group']    = '';
        $cond['distinct'] = 'DISTINCT';
    } else {
        $cond['group']    = 'GROUP BY i.id';
        $cond['distinct'] = '';
    }

    $basequery = "FROM {$serendipity['dbPrefix']}images AS i
       LEFT OUTER JOIN {$serendipity['dbPrefix']}authors AS a
                    ON i.authorid = a.authorid
                       {$cond['joins']}

                       {$cond['and']}";

    $query = "SELECT {$cond['distinct']} i.id, {$cond['orderkey']} AS orderkey, i.name, i.extension, i.mime, i.size, i.dimensions_width, i.dimensions_height, i.date, i.thumbnail_name, i.authorid, i.path, i.hotlink, i.realname,
                     a.realname AS authorname
                     $basequery
                     {$cond['group']}
            ORDER BY $order $ordermode $limitsql";

    $rs = serendipity_db_query($query, false, 'assoc');

    if (!is_array($rs) && $rs !== true && $rs !== 1) {
        echo '<div>' . $rs . '</div>';
        return array();
    } elseif (!is_array($rs)) {
        return array();
    }

    $total_query = "SELECT count(i.id)
                           $basequery
                           GROUP BY i.id";
    $total_rs = serendipity_db_query($total_query, false, 'num');
    if (is_array($total_rs)) {
        $total = count($total_rs);
    }

    return $rs;
}

/**
 * Fetch a specific media item from the mediadatabase
 *
 * @access public
 * @param   int     The ID of an media item
 * @return  array   The media info data
 */
function serendipity_fetchImageFromDatabase($id, $mode = 'read') {
    global $serendipity;

    if (is_array($id)) {
        $cond = array(
            'and' => "WHERE i.id IN (" . serendipity_db_implode(',', $id) . ")"
        );
        $single   = false;
        $assocKey = 'id';
        $assocVal = false;
    } else {
        $cond = array(
            'and' => "WHERE i.id = " . (int)$id
        );
        $single   = true;
        $assocKey = false;
        $assocVal = false;
    }

    if ($serendipity['dbType'] == 'postgres' ||
        $serendipity['dbType'] == 'pdo-postgres') {
        $cond['group']    = '';
        $cond['distinct'] = 'DISTINCT';
    } else {
        $cond['group']    = 'GROUP BY i.id';
        $cond['distinct'] = '';
    }

    if ($mode != 'discard') {
        serendipity_ACL_SQL($cond, false, 'directory', $mode);
    }

    $rs = serendipity_db_query("SELECT {$cond['distinct']} i.id, i.name, i.extension, i.mime, i.size, i.dimensions_width, i.dimensions_height, i.date, i.thumbnail_name, i.authorid, i.path, i.hotlink, i.realname
                                  FROM {$serendipity['dbPrefix']}images AS i
                                       {$cond['joins']}
                                       {$cond['and']}
                                       {$cond['group']}", $single, 'assoc', false, $assocKey, $assocVal);
    return $rs;
}

/**
 * Update a media item
 *
 * @access public
 * @param   array       An array of columns to update
 * @param   int         The ID of an media item to update
 * @return  boolean
 */
function serendipity_updateImageInDatabase($updates, $id) {
    global $serendipity;

    $admin = '';
    if (!serendipity_checkPermission('adminImagesAdd')) {
        $admin = ' AND (authorid = ' . $serendipity['authorid'] . ' OR authorid = 0)';
    }

    $i = 0;
    if (sizeof($updates) > 0) {
        $imageBeforeChange = serendipity_fetchImageFromDatabase($id);
        foreach ($updates AS $k => $v) {
            $q[] = $k ." = '" . serendipity_db_escape_string($v) . "'";
        }
        serendipity_db_query("UPDATE {$serendipity['dbPrefix']}images SET ". implode(',', $q) ." WHERE id = " . (int)$id . " $admin");
        $i++;

        // Check if this update changes important directory or filename attributes.
        // If yes, the plugin API will forward this change of files to plugins like
        // staticpage, so that they can update their contents.
        if (isset($updates['path']) || isset($updates['realname'])) {
            if (! isset($updates['path'])) {
                $updates['path'] = $imageBeforeChange['path'];
            }
            if (! isset($updates['realname'])) {
                $updates['realname'] = $imageBeforeChange['realname'];
            }

            // NOTE: Previously, the API supported multiple rename tasks like dir, filedir and file
            // Now the core will ALWAYS propagate each file change distinctly, never will a directory
            // be submitted to the API.
            $eventData = array( // array in array because the event api expects that
                array(
                    'type'      => 'file',
                    'oldDir'    => $imageBeforeChange['path'] . $imageBeforeChange['realname'],
                    'newDir'    => $updates['path'] . $updates['realname']
                ) 
            );
            serendipity_plugin_api::hook_event('backend_media_rename', $eventData);
        }

        // If the user manually saved the image properties, some values might need updating now
        // Name and realname are saved there as well, and the title is set to the name by default
        if (isset($updates['realname'])) {
            $q = "UPDATE {$serendipity['dbPrefix']}mediaproperties
                    SET value = '" . serendipity_db_escape_string($updates['realname']) . "'
                  WHERE mediaid = " . (int)$imageBeforeChange['id'] . ' AND property = "realname" AND value = "' . $imageBeforeChange['realname'] . '"';
            serendipity_db_query($q);
            $q = "UPDATE {$serendipity['dbPrefix']}mediaproperties
                    SET value = '" . serendipity_db_escape_string($updates['realname']) . "'
                  WHERE mediaid = " . (int)$imageBeforeChange['id'] . ' AND property = "TITLE" AND value = "' . $imageBeforeChange['realname'] .'"';
            serendipity_db_query($q);
        }
        if (isset($updates['name'])) {
            $q = "UPDATE {$serendipity['dbPrefix']}mediaproperties
                    SET value = '" . $updates['name'] . "'
                  WHERE mediaid = " . (int)$imageBeforeChange['id'] . ' AND property = "name" AND value = "' . $imageBeforeChange['name'] .'"';
            serendipity_db_query($q);
        }
    }
    return $i;
}

/**
 * Delete a media item
 *
 * @access public
 * @param   int     The ID of a media item to delete
 * @return
 */
function serendipity_deleteImage($id) {
    global $serendipity;
    $dThumb   = array();
    $messages = '';

    $file   = serendipity_fetchImageFromDatabase($id);

    if (!is_array($file)) {
        $messages .= sprintf('<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . FILE_NOT_FOUND . "</span>\n", $id);
        //return false;
    } else {

        $dFile  = $file['path'] . $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);

        $dThumb = array(array(
            'fthumb' => $file['thumbnail_name']
        ));

        if (!serendipity_checkPermission('adminImagesDelete')) {
            return;
        }

        if (!serendipity_checkPermission('adminImagesMaintainOthers') && $file['authorid'] != '0' && $file['authorid'] != $serendipity['authorid']) {
            // A non-admin user may not delete private files from other users.
            return;
        }

        if (!$file['hotlink']) {
            if (file_exists($serendipity['serendipityPath'] . $serendipity['uploadPath'] . $dFile)) {
                if (@unlink($serendipity['serendipityPath'] . $serendipity['uploadPath'] . $dFile)) {
                    $messages .= sprintf('<span class="msg_success"><span class="icon-ok-circled" aria-hidden="true"></span> ' . DELETE_FILE . "</span>\n", $dFile);
                } else {
                    $messages .= sprintf('<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . DELETE_FILE_FAIL . "</span>\n", $dFile);
                }

                serendipity_plugin_api::hook_event('backend_media_delete', $dThumb);
                foreach($dThumb AS $thumb) {
                    $dfnThumb = $file['path'] . $file['name'] . (!empty($thumb['fthumb']) ? '.' . $thumb['fthumb'] : '') . (empty($file['extension']) ? '' : '.' . $file['extension']);
                    $dfThumb  = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $dfnThumb;

                    if (@unlink($dfThumb)) {
                        $messages .= sprintf('<span class="msg_success"><span class="icon-ok-circled" aria-hidden="true"></span> ' . DELETE_THUMBNAIL . "</span>\n", $dfnThumb);
                    }
                }
            } else {
                $messages .= sprintf('<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . FILE_NOT_FOUND . "</span>\n", $dFile);
            }
        } else {
            $messages .= sprintf('<span class="msg_hint"><span class="icon-help-circled" aria-hidden="true"></span> ' . DELETE_HOTLINK_FILE . "</span>\n", $file['name']);
        }

        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}images WHERE id = ". (int)$id);
        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}mediaproperties WHERE mediaid = ". (int)$id);
    }

    return $messages;
}

/**
 * Open a directory and fetch all existing media items
 *
 * @access public
 * @param   boolean     deprecated
 * @param   int         deprecated
 * @param   int         deprecated
 * @param   array       Array list of found items
 * @param   string      sub-directory to investigate [recursive use]
 * @return  array       List of media items
 */
function serendipity_fetchImages($group = false, $start = 0, $end = 20, $images = '', $odir = '') {
    global $serendipity;

    // Open directory
    $basedir = $serendipity['serendipityPath'] . $serendipity['uploadPath'];
    $images = array();
    if ($dir = @opendir($basedir . $odir)) {
        $aTempArray = array();
        while (($file = @readdir($dir)) !== false) {
            if ($file == '.svn' || $file == 'CVS' || $file == '.htaccess' || strtolower($file) == 'thumbs.db' || $file == '.' || $file == '..') {
                continue; // 2013/06/05 added exclude .htaccess for ckeditor/kcfinder usage and 2013/12/25 added thumbs.db
            }
            array_push($aTempArray, $file);
        }
        @closedir($dir);
        sort($aTempArray);
        foreach($aTempArray AS $f) {
            if (strpos($f, $serendipity['thumbSuffix']) !== false) {
                // This is a s9y thumbnail, skip it.
                continue;
            }

            $cdir = ($odir != '' ? $odir . '/' : '');
            if (is_dir($basedir . $odir . '/' . $f)) {
                $temp = serendipity_fetchImages($group, $start, $end, $images, $cdir . $f);
                foreach($temp AS $tkey => $tval) {
                    array_push($images, $tval);
                }
            } else {
                array_push($images, $cdir . $f);
            }
        }
    }
    natsort($images);

    /* BC */
    $serendipity['imageList'] = $images;
    return $images;
}

/**
 * Inserts a hotlinked media file
 *
 * hotlinks are files that are only linked in your database, and not really stored on your server
 *
 * @access public
 * @param   string      The filename to hotlink
 * @param   string      The URL to hotlink with
 * @param   int         The owner of the hotlinked media item
 * @param   int         The timestamp of insertion (unix second)
 * @param   string      A temporary filename for fetching the file to investigate it
 * @return  int         The ID of the inserted media item
 */
function serendipity_insertHotlinkedImageInDatabase($filename, $url, $authorid = 0, $time = NULL, $tempfile = NULL) {
    global $serendipity;

    if (is_null($time)) {
        $time = time();
    }

    list($filebase, $extension) = serendipity_parseFileName($filename);

    if ($tempfile && file_exists($tempfile)) {
        $filesize = @filesize($tempfile);
        $fdim     = @serendipity_getimagesize($tempfile, '', $extension);
        $width    = $fdim[0];
        $height   = $fdim[1];
        $mime     = $fdim['mime'];
        @unlink($tempfile);
    }

    $query = sprintf(
      "INSERT INTO {$serendipity['dbPrefix']}images (
                    name,
                    date,
                    authorid,
                    extension,
                    mime,
                    size,
                    dimensions_width,
                    dimensions_height,
                    path,
                    hotlink,
                    realname
                   ) VALUES (
                    '%s',
                    %s,
                    %s,
                    '%s',
                    '%s',
                    %s,
                    %s,
                    %s,
                    '%s',
                    1,
                    '%s'
                   )",
      serendipity_db_escape_string($filebase),
      (int)$time,
      (int)$authorid,
      serendipity_db_escape_string($extension),
      serendipity_db_escape_string($mime),
      (int)$filesize,
      (int)$width,
      (int)$height,
      serendipity_db_escape_string($url),
      serendipity_db_escape_string($filename)
    );

    $sql = serendipity_db_query($query);
    if (is_string($sql)) {
        echo '<span class="block_level">' . $query . "</span>\n";
        echo '<span class="block_level">' . $sql . "</span>\n";
    }

    $image_id = serendipity_db_insert_id('images', 'id');
    if ($image_id > 0) {
        return $image_id;
    }

    return 0;
}

/**
 * Insert a media item in the database
 *
 * @access public
 * @param   string      The filename of the media item
 * @param   string      The path to the media item
 * @param   int         The owner author of the item
 * @param   int         The timestamp of when the media item was inserted
 * @return  int         The new media ID
 */
function serendipity_insertImageInDatabase($filename, $directory, $authorid = 0, $time = NULL, $realname = NULL) {
    global $serendipity;

    if (is_null($time)) {
        $time = time();
    }

    if (is_null($realname)) {
        $realname = $filename;
    }

    $filepath = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $directory . $filename;
    $filesize = @filesize($filepath);

    list($filebase, $extension) = serendipity_parseFileName($filename);

    $thumbpath = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $directory . $filebase . '.'. $serendipity['thumbSuffix'] . (empty($extension) ? '' : '.' . $extension);
    $thumbnail = (file_exists($thumbpath) ? $serendipity['thumbSuffix'] : '');

    $fdim   = @serendipity_getimagesize($filepath, '', $extension);
    $width  = $fdim[0];
    $height = $fdim[1];
    $mime   = $fdim['mime'];

    $query = sprintf(
      "INSERT INTO {$serendipity['dbPrefix']}images (
                    name,
                    extension,
                    mime,
                    size,
                    dimensions_width,
                    dimensions_height,
                    thumbnail_name,
                    date,
                    authorid,
                    path,
                    realname
                   ) VALUES (
                    '%s',
                    '%s',
                    '%s',
                    %s,
                    %s,
                    %s,
                    '%s',
                    %s,
                    %s,
                    '%s',
                    '%s'
                   )",
      serendipity_db_escape_string($filebase),
      serendipity_db_escape_string($extension),
      serendipity_db_escape_string($mime),
      (int)$filesize,
      (int)$width,
      (int)$height,
      serendipity_db_escape_string($thumbnail),
      (int)$time,
      (int)$authorid,
      serendipity_db_escape_string($directory),
      serendipity_db_escape_string($realname)
    );

    $sql = serendipity_db_query($query);
    if (is_string($sql)) {
        echo '<span class="block_level">' . $query . "</span>\n";
        echo '<span class="block_level">' . $sql . "</span>\n";
    }

    $image_id = serendipity_db_insert_id('images', 'id');
    if ($image_id > 0) {
        return $image_id;
        serendipity_cleanCache();
    }

    return 0;
}

/**
 * Create a thumbnail for an image
 *
 * LONG
 *
 * @access public
 * @param   string      The input image filename
 * @param   string      The directory to the image file
 * @param   string      The target size of the thumbnail (2-dimensional array width,height)
 * @param   string      Name of the thumbnail
 * @param   bool        Store thumbnail in temporary place?
 * @param   bool        Force enlarging of small images?
 * @return  array       The result size of the thumbnail
 */
function serendipity_makeThumbnail($file, $directory = '', $size = false, $thumbname = false, $is_temporary = false, $force_resize = false) {
    global $serendipity;

    if ($size === false) {
        $size = $serendipity['thumbSize'];
    }
    if ($size < 1) {
       return array(0,0);
    }

    if ($thumbname === false) {
        $thumbname = $serendipity['thumbSuffix'];
    }

    $t       = serendipity_parseFileName($file);
    $f       = $t[0];
    $suf     = $t[1];

    $infile  = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $directory . $file;
    #echo 'From: ' . $infile . '<br />';
    if ($is_temporary) {
        $temppath = dirname($thumbname);
        if (!is_dir($temppath)) {
            @mkdir($temppath);
        }
        $outfile = $thumbname;
    } else {
        $outfile = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $directory . $f . '.' . $thumbname . '.' . $suf;
    }
    $serendipity['last_outfile'] = $outfile;
    #echo 'To: ' . $outfile . '<br />';

    $fdim = @serendipity_getimagesize($infile, '', $suf);
    if (isset($fdim['noimage'])) {
        $r = array(0, 0);
    } else {
        if ($serendipity['magick'] !== true) {
            if (is_array($size)) {
                // The caller wants a thumbnail with a specific size
                $r = serendipity_resize_image_gd($infile, $outfile, $size['width'], $size['height']);
            } else {
                // The caller wants a thumbnail constrained in the dimension set by config
                $calc = serendipity_calculate_aspect_size($fdim[0], $fdim[1], $size, $serendipity['thumbConstraint']);
                $r    = serendipity_resize_image_gd($infile, $outfile, $calc[0], $calc[1]);
            }
        } else {
            if (is_array($size)) {
                if ($fdim[0] > $size['width'] && $fdim[1] > $size['height']) {
                    $r = $size;
                } else {
                    return array(0,0); // do not create any thumb, if image is smaller than defined sizes
                }
            } else {
                $calc = serendipity_calculate_aspect_size($fdim[0], $fdim[1], $size, $serendipity['thumbConstraint']);
                $r    = array('width' => $calc[0], 'height' => $calc[1]);
            }
            $newSize = $r['width'] . 'x' . $r['height'];
            if ($fdim['mime'] == 'application/pdf') {
                $cmd = escapeshellcmd($serendipity['convert']) . ' -antialias -flatten -scale '. serendipity_escapeshellarg($newSize) .' '. serendipity_escapeshellarg($infile . '[0]') . ' ' . serendipity_escapeshellarg($outfile . '.png');
            } else {
                if (!$force_resize && serendipity_ini_bool(ini_get('safe_mode')) === false) {
                    $newSize .= '>'; // tell imagemagick to not enlarge small images, only works if safe_mode is off (safe_mode turns > in to \>)
                }
                if (!$serendipity['imagemagick_nobang']) $newSize .= '!'; // force the first run image geometry exactly to given sizes, if there were rounding differences (see https://github.com/s9y/Serendipity/commit/94881ba4c0e3bdd4b5fac510e93977e239171c1c and comments)
                $cmd = escapeshellcmd($serendipity['convert'] . ' ' . $serendipity['imagemagick_thumb_parameters']) . ' -antialias -resize ' . serendipity_escapeshellarg($newSize) . ' ' . serendipity_escapeshellarg($infile) .' '. serendipity_escapeshellarg($outfile);
            }
            exec($cmd, $output, $result);
            if ($result != 0) {
                echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . sprintf(IMAGICK_EXEC_ERROR, $cmd, $output[0], $result) ."</span>\n";
                $r = false; // return failure
            } else {
               touch($outfile);
            }
            unset($output, $result);
        }
    }
    return $r;
}

/**
 * Scale an image
 *
 * LONG
 *
 * @access public
 * @param   int     The ID of an image
 * @param   int     The target width
 * @param   int     The target height
 * @return true
 */
function serendipity_scaleImg($id, $width, $height) {
    global $serendipity;

    $file = serendipity_fetchImageFromDatabase($id);
    if (!is_array($file)) {
        return false;
    }

    $admin = '';
    if (!serendipity_checkPermission('adminImagesMaintainOthers') && $file['authorid'] != '0' && $file['authorid'] != $serendipity['authorid']) {
        return;
    }

    $infile = $outfile = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $file['path'] . $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);

    if ($serendipity['magick'] !== true) {
        serendipity_resize_image_gd($infile, $outfile, $width, $height);
    } else {
        $cmd = escapeshellcmd($serendipity['convert']) . ' -scale ' .  serendipity_escapeshellarg($width . 'x' . $height) . ' ' . serendipity_escapeshellarg($infile) . ' ' . serendipity_escapeshellarg($outfile);
        exec($cmd, $output, $result);
        if ( $result != 0 ) {
            echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . sprintf(IMAGICK_EXEC_ERROR, $cmd, $output[0], $result) ."</span>\n";
            return false;
        }
        unset($output, $result);
    }

    if ($result == 0) {
        serendipity_updateImageInDatabase(array('dimensions_width' => $width, 'dimensions_height' => $height, 'size' => @filesize($outfile)), $id);
        return true;
    }
    return false;
}

/**
 * Rotate an image
 *
 * LONG
 *
 * @access public
 * @param   int     The ID of an image
 * @param   int     Number of degrees to rotate
 * @return boolean
 */
function serendipity_rotateImg($id, $degrees) {
    global $serendipity;

    $file = serendipity_fetchImageFromDatabase($id);
    if (!is_array($file)) {
        return false;
    }

    $admin = '';
    if (!serendipity_checkPermission('adminImagesMaintainOthers') && $file['authorid'] != '0' && $file['authorid'] != $serendipity['authorid']) {
        // A non-admin user may not delete private files from other users.
        return false;
    }

    $infile = $outfile = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $file['path'] . $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
    $thumbnails = serendipity_getThumbnails($id);

    if ($serendipity['magick'] !== true) {
        serendipity_rotate_image_gd($infile, $outfile, $degrees);
        foreach($thumbnails as $thumbnail) {
            $infileThumb = $outfileThumb = $thumbnail;
            serendipity_rotate_image_gd($infileThumb, $outfileThumb, $degrees);
        }
    } else {
        /* Why can't we just all agree on the rotation direction? */
        $degrees = (360 - $degrees);

        /* rotate main image */
        $cmd = escapeshellcmd($serendipity['convert']) . ' -rotate ' . serendipity_escapeshellarg($degrees) . ' ' . serendipity_escapeshellarg($infile) . ' ' . serendipity_escapeshellarg($outfile);
        exec($cmd, $output, $result);
        if ( $result != 0 ) {
            echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . sprintf(IMAGICK_EXEC_ERROR, $cmd, $output[0], $result) ."</span>\n";
        }
        unset($output, $result);

        /* rotate thumbnail */
        foreach($thumbnails as $thumbnail) {
            $infileThumb = $outfileThumb = $thumbnail;
            $cmd = escapeshellcmd($serendipity['convert']) . ' -rotate ' . serendipity_escapeshellarg($degrees) . ' ' . serendipity_escapeshellarg($infileThumb) . ' ' . serendipity_escapeshellarg($outfileThumb);
            exec($cmd, $output, $result);
            if ( $result != 0 ) {
                echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . sprintf(IMAGICK_EXEC_ERROR, $cmd, $output[0], $result) ."</span>\n";
            }
            unset($output, $result);
        }

    }

    $fdim = @getimagesize($outfile);

    serendipity_updateImageInDatabase(array('dimensions_width' => $fdim[0], 'dimensions_height' => $fdim[1]), $id);

    return true;
}

/**
 * Creates thumbnails for all images in the upload dir
 *
 * @access public
 * @return  int Number of created thumbnails
 */
function serendipity_generateThumbs() {
    global $serendipity;

    $i = 0;
    $serendipity['imageList'] = serendipity_fetchImagesFromDatabase(0, 0, $total);
    $msg_printed = false;

    foreach ($serendipity['imageList'] AS $k => $file) {
        $is_image = serendipity_isImage($file);

        if ($is_image && !$file['hotlink']) {
            $update   = false;
            $filename = $file['path'] . $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
            $ffull    = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $filename;

            if (!file_exists($ffull)) {
                serendipity_deleteImage($file['id']);
                continue;
            }

            if (empty($file['thumbnail_name'])) {
                $file['thumbnail_name'] = $serendipity['thumbSuffix'];
            }

            $oldThumb = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $file['path'] . $file['name'] . '.' . $file['thumbnail_name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
            $newThumb = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $file['path'] . $file['name'] . '.' . $serendipity['thumbSuffix'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
            $sThumb   = $file['path'] . $file['name'] . '.' . $serendipity['thumbSuffix'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
            $fdim     = @getimagesize($ffull);

            if (!file_exists($oldThumb) && !file_exists($newThumb) && ($fdim[0] > $serendipity['thumbSize'] || $fdim[1] > $serendipity['thumbSize'])) {
                $returnsize = serendipity_makeThumbnail($file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']), $file['path']);
                if ($returnsize !== false ) {
                    // Only print the resize message the first time
                    if (!$msg_printed) {
                        $resizemedia = sprintf(RESIZE_BLAHBLAH, THUMBNAIL_SHORT);
                        printf('<span class="msg_notice"><span class="icon-info-circled" aria-hidden="true"></span> ' . $resizemedia . "</span>\n");
                        echo "\n" . '<ul class="serendipityFileList">' . "\n";
                        $msg_printed = true;
                    }
                    echo '<li>' . $sThumb . ': ' . $returnsize['width'] . 'x' . $returnsize['height'] . "</li>\n";
                    if (!file_exists($newThumb)) {
                        printf('<li><span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . THUMBNAIL_FAILED_COPY . '</span></li>' . "\n", $sThumb);
                    } else {
                        $update = true;
                    }
                }
            } elseif (!file_exists($oldThumb) && !file_exists($newThumb) && $fdim[0] <= $serendipity['thumbSize'] && $fdim[1] <= $serendipity['thumbSize']) {
                if (!$msg_printed) {
                    $resizethumb = sprintf(RESIZE_BLAHBLAH, THUMB);
                    printf('<span class="msg_notice"><span class="icon-info-circled" aria-hidden="true"></span> ' . $resizethumb . "</span>\n");
                    echo "\n" . '<ul class="serendipityFileList">' . "\n";
                    $msg_printed = true;
                }
                $res = @copy($ffull, $newThumb);
                if (@$res === true) {
                    printf('<li>' . THUMBNAIL_USING_OWN . '</li>' . "\n", $sThumb);
                    $update = true;
                } else {
                    printf('<li><span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . THUMBNAIL_FAILED_COPY . '</span></li>' . "\n", $sThumb);
                }
            }

            if ($update) {
                $i++;
                $updates = array('thumbnail_name' => $serendipity['thumbSuffix']);
                serendipity_updateImageInDatabase($updates, $file['id']);
            }
        } else {
            // Currently, non-image files have no thumbnail.
        }
    }

    // Close the list, if it was created
    if ($msg_printed) {
        echo "</ul>\n";
    }


    return $i;
}

/**
 * Guess the MIME type of a file
 *
 * @access public
 * @param   string  Filename extension
 * @return  string  Mimetype
 */
function serendipity_guessMime($extension) {
    $mime = '';
    switch (strtolower($extension)) {
        case 'jpg':
        case 'jpeg':
            $mime = 'image/jpeg';
        break;

        case 'aiff':
        case 'aif':
            $mime = 'audio/x-aiff';
            break;

        case 'gif':
            $mime = 'image/gif';
        break;

        case 'png':
            $mime = 'image/png';
        break;

        case 'pdf':
            $mime = 'application/pdf';
            break;

        case 'doc':
            $mime = 'application/msword';
            break;

        case 'rtf':
            $mime = 'application/rtf';
            break;

        case 'wav':
        case 'wave':
            $mime = 'audio/x-wav';
            break;

        case 'mp2':
        case 'mpg':
        case 'mpeg':
            $mime = 'video/x-mpeg';
            break;

        case 'avi':
            $mime = 'video/x-msvideo';
            break;

        case 'mp3':
            $mime = 'audio/x-mpeg3';
            break;

        case 'xlm':
        case 'xlb':
        case 'xll':
        case 'xla':
        case 'xlw':
        case 'xlc':
        case 'xls':
        case 'xlt':
            $mime = 'application/vnd.ms-excel';
            break;

        case 'ppt':
        case 'pps':
            $mime = 'application/vnd.ms-powerpoint';
            break;

        case 'html':
        case 'htm':
            $mime = 'text/html';
            break;

        case 'xsl':
        case 'xslt':
        case 'xml':
        case 'wsdl':
        case 'xsd':
            $mime = 'text/xml';
            break;

        case 'zip':
            $mime = 'application/zip';
            break;

        case 'tar':
            $mime = 'application/x-tar';
            break;

        case 'tgz':
        case 'gz':
            $mime = 'application/x-gzip';
            break;

        case 'swf':
            $mime = 'application/x-shockwave-flash';
            break;

        case 'rm':
        case 'ra':
        case 'ram':
            $mime = 'application/vnd.rn-realaudio';
            break;

        case 'exe':
            $mime = 'application/octet-stream';
            break;

        case 'mov':
        case 'mp4':
        case 'qt':
            $mime = 'video/x-quicktime';
            break;

        case 'midi':
        case 'mid':
            $mime = 'audio/x-midi';
            break;

        case 'txt':
            $mime = 'text/plain';
            break;

        case 'qcp':
            $mime = 'audio/vnd.qcelp';
            break;

        case 'emf':
            $mime = 'image/x-emf';
            break;

        case 'wmf':
            $mime = 'image/x-wmf';
            break;

        case 'snd':
            $mime = 'audio/basic';
            break;

        case 'pmd':
            $mime = 'application/x-pmd';
            break;

        case 'wbmp':
            $mime = 'image/vnd.wap.wbmp';
            break;

        case 'gcd':
            $mime = 'text/x-pcs-gcd';
            break;

        case 'mms':
            $mime = 'application/vnd.wap.mms-message';
            break;

        case 'ogg':
        case 'ogm':
            $mime = 'application/ogg';
            break;

        case 'rv':
            $mime = 'video/vnd.rn-realvideo';
            break;

        case 'wmv':
            $mime = 'video/x-ms-wmv';
            break;

        case 'wma':
            $mime = 'audio/x-ms-wma';
            break;

        case 'qcp':
            $mime = 'audio/vnd.qcelp';
            break;

        case 'jad':
            $mime = 'text/vnd.sun.j2me.app-descriptor';
            break;

        case '3g2':
        case '3gp':
            $mime = 'video/3gpp';
            break;

        case 'jar':
            $mime = 'application/java-archive';
            break;

        case 'ico':
            $mime = 'image/x-icon';
            break;

        case 'svg':
            $mime = 'image/svg';
            break;

        default:
            $mime = 'application/octet-stream';
            break;
    }

    return $mime;
}

/**
 * Check all existing thumbnails if they are the right size, insert missing thumbnails
 *
 * LONG
 *
 * @access public
 * @return  int     Number of updated thumbnails
 */
function serendipity_syncThumbs($deleteThumbs = false) {
    global $serendipity;

    $i = 0;

    $files  = serendipity_fetchImages();
    $fcount = count($files);

    for ($x = 0; $x < $fcount; $x++) {
        $update = array();
        $f      = serendipity_parseFileName($files[$x]);
        if (empty($f[1]) || $f[1] == $files[$x]) {
            // No extension means bad file most probably. Skip it.
            printf('<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . SKIPPING_FILE_EXTENSION . "</span>\n", $files[$x]);
            continue;
        }

        $ffull   = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $files[$x];
        $fthumb  = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $f[0] . '.' . $serendipity['thumbSuffix'] . '.' . $f[1];
        $sThumb  = $f[0] . '.' . $serendipity['thumbSuffix'] . '.' . $f[1];
        $fbase   = basename($f[0]);
        $fdir    = dirname($f[0]) . '/';
        if ($fdir == './') {
            $fdir = '';
        }

        if (!is_readable($ffull) || filesize($ffull) == 0) {
            printf('<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . SKIPPING_FILE_UNREADABLE . "</span>\n", $files[$x]);
            continue;
        }

        $ft_mime = serendipity_guessMime($f[1]);
        $fdim    = serendipity_getimagesize($ffull, $ft_mime);

        // If we're supposed to delete thumbs, this is the easiest place. Leave messages plain unstiled.
        if (is_readable($fthumb)) {
            if ($deleteThumbs === true) {
                if (@unlink($fthumb)) {
                    printf(DELETE_THUMBNAIL . "<br />\n", $sThumb);
                    $i++;
                }
            } else if ($deleteThumbs == 'checksize') {
                // Find existing thumbnail dimensions
                $tdim = serendipity_getimagesize($fthumb);
                if ($tdim['noimage']) {
                    // Delete it so it can be regenerated
                    if (@unlink($fthumb)) {
                        printf(DELETE_THUMBNAIL . "<br />\n", $sThumb);
                        $i++;
                    }
                } else {
                    // Calculate correct thumbnail size from original image
                    $expect = serendipity_calculate_aspect_size($fdim[0], $fdim[1], $serendipity['thumbSize'], $serendipity['thumbConstraint']);
                    // Check actual thumbnail size
                    if ($tdim[0] != $expect[0] || $tdim[1] != $expect[1]) {
                        // This thumbnail is incorrect; delete it so
                        // it can be regenerated
                        if (@unlink($fthumb)) {
                            printf(DELETE_THUMBNAIL . "<br />\n", $sThumb);
                            $i++;
                        }
                    }
                }
            }
            // else the option is to keep all existing thumbs; do nothing.
        } // end if thumb exists

        $cond = array(
            'and' => "WHERE name = '" . serendipity_db_escape_string($fbase) . "'
                            " . ($fdir != '' ? "AND path = '" . serendipity_db_escape_string($fdir) . "'" : '') . "
                            AND mime = '" . serendipity_db_escape_string($fdim['mime']) . "'
                            AND extension = '" . serendipity_db_escape_string($f[1]) . "'"
        );
        serendipity_ACL_SQL($cond, false, 'directory');

        $rs = serendipity_db_query("SELECT *
                                      FROM {$serendipity['dbPrefix']}images AS i
                                           {$cond['joins']}

                                           {$cond['and']}", true, 'assoc');
        // Leave messages plain unstiled
        if (is_array($rs)) {
            // This image is in the database.  Check our calculated data against the database data.
            $update = array();
            // Is the width correct?
            if (isset($fdim[0]) && $rs['dimensions_width'] != $fdim[0]) {
                $update['dimensions_width'] = $fdim[0];
            }

            // Is the height correct?
            if (isset($fdim[1]) && $rs['dimensions_height'] != $fdim[1]) {
                $update['dimensions_height'] = $fdim[1];
            }

            // Is the image size correct?
            if ($rs['size'] != filesize($ffull)) {
                $update['size'] = filesize($ffull);
            }

            // Has the thumbnail suffix changed?
            $checkfile = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $rs['path'] . $rs['name'] . '.' . $rs['thumbnail_name'] . (empty($rs['extension']) ? '' : '.' . $rs['extension']);
            if (!file_exists($checkfile) && file_exists($fthumb)) {
                $update['thumbnail_name'] = $serendipity['thumbSuffix'];
            }

            /* Do the database update, if needed */
            if (sizeof($update) != 0) {
                printf(FOUND_FILE . ' (2)<br />', $files[$x]);
                serendipity_updateImageInDatabase($update, $rs['id']);
                $i++;
            }
        } else {
            printf(FOUND_FILE . ' (1)<br />', $files[$x]);
            serendipity_insertImageInDatabase($fbase . '.' . $f[1], $fdir, 0, filemtime($ffull));
            $i++;
        }
    }
    return $i;
}

/**
 * Wrapper for GDLib functions
 *
 * @access public
 * @param   string      Filename to operate on
 * @return string       Functionname to execute
 */
function serendipity_functions_gd($infilename) {
    if (!function_exists('imagecopyresampled')) {
        return false;
    }

    $func = array();
    $inf  = pathinfo(strtolower($infilename));
    switch ($inf['extension']) {
    case 'gif':
        $func['load'] = 'imagecreatefromgif';
        $func['save'] = 'imagegif';
        $func['qual'] = 100;
        break;

    case 'jpeg':
    case 'jpg':
    case 'jfif':
        $func['load'] = 'imagecreatefromjpeg';
        $func['save'] = 'imagejpeg';
        $func['qual'] = 100;
        break;

    case 'png':
        $func['load'] = 'imagecreatefrompng';
        $func['save'] = 'imagepng';
        $func['qual'] = 9;
        break;

    default:
        return false;
    }

    /* If our loader does not exist, we are doomed */
    if (!function_exists($func['load'])) {
        return false;
    }

    /* If the save function does not exist (i.e. read-only GIF), we want to output it as PNG */
    if (!function_exists($func['save'])) {
        if (function_exists('imagepng')) {
            $func['save'] = 'imagepng';
        } else {
            return false;
        }
    }

    return $func;
}

/**
 * Rotate an image (GDlib)
 *
 * @access public
 * @param   string      Source Filename to rotate
 * @param   string      Target file
 * @param   int         Degress to rotate
 * @return  array       New width/height of the image
 */
function serendipity_rotate_image_gd($infilename, $outfilename, $degrees)
{
    $func = serendipity_functions_gd($infilename);
    if (!is_array($func)) {
        return false;
    }

    $in        = $func['load']($infilename);

    $out       = imagerotate($in, $degrees, 0);
    $func['save']($out, $outfilename, $func['qual']);

    $newwidth  = imagesx($out);
    $newheight = imagesy($out);

    $out       = null;
    $in        = null;

    return array($newwidth, $newheight);
}

/**
 * Resize an image (GDLib)
 *
 * @access public
 * @param   string      Source Filename to resize
 * @param   string      Target file
 * @param   int         New width
 * @return  int         New height (can be autodetected)
 * @return  array       New image size
 */
function serendipity_resize_image_gd($infilename, $outfilename, $newwidth, $newheight=null)
{
    $func = serendipity_functions_gd($infilename);
    if (!is_array($func)) {
        return false;
    }

    try {
        // if an image exist that can not be loaded (invalid gif for example), the page shall still be rendered
        $in = $func['load']($infilename);
    } catch (Exception $e) {
        echo 'Could not create thumbnail: ',  $e->getMessage(), "\n";
        return false;
    }
    $width = imagesx($in);
    $height = imagesy($in);

    if (is_null($newheight)) {
        $newsizes  = serendipity_calculate_aspect_size($width, $height, $newwidth, 'width');
        $newwidth  = $newsizes[0];
        $newheight = $newsizes[1];
    }

    if (is_null($newwidth)) {
        $newsizes  = serendipity_calculate_aspect_size($width, $height, $newheight, 'height');
        $newwidth  = $newsizes[0];
        $newheight = $newsizes[1];
    }

    $out = imagecreatetruecolor($newwidth, $newheight);

    /* Attempt to copy transparency information, this really only works for PNG */
    if (function_exists('imagesavealpha')) {
        imagealphablending($out, false);
        imagesavealpha($out, true);
    }

    imagecopyresampled($out, $in, 0, 0, 0, 0, $newwidth, $newheight, $width, $height);
    @umask(0000);
    touch($outfilename); // safe_mode requirement
    $func['save']($out, $outfilename, $func['qual']);
    @chmod($outfilename, 0664);
    $out = null;
    $in  = null;

    return array($newwidth, $newheight);
}

/**
 * Calculate new size for an image, considering aspect ratio and constraint
 *
 * @access public
 * @param   int     Image width
 * @param   int     Image height
 * @param   int     Target dimension size
 * @param   string  Dimension to constrain ('width', 'height', 'largest',
 *                  'smallest'; defaults to original behavior, 'largest')
 * @return  array   An array with the scaled width and height
 */
function serendipity_calculate_aspect_size($width, $height, $size, $constraint = null) {

    // Allow for future constraints (idea: 'percent')
    $known_constraints = array('width', 'height', 'largest', 'smallest');

    // Rearrange params for calls from old imageselectorplus plugin
    if ($size == null) {
      $size       = $constraint;
      $constraint = 'smallest';
    }

    // Normalize relative constraint types
    if ($constraint == 'largest' || !in_array($constraint, $known_constraints)) {
        // Original default behavior, included for backwards compatibility
        // Constrains largest dimension
        if ($width >= $height) {
            $constraint = 'width';
        } else {
            $constraint = 'height';
        }
    } else if ($constraint == 'smallest') {
        // Only ever called from imageselectorplus plugin, included for
        // backwards compatibility with its older versions
        if ($width >= $height) {
            $constraint = 'height';
        } else {
            $constraint = 'width';
        }
    }

    // Constraint is now definitely one of the known absolute types,
    // either 'width' or 'height'
    if ($constraint == 'height') {
        // Is the image big enough to resize?
        if ($height > $size) {
            // Calculate new size
            $ratio    = $width / $height;
            $newwidth = round($size * $ratio);
            // Limit calculated dimension to at least 1px
            if ($newwidth <= 0) {
                $newwidth = 1;
            }
            $newsize = array($newwidth, $size);
        } else {
            // Image is too small to be resized; use original dimensions
            $newsize = array($width, $height);
        }
    } else {
        // Default constraint is width
        if ($width > $size) {
            // Image is big enough to resize
            $ratio = $height / $width;
            $newheight = round($size * $ratio);
            // Limit calculated dimension to at least 1px
            if ($newheight <= 0) {
                $newheight = 1;
            }
            $newsize = array($size, $newheight);
        } else {
            // Do not scale small images
            $newsize = array($width, $height);
        }
    }

    return $newsize;
}

/**
 * Display the list of images in our database
 *
 * @access public
 * @param   int     The current page number
 * @param   string  The HTML linebreak to use after a row of images
 * @param   boolean Is this the ML-Version for managing everything (true), or is it about selecting one image for the editor? (false)
 * @param   string  The URL to use for pagination
 * @param   boolean Show the "upload media item" feature?
 * @param   boolean Restrict viewing images to a specific directory
 * @param   array   Map of smarty vars transported into all following templates
 * @return  string   Generated HTML
 */
function serendipity_displayImageList($page = 0, $lineBreak = NULL, $manage = false, $url = NULL, $show_upload = false, $limit_path = NULL, $smarty_vars = array()) {
    global $serendipity;
    static $debug = false;

    $extraParems = serendipity_generateImageSelectorParems();

    $serendipity['GET']['only_path']     = serendipity_uploadSecure($limit_path . $serendipity['GET']['only_path'], true);
    $serendipity['GET']['only_filename'] = serendipity_specialchars(str_replace(array('*', '?'), array('%', '_'), $serendipity['GET']['only_filename']));

    $perPage = (!empty($serendipity['GET']['sortorder']['perpage']) ? (int)$serendipity['GET']['sortorder']['perpage'] : 8);
    while ($perPage % $lineBreak !== 0) {
        $perPage++;
    }
    $start = ($page-1) * $perPage;

    if ($manage && $limit_path == NULL) {
        ## SYNCH START ##
        $aExclude = array("CVS" => true, ".svn" => true, "_vti_cnf" => true); // _vti_cnf to exclude possible added servers frontpage extensions
        serendipity_plugin_api::hook_event('backend_media_path_exclude_directories', $aExclude);
        $paths        = array();
        $aFilesOnDisk = array();

        $aResultSet   = serendipity_traversePath(
            $serendipity['serendipityPath'] . $serendipity['uploadPath']. $limit_path,
            '',
            false,
            NULL,
            1,
            NULL,
            FALSE,
            $aExclude
        );
        foreach ($aResultSet AS $sKey => $sFile) {
                if ($sFile['directory']) {
                    if ($debug) echo "<span class='block_level'>{$sFile['relpath']} is a directory.</span>";
                    array_push($paths, $sFile);
                } else {
                    if ($debug) echo "<span class='block_level'>{$sFile['relpath']} is a file.</span>";
                    // Store the file in our array, remove any ending slashes
                    $aFilesOnDisk[$sFile['relpath']] = 1;
                }
                unset($aResultSet[$sKey]);
        }

        usort($paths, 'serendipity_sortPath');

        if ($debug) echo "<p>Got files: <pre>" . print_r($aFilesOnDisk, true) . "</pre></p>";
        $serendipity['current_image_hash'] = md5(serialize($aFilesOnDisk));

        $nTimeStart = microtime_float();
        // MTG 21/01/06: request all images from the database, delete any which don't exist
        // on the filesystem, and mark off files from the file list which are already
        // in the database

        $nCount = 0;

        if ($debug) {
            echo "<p>Image Sync Right: " . serendipity_checkPermission('adminImagesSync') . " Onthefly Sync: " . $serendipity['onTheFlySynch'] . " Hash: " . $serendipity['current_image_hash'] . "!=" . $serendipity['last_image_hash']. "</p>";
        }

        if ($serendipity['onTheFlySynch'] && serendipity_checkPermission('adminImagesSync') && ($debug || ($serendipity['current_image_hash'] != ($serendipity['last_image_hash'] ?? null)))) {
            $aResultSet = serendipity_db_query("SELECT path, name, extension, thumbnail_name, id
                                                FROM {$serendipity['dbPrefix']}images", false, 'assoc');

            if ($debug) {
                echo "<p>Got images: <pre>" . print_r($aResultSet, true) . "</pre></p>";
            }

            if (is_array($aResultSet)) {
                foreach ($aResultSet AS $sKey => $sFile) {
                    serendipity_plugin_api::hook_event('backend_thumbnail_filename_select', $sFile);
                    $sThumbNailFile = '';
                    if (isset($sFile['thumbnail_filename'])) {
                        $sThumbNailFile = $sFile['thumbnail_filename'];
                    } else {
                        $sThumbNailFile = $sFile['path'] . $sFile['name'] . '.' . $sFile['thumbnail_name'] . (empty($sFile['extension']) ? '' : '.' . $sFile['extension']);
                    }

                    $sFileName = $sFile['path'] . $sFile['name'] . (empty($sFile['extension']) ? '' : '.' . $sFile['extension']);

                    if ($debug) { 
                        echo "<p>File name is $sFileName, thumbnail is $sThumbNailFile</p>";
                    }

                    unset($aResultSet[$sKey]);

                    if (isset($aFilesOnDisk[$sFileName])) {
                        unset($aFilesOnDisk[$sFileName]);
                    } else {
                        if ($debug) {
                            echo "<span class='block_level'>Deleting Image {$sFile['id']}</span>";
                        }

                        serendipity_deleteImage($sFile['id']);
                        ++$nCount;
                    }
                    unset($aFilesOnDisk[$sThumbNailFile]);
                }
            }

            if ($nCount > 0){
                if ($debug) {
                    echo "<p>Cleaned up ".$nCount." database entries</p>";
                }
            }

            serendipity_set_config_var('last_image_hash', $serendipity['current_image_hash'], 0);
            $aUnmatchedOnDisk = array_keys($aFilesOnDisk);

            if ($debug) {
                echo "<p>Got unmatched files: <pre>" . print_r($aUnmatchedOnDisk, true) . "</pre></p>";
            }

            $nCount = 0;
            foreach ($aUnmatchedOnDisk AS $sFile) {
                if (preg_match('@\.' . $serendipity['thumbSuffix'] . '\.@', $sFile)) {
                    if ($debug) {
                        echo "<p>Skipping thumbnailed file $sFile</p>";
                    }
                    continue;
                } else {
                    if ($debug) {
                        echo "<p>Checking $sFile</p>";
                    }
                }

                // MTG: 21/01/06: put files which have just 'turned up' into the database
                $aImageData = serendipity_getImageData($sFile);
                if (serendipity_isImage($aImageData, false, '(image)|(video)|(audio)/')) {
                    $nPos = strrpos($sFile, "/");
                    if (is_bool($nPos) && !$nPos) {
                       $sFileName  = $sFile;
                       $sDirectory = "";
                    } else {
                       ++$nPos;
                       $sFileName  = substr($sFile, $nPos);
                       $sDirectory = substr($sFile, 0, $nPos);
                    }
                    if ($debug) {
                        echo "<p>Inserting image $sFileName from $sDirectory <pre>" . print_r($aImageData, true) . "</pre> into database</p>";
                    }
                    # TODO: Check if the thumbnail generation goes fine with Marty's code
                    serendipity_makeThumbnail($sFileName, $sDirectory);
                    serendipity_insertImageInDatabase($sFileName, $sDirectory);
                    ++$nCount;
                }
            }

            if ($nCount > 0) {
                if ($debug) {
                    echo "<p>Inserted ".$nCount." images into the database</p>";
                }
            }
        } else {
            if ($debug) {
                echo "<p>Media Gallery database is up to date</p>";
            }
        }

         /*
         $nTimeEnd = microtime_float ( );
         $nDifference = $nTimeEnd - $nTimeStart;
         echo "<p> total time taken was " . $nDifference . "</p>";
        */
        ## SYNCH FINISHED ##
    }

    ## Aply ACL afterwards:
    serendipity_directoryACL($paths, 'read');

    // set remember filter settings for SetCookie
    if (!isset($serendipity['GET']['filter'])) {
        serendipity_restoreVar($serendipity['COOKIE']['filter'], $serendipity['GET']['filter']);
    }

    $serendipity['imageList'] = serendipity_fetchImagesFromDatabase(
                                  $start,
                                  $perPage,
                                  $totalImages, // Passed by ref
                                  (isset($serendipity['GET']['sortorder']['order']) ? $serendipity['GET']['sortorder']['order'] : false),
                                  (isset($serendipity['GET']['sortorder']['ordermode']) ? $serendipity['GET']['sortorder']['ordermode'] : false),
                                  (isset($serendipity['GET']['only_path']) ? $serendipity['GET']['only_path'] : ''),
                                  (isset($serendipity['GET']['only_filename']) ? $serendipity['GET']['only_filename'] : ''),
                                  (isset($serendipity['GET']['keywords']) ? $serendipity['GET']['keywords'] : ''),
                                  (isset($serendipity['GET']['filter']) ? $serendipity['GET']['filter'] : ''),
                                  isset($serendipity['GET']['hideSubdirFiles'])
    );

    $pages         = ceil($totalImages / $perPage);
    $linkPrevious  = '?' . $extraParems . '&amp;serendipity[page]=' . ($page-1);
    $linkNext      = '?' . $extraParems . '&amp;serendipity[page]=' . ($page+1);
    // Keep the inner to be build first. Now add first and last. Has to do with adding $param to $extraParems.
    $linkFirst     = '?' . $extraParems . '&amp;serendipity[page]=' . 1;
    $linkLast      = '?' . $extraParems . '&amp;serendipity[page]=' . $pages;
    if (is_null($lineBreak)) {
        $lineBreak = floor(750 / ($serendipity['thumbSize'] + 20));
    }

    $dprops = $keywords = array();
    if ($serendipity['parseMediaOverview'] ?? false) {
        $ids = array();
        foreach ($serendipity['imageList'] AS $k => $file) {
            $ids[] = $file['id'];
        }
        $allprops =& serendipity_fetchMediaProperties($ids);
    }

    if (count($serendipity['imageList']) > 0) {
        foreach ($serendipity['imageList'] AS $k => $file) {
            if (!($serendipity['authorid'] == $file['authorid'] || $file['authorid'] == '0' || serendipity_checkPermission('adminImagesViewOthers'))) {
                // This is a fail-safe continue. Basically a non-matching file should already be filtered in SQL.
                continue;
            }

            serendipity_prepareMedia($serendipity['imageList'][$k], $url);

            if ($serendipity['parseMediaOverview'] ?? false) {
                $serendipity['imageList'][$k]['props'] =& $allprops[$file['id']];
                if (!is_array($serendipity['imageList'][$k]['props']['base_metadata'])) {
                    $serendipity['imageList'][$k]['metadata'] =& serendipity_getMetaData($serendipity['imageList'][$k]['realfile'], $serendipity['imageList'][$k]['header']);
                } else {
                    $serendipity['imageList'][$k]['metadata'] = $serendipity['imageList'][$k]['props']['base_metadata'];
                    serendipity_plugin_api::hook_event('media_getproperties_cached', $serendipity['imageList'][$k]['metadata'], $serendipity['imageList'][$k]['realfile']);
                }
                serendipity_parseMediaProperties($dprops, $keywords, $serendipity['imageList'][$k], $serendipity['imageList'][$k]['props'], 3, false);
            }
        }
    }

    $smarty_vars = array_merge($smarty_vars, array(
        'limit_path'    => $limit_path,
        'perPage'       => $perPage,
        'show_upload'   => $show_upload,
        'page'          => $page,
        'pages'         => $pages,
        'linkFirst'     => $linkFirst,
        'linkNext'      => $linkNext,
        'linkPrevious'  => $linkPrevious,
        'linkLast'      => $linkLast,
        'extraParems'   => $extraParems,
        'totalImages'   => $totalImages
    ));

    return serendipity_showMedia(
        $serendipity['imageList'],
        $paths,
        $url,
        $manage,
        $lineBreak,
        true,
        $smarty_vars
    );
} // End serendipity_displayImageList()

/**
 * Generate the url-parameters needed when generating the ML to select an image to add to the editor,
 * to store the relevant options (like which textarea to add it to)
 *
 * @param   string  Url or Form format
 */
function serendipity_generateImageSelectorParems($format = 'url') {
    global $serendipity;

    $sortParams   = array('perpage', 'order', 'ordermode');
    $importParams = array('adminModule', 'htmltarget', 'filename_only', 'textarea', 'subpage',  'keywords', 'noBanner', 'noSidebar', 'noFooter', 'showUpload','showMediaToolbar');
    $extraParems  = '';
    $filterParams = $serendipity['GET']['filter'] ?? array(); 

    $standaloneFilterParams = array('only_path', 'only_filename');
    $parems = array();

    foreach($importParams AS $importParam) {
        if (isset($serendipity['GET'][$importParam])) {
            $parems['serendipity[' . $importParam . ']'] = $serendipity['GET'][$importParam];
        }
    }

    foreach($sortParams AS $sortParam) {
        serendipity_restoreVar($serendipity['COOKIE']['sortorder_' . $sortParam], $serendipity['GET']['sortorder'][$sortParam]);
        $parems['serendipity[sortorder]['. $sortParam .']'] = $serendipity['GET']['sortorder'][$sortParam];
    }

    foreach($standaloneFilterParams AS $filterParam) {
        serendipity_restoreVar($serendipity['COOKIE'][$filterParam], $serendipity['GET'][$filterParam]);
        if (!empty($serendipity['GET'][$filterParam]) && $serendipity['GET'][$filterParam] != "undefined") {
            $parems['serendipity[' . $filterParam . ']'] = $serendipity['GET'][$filterParam];
        }
    }

    foreach($filterParams AS $filterParam => $filterValue) {
        serendipity_restoreVar($serendipity['COOKIE']['filter'][$filterParam], $serendipity['GET']['filter'][$filterParam]);
        if (!empty($serendipity['GET']['filter'][$filterParam]) && $serendipity['GET']['filter'][$filterParam] != "undefined") {
            if (is_array($filterValue)) {
                foreach($filterValue as $key => $value) {
                    $parems['serendipity[filter][' . $filterParam . '][' . $key . ']'] = $value;
                }
            } else {
                $parems['serendipity[filter][' . $filterParam . ']'] = $filterValue;
            }
        }
    }

    foreach ($parems as $param => $value) {
        if ($format == "form") {
            $extraParems .= '<input type="hidden" name="'. $param .'" value="'. serendipity_specialchars($value) .'">'."\n";
        } else {
            $extraParems .= $param.'='. serendipity_specialchars($value) .'&amp;';
        }
    }

    return rtrim($extraParems, '&amp;');
}

/**
 * Check if a media item is an image
 *
 * @access public
 * @param   array       File information
 * @param   boolean     Use a strict check that does not list PDFs as an image?
 * @return  boolean     True if the file is an image
 */
function serendipity_isImage(&$file, $strict = false, $allowed = 'image/') {
    global $serendipity;

    $file['displaymime'] = $file['mime'];

    if (! isset($file['imgsrc'])) {
        return false;
    }

    // Strip HTTP path out of imgsrc
    $file['location'] = $serendipity['serendipityPath'] . preg_replace('@^(' . preg_quote($serendipity['serendipityHTTPPath']) . ')@i', '', $file['imgsrc']);

    // File is PDF -> Thumb is PNG
    // Detect PDF thumbs
    if ($file['mime'] == 'application/pdf' && file_exists($file['location'] . '.png') && $strict == false) {
        $file['imgsrc']     .= '.png';
        $file['displaymime'] = 'image/png';
    }

    return preg_match('@' . $allowed . '@i', $file['displaymime']);
}

/**
 * Recursively delete a directory tree
 *
 * @access public
 * @param   string      The originating directory
 * @param   string      The subdirectory
 * @param   boolean     Force deleting an directory even if there are files left in it?
 * @return true
 */
function serendipity_killPath($basedir, $directory = '', $forceDelete = false) {
    static $serious = true;

    if ($handle = @opendir($basedir . $directory)) {
        $filestack = [];
        while (false !== ($file = @readdir($handle))) {
            if ($file != '.' && $file != '..') {
                if (is_dir($basedir . $directory . $file)) {
                    serendipity_killPath($basedir, $directory . $file . '/', $forceDelete);
                } else {
                    $filestack[$file] = $directory . $file;
                }
            }
        }
        @closedir($handle);

        echo '<span class="msg_notice"><span class="icon-info-circled" aria-hidden="true"></span> ';
        printf(CHECKING_DIRECTORY, $directory);
        echo "</span>";

        // No, we just don't kill files the easy way. We sort them out properly from the database
        // and preserve files not entered therein.
        $files = serendipity_fetchImagesFromDatabase(0, 0, $total, false, false, $directory);
        if (is_array($files)) {
            echo "<ul class='plainList'>\n";
            foreach($files AS $f => $file) {
                echo "<li>\n";
                if ($serious) {
                    echo serendipity_deleteImage($file['id']);
                } else {
                    echo $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
                }
                echo "</li>\n";

                unset($filestack[$file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension'])]);
                unset($filestack[$file['name'] . (!empty($file['thumbnail_name']) ? '.' . $file['thumbnail_name'] : '') . (empty($file['extension']) ? '' : '.' . $file['extension'])]);
            }
            echo "</ul>\n";
        }

        if (count($filestack) > 0) {
            if ($forceDelete) {
                echo "<ul class='plainList'>\n";
                foreach($filestack AS $f => $file) {
                    if ($serious && @unlink($basedir . $file)) {
                        printf('<li><span class="msg_success"><span class="icon-ok-circled" aria-hidden="true"></span> ' . DELETING_FILE . ' ' . DONE . "</span></li>\n", $file);
                    } else {
                        printf('<li><span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . DELETING_FILE . ' ' . ERROR . "</span></li>\n", $file);
                    }
                }
                echo "</ul>\n";
            } else {
                echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . ERROR_DIRECTORY_NOT_EMPTY . "</span>\n";
                echo "<ul>\n";
                foreach($filestack AS $f => $file) {
                    echo '<li>' . $file . "</li>\n";
                }
                echo "</ul>\n";
            }
        }

        if ($serious && !empty($directory) && !preg_match('@^.?/?$@', $directory) && @rmdir($basedir . $directory)) {
            echo '<span class="msg_success"><span class="icon-ok-circled" aria-hidden="true"></span> ';
            printf(DIRECTORY_DELETE_SUCCESS, $directory);
            echo "</span>\n";
        } else {
            echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ';
            printf(DIRECTORY_DELETE_FAILED, $directory);
            echo "</span>\n";
        }
    }

    return true;
}

/**
 * Recursively walk a directory tree
 *
 *
 * @access public
 * @param   string      The core directory
 * @param   string      The subdirectory
 * @param   boolean     Only return directories instead of files as well?
 * @param   string      A regexp patter to include files
 * @param   int         Level of nesting (recursive use)
 * @param   int         The maximum level of nesting (recursive use)
 * @param   mixed       Toggle whether to apply serendipity_directoryACL (false / 'read' / 'write')
 * @param   array       An array of directories to skip [passed by plugins, for example]
 * @return  array       Array of files/directories
 */
function serendipity_traversePath($basedir, $dir='', $onlyDirs = true, $pattern = NULL, $depth = 1, $max_depth = NULL, $apply_ACL = false, $aExcludeDirs = NULL) {
    global $serendipity;
    
    if ($serendipity['useInternalCache']) {
        $key = md5($basedir . $dir . $onlyDirs . $pattern . $depth . $max_depth . $apply_ACL . serialize($aExcludeDirs) . $serendipity['serendipityUser']);
        $files = serendipity_getCacheItem($key);
        if ($files && $files !== false) {
            return unserialize($files);
        }
    }
    
    if ($aExcludeDirs === null) {
        // add _vti_cnf to exclude possible added servers frontpage extensions
        // add ckeditor/kcfinders .thumb dir to exclude, since no hook
        $aExcludeDirs = array("CVS" => true, ".svn" => true, ".thumbs" => true, "_vti_cnf" => true, ".git" => true);
    }

    $odir = serendipity_dirSlash('end', $basedir) . serendipity_dirSlash('end', $dir);
    $dh = @opendir($odir);
    if (!$dh) {
        return array();
    }

    $files = array();
    while (($file = @readdir($dh)) !== false) {
        if ($file != '.' && $file != '..') {
            $bPatternMatch = (is_null($pattern) || preg_match($pattern, $file));
            $sFullPath     = $odir . $file;
            $bIsDir        = is_dir($sFullPath);
            if ($onlyDirs === false || $bIsDir) {
                if ($bPatternMatch &&
                    (!$bIsDir || $aExcludeDirs == null || !isset($aExcludeDirs[$file]))) {
                    $files[] = array(
                        'name'      => $file,
                        'depth'     => $depth,
                        'relpath'   => ltrim(str_replace('\\', '/', serendipity_dirSlash('end', $dir)) . basename($file) . ($bIsDir ? '/' : ''), '/'),
                        'directory' => $bIsDir
                    );
                }
            }

            if ($bIsDir &&
                    ($max_depth === null || $depth < $max_depth) &&
                    ($aExcludeDirs == null || !isset($aExcludeDirs[$file]))) {
                $next_dir = serendipity_dirSlash('end', $dir) . basename($file);
                $files    = array_merge($files, serendipity_traversePath($basedir, $next_dir, $onlyDirs, $pattern, ($depth+1), $max_depth, $apply_ACL, $aExcludeDirs));
            }
        }
    }

    @closedir($dh);

    if ($depth == 1 && $apply_ACL !== FALSE) {
        serendipity_directoryACL($files, $apply_ACL);
    }

    if ($serendipity['useInternalCache']) {
        $key = md5($basedir . $dir . $onlyDirs . $pattern . $depth . $max_depth . $apply_ACL . serialize($aExcludeDirs) . $serendipity['serendipityUser']);

        serendipity_cacheItem($key, serialize($files));
    }

    return $files;
}

/**
 * Custom usort() function that properly sorts a path
 *
 * @access public
 * @param   array      First array
 * @param   array      Second array
 * @return
 */
function serendipity_sortPath($a, $b) {
    return strcasecmp($a['relpath'], $b['relpath']);
}

/**
 * Delete a directory with all its files
 *
 * @access public
 * @param   string      The directory to delete
 * @return
 */
function serendipity_deletePath($dir) {
    $d = dir($dir);
    if ($d) {
        while ($f = $d->read() ){
            if ($f != '.' && $f != '..') {
                if (is_dir($dir . $f)){
                    serendipity_deletePath($dir . $f . '/');
                    rmdir($dir . $f);
                }

                if (is_file($dir . $f)) {
                    unlink($dir . $f);
                }
            }
        }

        $d->close();
    }
}

/**
 * Transform a filename into a valid  upload path
 *
 * @access public
 * @param   string      The input filename
 * @param   boolean     Shall all paths be stripped?
 * @param   boolean     Shall a trailing slash be appended?
 * @return  string      The valid filename
 */
function serendipity_uploadSecure($var, $strip_paths = true, $append_slash = false) {

    $var = str_replace(' ', '_', $var);
    $var = preg_replace('@[^0-9a-z\._/-]@i', '', $var);
    $var = preg_replace('@\.+$@i', '', $var); # remove trailing dots
    if ($strip_paths) {
        $var = preg_replace('@(\.+[/\\\\]+)@', '/', $var);
    }

    $var = preg_replace('@^(/+)@', '', $var);

    if ($append_slash) {
        if (!empty($var) && substr($var, -1, 1) != '/') {
            $var .= '/';
        }
    }

    # truncate extensions to 5 chars
    # 1) (\.[^./\\\]{5}) up to five chars after a dot are captured
    # 2) expression matches only if followed by anything that is no dot
    #    and has no / or \ at the end
    # 3) if epxression matches, everything after the capture group is deleted
    $var = preg_replace('@(\.[^./\\\]{5})[^./\\\]+$@', '$1', $var);

    return $var;
}

/**
 * Get the imagesize for a file
 *
 * @access public
 * @param   string      The filename of the image
 * @param   string      The mimetype of an image (can be autodetected)
 * @param   string      The file extension of an image
 * @return  array       The width/height of the file
 */
function serendipity_getimagesize($file, $ft_mime = '', $suf = '') {
    if (empty($ft_mime) && !empty($suf)) {
        $ft_mime = serendipity_guessMime($suf);
    }

    if ($ft_mime == 'application/pdf') {
        $fdim = array(1000,1000,24, '', 'bits'=> 24, 'channels' => '3', 'mime' => 'application/pdf');
    } else {
        $fdim = @getimagesize($file);
    }

    if (is_array($fdim)) {
        if (empty($fdim['mime'])) {
            $fdim['mime'] = $ft_mime;
        }

        if ($fdim['mime'] == 'image/vnd.wap.wbmp' && $ft_mime == 'video/x-quicktime') {
            // PHP Versions prior to 4.3.9 reported .mov files wrongly as WAP. Fix this and mark the file as 'non-image' with 0x0 dimensions
            $fdim['mime'] = $ft_mime;
        }
    } else {
        // The file is no image. Return a fake array so that files are inserted (but without a thumb)
        $fdim = array(
            0         => 0,
            1         => 0,
            'mime'    => $ft_mime,
            'noimage' => true
        );
    }

    return $fdim;
}

/**
 * Get the available fields of the media database
 *
 * @access public
 * @return array    Array with available, sortable fields
 */
function serendipity_getImageFields() {
    global $serendipity;

    if (($serendipity['simpleFilters'] ?? true) !== false) {
        $x = array(
            'i.date'              => array('desc' => SORT_ORDER_DATE,
                                         'type' => 'date'
                                   ),

            'i.name'              => array('desc' => SORT_ORDER_NAME,
                                        'type' => 'text'
                                   ),

        );

    } else {
        $x = array(
            'i.date'              => array('desc' => SORT_ORDER_DATE,
                                         'type' => 'date'
                                   ),

            'i.name'              => array('desc' => SORT_ORDER_NAME,
                                         'type' => 'text'
                                   ),

            'i.authorid'          => array('desc' => AUTHOR,
                                         'type' => 'authors'
                                   ),

            'i.extension'         => array('desc' => SORT_ORDER_EXTENSION,
                                        'type' => 'text'
                                   ),

            'i.size'              => array('desc' => SORT_ORDER_SIZE,
                                         'type' => 'intrange'
                                   ),

            'i.dimensions_width'  => array('desc' => SORT_ORDER_WIDTH,
                                         'type' => 'intrange'
                                   ),

            'i.dimensions_height' => array('desc' => SORT_ORDER_HEIGHT,
                                         'type' => 'intrange'
                                   )
        );

        $addProp = explode(';', $serendipity['mediaProperties']);
        foreach($addProp AS $prop) {
            $parts = explode(':', $prop);
            $name  = $parts[0];
            $x['bp.' . $name] = array('desc' => (defined('MEDIA_PROPERTY_' . $name) ? constant('MEDIA_PROPERTY_' . $name) : serendipity_specialchars($name)));
            if (preg_match('@date@i', $name)) {
                $x['bp.' . $name]['type'] = 'date';
            }
            if (preg_match('@length@i', $name)) {
                $x['bp.' . $name]['type'] = 'intrange';
            }
            if (preg_match('@dpi@i', $name)) {
                $x['bp.' . $name]['type'] = 'int';
            }
        }
    }

    return $x;
}

/**
 * Escape a shell argument for imagemagick use
 *
 * @access public
 * @param   string  Input argument
 * @return  string  Output argument
 */
function serendipity_escapeshellarg($string) {
    return escapeshellarg(str_replace('%', '', $string));
}



/**
 * Move file with all thumbnails to given directory and update database and entries
 * */
function serendipity_moveFileTo($id, $dir) {
    global $serendipity;
    $file = serendipity_fetchImageFromDatabase($id);
    serendipity_renameFile($id, $file['name'], $dir);
    serendipity_updateImageInDatabase(array('path' => $dir), $id);
    serendipity_updateImageInEntries($id, $file);
    return true;
}

/**
 * Rename a media directory
 *
 * @access public
 * @param   string  Old directory name
 * @param   string  New directory name
 */
function serendipity_renameDir($oldDir, $newDir) {
    global $serendipity;
    # Plan: 1. Get id of all images under $oldDir (including those in subdirs)
    #       2. Move $oldDir to newDir
    #       3. Update image (path) in database
    #       3. Update image in entries via serendipity_updateImageInEntries
    $imgBase = serendipity_dirSlash('end', $serendipity['serendipityPath'] . $serendipity['uploadPath']);

    $total = null;
    $images = serendipity_fetchImagesFromDatabase(0, 0, $total, false, false, $oldDir);

    // Perform ACL renames
    $dirs = serendipity_db_query("SELECT groupid, artifact_id, artifact_type, artifact_mode, artifact_index
                                        FROM {$serendipity['dbPrefix']}access
                                       WHERE artifact_type = 'directory'
                                         AND artifact_index LIKE '" . serendipity_db_escape_string($oldDir) . "%'", false, 'assoc');
    if (is_array($dirs)) {
        foreach($dirs AS $dir) {
            $old = $dir['artifact_index'];
            $new = preg_replace('@^(' . preg_quote($oldDir) . ')@i', $newDir, $old);
            serendipity_db_query("UPDATE {$serendipity['dbPrefix']}access
                                         SET artifact_index = '" . serendipity_db_escape_string($new) . "'
                                       WHERE groupid        = '" . serendipity_db_escape_string($dir['groupid']) . "'
                                         AND artifact_id    = '" . serendipity_db_escape_string($dir['artifact_id']) . "'
                                         AND artifact_type  = '" . serendipity_db_escape_string($dir['artifact_type']) . "'
                                         AND artifact_mode  = '" . serendipity_db_escape_string($dir['artifact_mode']) . "'
                                         AND artifact_index = '" . serendipity_db_escape_string($dir['artifact_index']) . "'");
        }
    }

    if (! file_exists("${imgBase}${newDir}")) {
        rename("${imgBase}${oldDir}", "${imgBase}${newDir}");

        foreach($images as $image) {
            serendipity_updateImageInDatabase(
                array('path' => preg_replace('@' . preg_quote(serendipity_dirSlash('end', $oldDir)) . '@', serendipity_dirSlash('end', $newDir), $image['path'], 1)), // we use preg_replace and not str_replace to be able to limit to exacty one replacement, preventing issues when a path has loops in it
                $image['id']
            );
            serendipity_updateImageInEntries($image['id'], $image);
        }
        serendipity_cleanCache();
        return true;
    }
    return false;
    
}   

/**
 * Rename a file in the ML (upload folder and database). Also edit entries to use the new name, and move the thumbnails
 *
 * @param   Id of the image to be renamed
 * @param   The new name (without extension)
 * @path    The new path to be prepended to the new name, if the file is also to be moved 
 * */
function serendipity_renameFile($id, $newName, $path = null) {
    global $serendipity;
    $file = serendipity_fetchImageFromDatabase($id);

    if (!is_array($file) || (!serendipity_checkPermission('adminImagesMaintainOthers') && $file['authorid'] != '0' && $file['authorid'] != $serendipity['authorid'])) {
        return;
    }
    $oldName = $file['name'];
    
    if (LANG_CHARSET == 'UTF-8') {
        $newName = utf8_encode($newName);
    }

    if ($path === null) {
        $path = $file['path'];
    }
    
    $newName = serendipity_uploadSecure(serendipity_makeFilename($newName), true);
    $imgBase = $serendipity['serendipityPath'] . $serendipity['uploadPath'];
    
    $newPath = $imgBase . $path . $newName . (empty($file['extension']) ? '' : '.' . $file['extension']);

    if (serendipity_isActiveFile($newPath)) {
        return sprintf('<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . ERROR_FILE_FORBIDDEN . "</span>\n", $newName);
    }

    if (file_exists($newPath)) {
        return sprintf('<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . ERROR_FILE_EXISTS . "</span>\n", $newName);
    }
    
    if (rename("{$imgBase}{$file['path']}{$file['realname']}", $newPath)) {
        # if renaming was successfull, rename thumbnails and update
        # databases and entries

        serendipity_renameThumbnails($id, "{$path}$newName");

        serendipity_updateImageInDatabase(array('name' => $newName, 'realname' => basename($newPath)), $id);
        serendipity_updateImageInEntries($id, $file);
    } else {
        return '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ' . MEDIA_RENAME_FAILED . "</span>\n";
    }
    serendipity_cleanCache();
    return TRUE;
}

/**
 * Rename thumbnails linked to $id
 * 
 * */
function serendipity_renameThumbnails($id, $newName) {
    global $serendipity;
    
    $file = serendipity_fetchImageFromDatabase($id);

    $thumbnails = serendipity_getThumbnails($id);
    foreach($thumbnails as $thumbnail) {
        $newThumbnail = str_replace("{$file['path']}{$file['name']}", $newName, $thumbnail);
        rename($thumbnail, $newThumbnail);

        $eventData = array( // array in array because the event api expects that
            array(
                'type'      => 'file', // TODO: Use proper preg_quote
                'oldDir'    => preg_replace('@' . $serendipity['serendipityPath'] . $serendipity['uploadPath'] . '@', '', $thumbnail),
                'newDir'    => preg_replace('@' . $serendipity['serendipityPath'] . $serendipity['uploadPath'] . '@', '', $newThumbnail)
            )
        );
        serendipity_plugin_api::hook_event('backend_media_rename', $eventData);

    }
    return true;
}

/**
 * Get an array of existing thumbnails linked to the image with the given id
 *
 * */
function serendipity_getThumbnails($id) {
    global $serendipity;
    
    $file = serendipity_fetchImageFromDatabase($id);
    $name = $file['name'];

    $imagePath = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $file['path'] . $file['realname'];
    $thumbnailBase = str_replace($file['extension'], '', $imagePath);
    $uploadDir = dirname($imagePath);

    return glob("$thumbnailBase*{$file['thumbnail_name']}.{$file['extension']}");
}

/**
 * Set image references to current path in all articles linking to them via the ML
 *
 * @id is the id of the image, used for identification
 * @old is the old file property array of the image, used to keep a manual set link
 * */
function serendipity_updateImageInEntries($id, $old) {
    global $serendipity;
    
    $file = serendipity_fetchImageFromDatabase($id);
    $imageHTTPPath = $serendipity['serendipityHTTPPath'] . $serendipity['uploadHTTPPath'] . $file['path'] . $file['realname'];
    $oldImageHTTPPath = $serendipity['serendipityHTTPPath'] . $serendipity['uploadHTTPPath'] . $old['path'] . $old['realname'];
    $thumbnailHTTPPath = str_replace(".{$file['extension']}", ".{$file['thumbnail_name']}.{$file['extension']}", $imageHTTPPath);
    $thumbSuffix = $serendipity['thumbSuffix'];


    $q = "SELECT id, body, extended FROM {$serendipity['dbPrefix']}entries
          WHERE
          body LIKE '%<!-- s9ymdb:$id -->%' OR extended LIKE '%<!-- s9ymdb:$id -->%'";

    $entries = serendipity_db_query($q, false, 'assoc');

    if (is_array($entries) && !empty($entries)) {
        foreach($entries as $entry) {
            # First change the img element, be it a thumbnail or an image
            $pattern = "@(<!-- s9ymdb:$id -->[^>]*) src=[\"']([^'\"]+)[\"']@";

            $callback = function($matches) use ($imageHTTPPath, $thumbnailHTTPPath, $file, $thumbSuffix) {
                if (strpos($matches[2],  "$thumbSuffix.{$file['extension']}") === false) {
                    // the image showed the full size image
                    return $matches[1] . ' src="' . $imageHTTPPath . '"';
                } else {
                    return $matches[1] . ' src="' . $thumbnailHTTPPath . '"';
                }
            };
            
            $entry['body'] = preg_replace_callback($pattern, $callback, $entry['body']);
            $entry['extended'] = preg_replace_callback($pattern, $callback, $entry['extended']);

            # But we should not forget to update the a element
            $pattern = "@href=[\"']([^'\"]+)[\"']([^>]*)>(<!-- s9ymdb:$id -->)@";

            $callback = function($matches) use ($imageHTTPPath, $thumbnailHTTPPath, $oldImageHTTPPath) {
                # We only update the link if it is not a manual link, if it pointed to the old image location
                if ($matches[1] === $oldImageHTTPPath) {
                    return 'href="' . $imageHTTPPath . '"' . $matches[2] . '>' . $matches[3];
                } else {
                    return 'href="' . $matches[1] . '"' . $matches[2] . '>' . $matches[3];
                }
            };
            
            $entry['body'] = preg_replace_callback($pattern, $callback, $entry['body']);
            $entry['extended'] = preg_replace_callback($pattern, $callback, $entry['extended']);
            

            $uq = "UPDATE {$serendipity['dbPrefix']}entries
                                        SET
                                        body = '" . serendipity_db_escape_string($entry['body']) . "' ,
                                        extended = '" . serendipity_db_escape_string($entry['extended']) . "'
                                        WHERE
                                        id =  " . serendipity_db_escape_string($entry['id']);
            serendipity_db_query($uq);
        }
    }
}

/**
 * Makes sure a directory begins with or ends with a "/"
 *
 * @access public
 * @param   string  Type of where to append/prepend slash ('end', 'start', 'both')
 * @param   string  Directory name
 * @return  string  Output argument
 */
function serendipity_dirSlash($type, $dir) {

    if ($dir == '') {
        return $dir;
    }

    if ($type == 'start' || $type == 'both') {
        if (substr($dir, 0, 1) != '/') {
            $dir = '/' . $dir;
        }
    }

    if ($type == 'end' || $type == 'both') {
        if (substr($dir, -1) != '/') {
            $dir .= '/';
        }
    }

    return $dir;
}

/**
 * Cycle a serendipity_traversePath resultset and apply read/write ACLs.
 *
 * @access public
 * @param   array   serendipity_traversePath result array
 * @param   string  ACL type ('read', 'write')
 */
function serendipity_directoryACL(&$paths, $type = 'read') {
    global $serendipity;
    static $debug = false;

    if ($debug) {
        echo "<span class='block_level'>Applying ACL for mode '$type'.</span>";
    }

    if (!is_array($paths)) {
        return true;
    }

    $startCount = count($paths);
    if (!isset($serendipity['enableACL']) || $serendipity['enableACL'] == true) {
        // Check if we are a cool superuser. Bail out if we are.
        $logged_in = serendipity_userLoggedIn();
        if ($logged_in && serendipity_checkPermission('adminImagesMaintainOthers') && serendipity_checkPermission('adminImagesDirectories')) {
            if (!$debug) {
                return true;
            }
        }

        // Get list of all ACLs for directories.
        $q = "SELECT a.artifact_index AS directory,
                     a.groupid
                FROM {$serendipity['dbPrefix']}access AS a
               WHERE a.artifact_type = 'directory'
                 AND a.artifact_mode = '" . serendipity_db_escape_string($type) . "'";
        $allowed = serendipity_db_query($q);
        if (!is_array($allowed)) {
            return true;
        }

        // Get a list of all the groups for this user. Pipe it into a usable array.
        if ($logged_in) {
            $my_groups =& serendipity_getGroups($serendipity['authorid']);
            $acl_allowed_groups = array();
            foreach($my_groups AS $my_group) {
                $acl_allowed_groups[$my_group['id']] = true;
            }
        } else {
            // Only the 'ALL AUTHORS' group is valid for non-logged in authors.
            $acl_allowed_groups = array(0 => true);
        }

        // Iterate every ACL and check if we are allowed to use it. Parse that data into a workable array.
        $acl_allowed = array();
        foreach($allowed AS $row) {
            $acl_allowed[$row['directory']][$row['groupid']] = true;
        }

        // Iterate the input path array and check it against ACL.
        foreach($paths AS $idx => $info) {
            if (!isset($acl_allowed[$info['relpath']])) {
                // ACL for directory not set. Assume we are allowed to access.
                continue;
            }

            $granted = false;
            foreach($acl_allowed[$info['relpath']] AS $groupid => $set) {
                if ($groupid === 0 || isset($acl_allowed_groups[$groupid])) {
                    // We are allowed to access this element
                    $granted = true;
                    break;
                }
            }

            if ($granted === false) {
                // We are not allowed to access this element
                if ($debug) {
                    echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ACL for ' . $info['relpath'] . " DENIED.</span>";
                }
                unset($paths[$idx]);
            } else {
                if ($debug) {
                    echo '<span class="msg_success"><span class="icon-ok-circled" aria-hidden="true"></span> ACL for ' . $info['relpath'] . " granted.</span>";
                }
            }
        }

        if (count($paths) < $startCount) {
            if ($debug) {
                echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ACL denied all.</span>';
            }
            return false;
        }
    }

    return true;
}

/**
 * Build the name of a thumbnail image file.
 *
 * @author MTG
 * @param  string   Relative Path
 * @param  string   File name
 * @param  string   File extension
 * @param  string   Thumbnail suffix
 * @return array    Thumbnail path
 *
 */
function serendipity_getThumbNailPath($sRelativePath, $sName, $sExtension, $sThumbName) {
    $aTempArray = array('path'      => $sRelativePath,
                        'name'      => $sName,
                        'extension' => $sExtension);
    serendipity_plugin_api::hook_event('backend_thumbnail_filename_select', $aTempArray);

    if (isset($aTempArray['thumbnail_filename'])) {
        $sThumbNailPath = $aTempArray['thumbnail_filename'];
    } else {
        if ($sExtension) {
            $sThumbNailPath = $sRelativePath . $sName . (!empty($sThumbName) ? '.' . $sThumbName : '') . '.' . $sExtension;
        } else {
            $sThumbNailPath = $sRelativePath . $sName . (!empty($sThumbName) ? '.' . $sThumbName : '');
        }
    }

    return $sThumbNailPath;
}

 /**
 * Given a relative path to an image, construct an array containing all
 * relevant information about that image in the file structure.
 *
 * @author MTG
 * @param  string   Relative Path
 * @return array    Data about image
 *
 */
function &serendipity_getImageData($sRelativePath) {
    global $serendipity;

    // First, peel off the file name from the path
    $nPos = strrpos($sRelativePath, '/');
    if (is_bool($nPos) && !$nPos) {
        $sFileName  = $sRelativePath;
        $sDirectory = '';
    } else {
        $nLastSlashPos = 1 + $nPos;
        $sFileName     = substr($sRelativePath, $nLastSlashPos);
        $sDirectory    = substr($sRelativePath, 0, $nLastSlashPos);
    }

    list($sName, $sExtension) = serendipity_parseFileName($sFileName);

    $sImagePath = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $sRelativePath;

    $aSizeData = @serendipity_getimagesize($sImagePath , '', $sExtension);
    $nWidth    = $aSizeData[0];
    $nHeight   = $aSizeData[1];
    $sMime     = $aSizeData['mime'];
    $nFileSize = @filesize($sImagePath);

    $array = array(
        'name'              => $sName,
        'extension'         => $sExtension,
        'mime'              => $sMime,
        'size'              => $nFileSize,
        'dimensions_width'  => $nWidth,
        'dimensions_height' => $nHeight,
        'path'              => $sDirectory,
        'authorid'          => 0,
        'hotlink'           => 0,
        'id'                => $sRelativePath,
        'realname'          => $sFileName
    );

    return $array;
}

/**
 * Shows the HTML form to add/edit properties of uploaded media items
 *
 * @param  array    Associative array holding an array('image_id', 'target', 'created_thumbnail') that points to the uploaded media
 * @param  int      How many keyword checkboxes to display next to each other?
 * @param  boolean  Can existing data be modified?
 * @return string   Generated HTML
 *
 */
function serendipity_showPropertyForm(&$new_media, $keywordsPerBlock = 3, $is_edit = true) {
    global $serendipity;

    if (!is_array($new_media) || count($new_media) < 1) {
        return true;
    }

    $mirror = array();
    serendipity_checkPropertyAccess($new_media, $mirror, 'read');

    $editform_hidden = '';
    if (isset($GLOBALS['image_selector_addvars']) && is_array($GLOBALS['image_selector_addvars'])) {
        // These variables may come from serendipity_admin_image_selector.php to show embedded upload form
        foreach($GLOBALS['image_selector_addvars'] AS $imgsel_key => $imgsel_val) {
            $editform_hidden .= '          <input type="hidden" name="serendipity[' . serendipity_specialchars($imgsel_key) . ']" value="' . serendipity_specialchars($imgsel_val) . '">' . "\n";
        }
    }

    $dprops   = explode(';', $serendipity['mediaProperties']);
    $keywords = explode(';', $serendipity['mediaKeywords']);

    $show = array();
    foreach($new_media AS $idx => $media) {
        $props =& serendipity_fetchMediaProperties($media['image_id']);

        $show[$idx] =& $media['internal'];
        $show[$idx]['image_id'] = $media['image_id'];

        serendipity_prepareMedia($show[$idx]);
        if (!is_array($props['base_metadata'] ?? null)) {
            $show[$idx]['metadata'] =& serendipity_getMetaData($show[$idx]['realfile'], $show[$idx]['header']);
        } else {
            $show[$idx]['metadata'] = $props['base_metadata'];
            serendipity_plugin_api::hook_event('media_getproperties_cached', $show[$idx]['metadata'], $show[$idx]['realfile']);
        }

        serendipity_parseMediaProperties($dprops, $keywords, $show[$idx], $props, $keywordsPerBlock, $is_edit);
    }
    $smarty_vars = array(
        'is_edit'           => $is_edit,
        'editform_hidden'   => $editform_hidden,
        'keywordsPerBlock'  => $keywordsPerBlock,
        'keywords'          => $keywords,
        'dprops'            => $dprops,
        'case_add'          => is_array($new_media[0]['created_thumbnail'] ?? null)     // created_thumbnail is only set when viewing properties after adding an image
    );

    return serendipity_showMedia(
        $show,
        $mirror,
        '',
        false,
        1,
        false,
        $smarty_vars);
}

/**
 * Parse/Convert properties
 *
 * @param  array    Holds the property key array
 * @param  array    Holds the keyword key array
 * @param  int      Holds the media metadata
 * @param  int      Holds the media properties
 * @param  int      How many keyword checkboxes to display next to each other?
 * @param  boolean  Can existing data be modified?
 * @return boolean
 *
 */
function serendipity_parseMediaProperties(&$dprops, &$keywords, &$media, &$props, $keywordsPerBlock, $is_edit) {
    global $serendipity;

    if (!is_array($dprops)) {
        $dprops   = explode(';', $serendipity['mediaProperties']);
    }
    if (!is_array($keywords)) {
        $keywords = explode(';', $serendipity['mediaKeywords']);
    }

    $media['references'] = serendipity_db_query("SELECT link, name
                            FROM {$serendipity['dbPrefix']}references
                           WHERE entry_id = " . $media['id'] . "
                             AND type = 'media'
                        ORDER BY name DESC
                           LIMIT 15", false, 'assoc');
    if (!is_array($media['references'])) {
        $media['references'] = false;
    }

    foreach($dprops AS $prop) {
        $type = 'input';
        $parts = explode(':', trim($prop));

        if (in_array('MULTI', $parts)) {
            $type = 'textarea';
        }

        if (preg_match('@(AUDIO|VIDEO|DOCUMENT|IMAGE|ARCHIVE|BINARY)@i', $prop)) {
            $show_item = false;
            if ($media['mediatype'] == 'video' && in_array('VIDEO', $parts)) {
                $show_item = true;
            }

            if ($media['mediatype'] == 'audio'  && in_array('AUDIO', $parts)) {
                $show_item = true;
            }

            if ($media['mediatype'] == 'image'  && in_array('IMAGE', $parts)) {
                $show_item = true;
            }

            if ($media['mediatype'] == 'document' && in_array('DOCUMENT', $parts)) {
                $show_item = true;
            }

            if ($media['mediatype'] == 'archive' && in_array('ARCHIVE', $parts)) {
                $show_item = true;
            }

            if ($media['mediatype'] == 'binary' && in_array('BINARY', $parts)) {
                $show_item = true;
            }

            if (!$show_item) {
                continue;
            }
        }

        if (!$is_edit) {
            $type = 'readonly';
        }
        $val = serendipity_mediaTypeCast($parts[0], $props['base_property'][$parts[0]] ?? null, true);

        $propkey = serendipity_specialchars($parts[0]) . ($idx ?? '');

        $media['base_property'][$propkey] = array(
            'label' => serendipity_specialchars(defined('MEDIA_PROPERTY_' . strtoupper($parts[0])) ? constant('MEDIA_PROPERTY_' . strtoupper($parts[0])) : $parts[0]),
            'type'  => $type,
            'val'   => $val,
            'title' => serendipity_specialchars($parts[0])
        );

        if (!is_array($GLOBALS['IPTC'] ?? null)) {
            // Your templates config.inc.php or any of the language files can declare this variable,
            // if you want to use other default settings for this. No interface ability to declare this
            // yet, sorry.
            $GLOBALS['IPTC'] = array(
                'DATE'          => array('DateCreated'),
                'RUN_LENGTH'    => array('RunLength'),
                'DPI'           => array('XResolution'),
                'COPYRIGHT'     => array('Creator'),
                'TITLE'         => array('Title', 'ObjectName'),
                'COMMENT1'      => array('Description'),
                'ALT'           => array('Title', 'ObjectName'),
                'COMMENT2'      => array('Keywords', 'PhotoLocation')
            );
        }

        $default_iptc_val = null;
        if (empty($val)) {
            switch($parts[0]) {
                case 'DATE':
                    $default_iptc_val = serendipity_serverOffsetHour();

                case 'RUN_LENGTH':
                    if (!isset($default_iptc_val)) {
                        $default_iptc_val = '00:00:00.00';
                    }

                case 'DPI':
                    if (!isset($default_iptc_val)) {
                        $default_iptc_val = '72';
                    }

                case 'COPYRIGHT':
                    if (!isset($default_iptc_val)) {
                        $default_iptc_val = $serendipity['serendipityUser'];
                    }

                case 'TITLE':
                    if (!isset($default_iptc_val)) {
                        $default_iptc_val = $media['realname'];
                    }

                case 'ALT':
                    if (!isset($default_iptc_val)) {
                        $default_iptc_val = '';
                    }

                case 'COMMENT1':
                    if (!isset($default_iptc_val)) {
                        $default_iptc_val = '';
                    }

                case 'COMMENT2':
                    if (!isset($default_iptc_val)) {
                        $default_iptc_val = '';
                    }

                    $media['base_property'][$propkey]['val'] = serendipity_pickKey($media['metadata'], 'Keywords', '');

                    $new_iptc_val     = false;
                    foreach($GLOBALS['IPTC'][$parts[0]] AS $iptc_key) {
                        if (empty($new_iptc_val)) {
                            $new_iptc_val = serendipity_pickKey($media['metadata'], $iptc_key, '');
                        }
                    }

                    if (empty($new_iptc_val)) {
                        $new_iptc_val = $default_iptc_val;
                    }

                    if ($parts[0] == 'DATE') {
                        $media['base_property'][$propkey]['val'] = serendipity_strftime(DATE_FORMAT_SHORT, $new_iptc_val);
                    } else {
                        $media['base_property'][$propkey]['val'] = $new_iptc_val;
                    }

                    break;

                default:
                    serendipity_plugin_api::hook_event('media_showproperties', $media, $propkey);
                    break;
            }
        }
    }

    if ($keywordsPerBlock > 0) {
        $rows  = ceil(count($keywords) / $keywordsPerBlock);
        for($i = 0; $i < $rows; $i++) {
            for ($j = 0; $j < $keywordsPerBlock; $j++) {
                $kidx = ($i*$keywordsPerBlock) + $j;
                if (isset($keywords[$kidx])) {
                    $media['base_keywords'][$i][$j] = array(
                        'name'      => serendipity_specialchars($keywords[$kidx]),
                        'selected'  => isset($props['base_keyword'][$keywords[$kidx]]) ? true : false
                    );
                } else {
                    $media['base_keywords'][$i][$j] = array();
                }
            }
        }
    }
}

/**
 * Tries to auto-convert specific fields into DB-storable values
 *
 * @param  string   The keyname
 * @param  string   The value
 * @param  string   Invert?
 * @return array    array('image_id') holding the last created thumbnail for immediate processing
 *
 */
function serendipity_mediaTypeCast($key, $val, $invert = false) {
    if (stristr($key, 'date') !== FALSE) {
        if ($invert && is_numeric($val)) {
            return serendipity_strftime(DATE_FORMAT_SHORT, $val, false);
        } elseif ($invert === false) {
            $tmp = strtotime($val);
            if ($tmp !== FALSE && $tmp > 1) {
                return $tmp;
            }
        }
    } elseif ($invert && stristr($key, 'length') !== FALSE) {
        $tmp = '';

        $hours    = intval(intval($val) / 3600);
        $minutes  = intval(($val / 60) % 60);
        $seconds  = intval($val % 60);
        $mseconds = intval((($val - $seconds) * 100) % 100);

        $tmp .= str_pad($hours, 2, '0', STR_PAD_LEFT) . ':';
        $tmp .= str_pad($minutes, 2, '0', STR_PAD_LEFT). ':';
        $tmp .= str_pad($seconds, 2, '0', STR_PAD_LEFT) . '.';
        $tmp .= str_pad($mseconds, 2, '0', STR_PAD_LEFT);

        return $tmp;
    } elseif ($invert === false && preg_match('@^([0-9]+):([0-9]+):([0-9]+).([0-9]+)$@i', $val, $m)) {
        $tmp = ($m[1] * 3600)
             + ($m[2] * 60)
             + ($m[3])
             + ($m[4] / 100);
        return $tmp;
    }

    return $val;
}

/**
 * Inserts media properties
 *
 * @param   string  Property_group
 * @return array    array('image_id') holding the last created thumbnail for immediate processing
 *
 */
function serendipity_insertMediaProperty($property_group, $property_subgroup, $image_id, &$media, $use_cast = true) {
    global $serendipity;

    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}mediaproperties
                                WHERE mediaid = " . (int)$image_id . "
                                  " . ($property_subgroup != 'ALL' ? "AND property_subgroup = '" . serendipity_db_escape_string($property_subgroup) . "'" : '') . "
                                  AND property_group = '" . serendipity_db_escape_string($property_group) . "'");

    if (is_array($media)) {
        foreach($media AS $key => $val) {
            if ($key == 'image_id') {
                continue;
            }

            if (is_array($val)) {
                $use_property_subgroup = $key;
                $use_val = $val;
            } else {
                $use_property_subgroup = $property_subgroup;
                $use_val = array($key => $val);
            }

            foreach($use_val AS $insert_key => $insert_val) {
                if ($use_cast) {
                    $insert_val = serendipity_mediaTypeCast($insert_key, $insert_val);
                }
                $q = sprintf("INSERT INTO {$serendipity['dbPrefix']}mediaproperties
                                          (mediaid, property_group, property_subgroup, property, value)
                                   VALUES (%d, '%s', '%s', '%s', '%s')",
                             $image_id,
                             serendipity_db_escape_string($property_group),
                             serendipity_db_escape_string($use_property_subgroup),
                             serendipity_db_escape_string($insert_key),
                             serendipity_db_escape_string($insert_val));
                serendipity_db_query($q);
            }
        }
    }
}

/**
 * Inserts the submitted properties of uploaded media items
 *
 * @return array    array('image_id') holding the last created thumbnail for immediate processing
 *
 */
function serendipity_parsePropertyForm() {
    global $serendipity;

    if (!is_array($serendipity['POST']['mediaProperties'])) {
        return false;
    }

    serendipity_checkPropertyAccess($serendipity['POST']['mediaProperties'], $serendipity['POST']['mediaKeywords'], 'write');

    foreach($serendipity['POST']['mediaProperties'] AS $id => $media) {
        serendipity_insertMediaProperty('base_property', '', $media['image_id'], $media);

        $s9y_img = $media['internal'];
        $s9y_img['image_id'] = $media['image_id'];
        serendipity_prepareMedia($s9y_img);
        $s9y_img['metadata'] =& serendipity_getMetaData($s9y_img['realfile'], $s9y_img['header']);
        serendipity_insertMediaProperty('base_metadata', 'ALL', $media['image_id'], $s9y_img['metadata']);
        $s9y_img['hidden'] = array(
            'author'   => $serendipity['serendipityUser'],
            'authorid' => $serendipity['authorid']
        );
        serendipity_insertMediaProperty('base_hidden', '', $media['image_id'], $s9y_img['hidden']);

        if ($serendipity['POST']['oldDir'][$id] != $serendipity['POST']['newDir'][$id]) {
            serendipity_moveMediaDirectory(
                serendipity_uploadSecure($serendipity['POST']['oldDir'][$id]),
                serendipity_uploadSecure($serendipity['POST']['newDir'][$id]),
                'filedir',
                $media['image_id']);
        }
    }

    if (is_array($serendipity['POST']['mediaKeywords'])) {
        foreach($serendipity['POST']['mediaKeywords'] AS $id => $keywords) {
            serendipity_insertMediaProperty('base_keyword', '', $serendipity['POST']['mediaProperties'][$id]['image_id'], $keywords);
        }
    }

    $array = array(
        'image_id' => $serendipity['POST']['mediaProperties'][0]['image_id'],
    );

    return $array;
}

/**
 * Fetches existing Media Properties for images
 *
 * @param  int      The media item id
 * @return array    Array of image metadata
 *
 */
function &serendipity_fetchMediaProperties($id) {
    global $serendipity;

    $sql = "SELECT mediaid, property, property_group, property_subgroup, value
              FROM {$serendipity['dbPrefix']}mediaproperties
             WHERE mediaid IN (" . (is_array($id) ? serendipity_db_implode(',', $id) : (int)$id) . ")";
    $rows  = serendipity_db_query($sql, false, 'assoc');
    $props = array();
    if (is_array($rows)) {
        foreach($rows AS $row) {
            if (empty($row['property_subgroup'])) {
                if (is_array($id)) {
                    $props[$row['mediaid']][$row['property_group']][$row['property']] = $row['value'];
                } else {
                    $props[$row['property_group']][$row['property']] = $row['value'];
                }
            } else {
                if (is_array($id)) {
                    $props[$row['mediaid']][$row['property_group']][$row['property_subgroup']][$row['property']] = $row['value'];
                } else {
                    $props[$row['property_group']][$row['property_subgroup']][$row['property']] = $row['value'];
                }
            }
        }
    }
    return $props;
}

/**
 * Checks if properties to a specific image are allowed to be fetched
 *
 * @param  array    Array of image metadata
 * @param  array    Array of additional image metadata
 * @param  string   ACL toggle type ('read', 'write')
 * @return array    Stripped Array of image metadata
 *
 */
function serendipity_checkPropertyAccess(&$new_media, &$additional, $mode = 'read') {
    global $serendipity;

    // Strip out images we don't have access to
    $ids = array();
    foreach($new_media AS $id => $item) {
        $ids[] = $item['image_id'];
    }

    $valid_images = serendipity_fetchImageFromDatabase($ids, $mode);
    foreach ($new_media AS $id => $media) {
        if (!isset($valid_images[$media['image_id']])) {
            unset($new_media[$id]);
            unset($additional[$id]);
        } else {
            $new_media[$id]['internal'] = $valid_images[$media['image_id']];
        }
    }

    return true;
}

/**
 * Prepare a media item for showing
 *
 * @param  array    Array of image metadata
 * @param  string   URL for maintenance tasks, set when using the ML for inserting images
 * @return bool
 *
 */
function serendipity_prepareMedia(&$file, $url = '') {
    global $serendipity;
    static $full_perm = null;

    if ($full_perm === null) {
        $full_perm = serendipity_checkPermission('adminImagesMaintainOthers');
    }

    $sThumbSource = serendipity_getThumbNailPath($file['path'], $file['name'], $file['extension'], $file['thumbnail_name']);
    if (! $file['hotlink']) {
        $file['full_thumb']     = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $sThumbSource;
        $file['full_thumbHTTP'] = $serendipity['serendipityHTTPPath'] . $serendipity['uploadHTTPPath'] . $sThumbSource;
    }

    $file['url'] = $url;

    if ($file['hotlink']) {
        $file['full_file']  = $file['path'];
        $file['show_thumb'] = $file['path'];
        if (!isset($file['imgsrc'])) {
            $file['imgsrc'] = $file['show_thumb'];
        }
    } else {
        $file['full_file']  = $serendipity['serendipityHTTPPath'] . $serendipity['uploadHTTPPath'] . $file['path'] . $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
        $file['show_thumb'] = $file['full_thumbHTTP'];
        if (!isset($file['imgsrc'])) {
            $file['imgsrc'] = $serendipity['uploadHTTPPath'] . $file['path'] . $file['name'] . (!empty($file['thumbnail_name']) ? '.' . $file['thumbnail_name'] : '') . (empty($file['extension']) ? '' : '.' . $file['extension']);
        }
    }

    // Detect PDF thumbs
    if (file_exists($file['full_thumb'] . '.png')) {
        $file['full_thumb']     .= '.png';
        $file['full_thumbHTTP'] .= '.png';
        $file['show_thumb']     .= '.png';
        $sThumbSource           .= '.png';
    }

    if (empty($file['realname'])) {
        $file['realname'] = $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);
    }
    $file['diskname'] = $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);

    $file['links'] = array('imagelinkurl' => $file['full_file']);
    $file['realfile']  = $serendipity['serendipityPath'] . $serendipity['uploadPath'] . $file['path'] . $file['name'] . (empty($file['extension']) ? '' : '.' . $file['extension']);

    $file['dim']       = @getimagesize($file['full_thumb'], $file['thumb_header']);
    $file['dim_orig']  = @getimagesize($file['realfile'], $file['header']);
    $file['is_image']  = serendipity_isImage($file);

    if ($file['is_image']) {
        $file['mediatype'] = 'image';
    } elseif (0 === strpos(strtolower($file['displaymime']), 'video/') || 0 === strpos(strtolower($file['displaymime']), 'application/x-shockwave')) {
        $file['mediatype'] = 'video';
    } elseif (0 === strpos(strtolower($file['displaymime']), 'audio/') || 0 === strpos(strtolower($file['displaymime']), 'application/vnd.rn-') || 0 === strpos(strtolower($file['displaymime']), 'application/ogg')) {
        $file['mediatype'] = 'audio';
    } elseif (0 === strpos(strtolower($file['displaymime']), 'text/')) {
        $file['mediatype'] = 'document';
    } elseif (preg_match('@application/(pdf|rtf|msword|msexcel|excel|x-excel|mspowerpoint|postscript|vnd\.ms*|powerpoint)@i', $file['displaymime'])) {
        $file['mediatype'] = 'document';
    } elseif (preg_match('@application/(java-archive|zip|gzip|arj|x-bzip|x-bzip2|x-compressed|x-gzip|x-stuffit)@i', $file['displaymime'])) {
        $file['mediatype'] = 'archive';
    } else {
        $file['mediatype'] = 'binary';
    }

    if ($full_perm || $serendipity['authorid'] == $file['authorid'] || $file['authorid'] == '0') {
        $file['is_editable'] = true;
    } else {
        $file['is_editable'] = false;
    }

    /* If it is an image, and the thumbnail exists */
    if ($file['is_image'] && file_exists($file['full_thumb'])) {
        $file['thumbWidth']  = $file['dim'][0];
        $file['thumbHeight'] = $file['dim'][1];
    } elseif ($file['is_image'] && $file['hotlink']) {
        $sizes = serendipity_calculate_aspect_size($file['dimensions_width'], $file['dimensions_height'], $serendipity['thumbSize'], $serendipity['thumbConstraint']);
        $file['thumbWidth']  = $sizes[0];
        $file['thumbHeight'] = $sizes[1];
    /* If it's not an image, or the thumbnail does not exist */
    } else {
        $mimeicon = serendipity_getTemplateFile('admin/img/mime_' . preg_replace('@[^a-z0-9\-\_]@i', '-', $file['mime']) . '.png');
        if (!$mimeicon) {
            $mimeicon = serendipity_getTemplateFile('admin/img/mime_unknown.png');
        }
        $file['mimeicon'] = $mimeicon;
    }

    $file['popupWidth']   = ($file['is_image'] ? ($file['dimensions_width']  + 20) : 600);
    $file['popupHeight']  = ($file['is_image'] ? ($file['dimensions_height'] + 20) : 500);
    if ($file['hotlink']) {
        $file['nice_hotlink'] = wordwrap($file['path'], 45, '<br />', 1);
    }
    $file['nice_size']    = number_format(round($file['size']/1024, 2), NUMBER_FORMAT_DECIMALS, NUMBER_FORMAT_DECPOINT, NUMBER_FORMAT_THOUSANDS);

    return true;
}

/**
 * Prints a media item
 *
 * @param  array    Array of image metadata
 * @param  string   URL for maintenance tasks
 * @param  boolean  Whether to show maintenance task items
 * @param  int      how many media items to display per row
 * @param  boolean  Enclose within a table cell?
 * @param  array    Additional Smarty variables
 * @return string   Generated HTML
 *
 */
function serendipity_showMedia(&$file, &$paths, $url = '', $manage = false, $lineBreak = 3, $enclose = true, $smarty_vars = array()) {
    global $serendipity;

    $form_hidden = '';
    // do not add, if not for the default media list form
    if (($serendipity['GET']['adminAction'] == 'default' || empty($serendipity['GET']['adminAction'])) && ! ($serendipity['GET']['fid'] ?? null)) {
        foreach($serendipity['GET'] AS $g_key => $g_val) {
            // do not add token, since this is assigned separately to properties and list forms
            if (!is_array($g_val) && $g_key != 'page' && $g_key != 'token') {
                $form_hidden .= '        <input type="hidden" name="serendipity[' . $g_key . ']" value="' . serendipity_specialchars($g_val) . '">'."\n";
            }
        }
    }

    // Add variables to the hidden form, so they persist in the ML popup after an item as been uploaded and the
    // form filter gets used
    if ($serendipity['GET']['adminAction'] == 'add') {
        $target_fields = ['textarea', 'htmltarget', 'filename_only', 'noBanner', 'noSidebar', 'noFooter', 'showMediaToolbar', 'multiselect', 'showUpload'];
        foreach ($target_fields as $target_field) {
            if (isset($serendipity['GET'][$target_field])) {
                $form_hidden .= '        <input type="hidden" name="serendipity[' . $target_field . ']" value="' . serendipity_specialchars($serendipity['GET'][$target_field]) . '">'."\n";
            }
        }
        $form_hidden .=  '        <input type="hidden" name="serendipity[adminModule]" value="media">'."\n";
    }

    if (!is_object($serendipity['smarty'])) {
        serendipity_smarty_init();
    }
    $order_fields = serendipity_getImageFields();

    $media = array(
        'manage'            => $manage,
        'multiperm'         => serendipity_checkPermission('adminImagesDirectories'),
        'lineBreak'         => $lineBreak,
        'lineBreakP'        => round(1/$lineBreak*100),
        'url'               => $url,
        'enclose'           => $enclose,
/*        'zoomIMG'           => serendipity_getTemplateFile('admin/img/big_zoom.png'),
        'renameIMG'         => serendipity_getTemplateFile('admin/img/big_rename.png'),
        'resizeIMG'         => serendipity_getTemplateFile('admin/img/big_resize.png'),
        'rotatecwIMG'       => serendipity_getTemplateFile('admin/img/big_rotate_cw.png'),
        'rotateccwIMG'      => serendipity_getTemplateFile('admin/img/big_rotate_ccw.png'),
        'configureIMG'      => serendipity_getTemplateFile('admin/img/configure.png'),
        'deleteIMG'         => serendipity_getTemplateFile('admin/img/big_delete.png'),
        'prevIMG'           => serendipity_getTemplateFile('admin/img/previous.png'),
        'nextIMG'           => serendipity_getTemplateFile('admin/img/next.png'),*/
        'token'             => serendipity_setFormToken(),
        'form_hidden'       => $form_hidden,
        'blimit_path'       => empty($smarty_vars['limit_path']) ? '' : basename($smarty_vars['limit_path']),
        'only_path'         => $serendipity['GET']['only_path'] ?? null,
        'only_filename'     => $serendipity['GET']['only_filename'] ?? null,
        'sortorder'         => $serendipity['GET']['sortorder'] ?? null,
        'keywords_selected' => $serendipity['GET']['keywords'] ?? null,
        'filter'            => $serendipity['GET']['filter'] ?? ['fileCategory' => null],
        'sort_order'        => $order_fields,
        'simpleFilters'     => $serendipity['simpleFilters'] ?? true,
        'hideSubdirFiles'   => $serendipity['GET']['hideSubdirFiles'] ?? null,
        'authors'           => serendipity_fetchUsers(),
        'sort_row_interval' => array(8, 16, 50, 100),
        'nr_files'          => count($file),
        'keywords'          => explode(';', $serendipity['mediaKeywords']),
        'thumbSize'         => $serendipity['thumbSize'],
        'multiselect'       => isset($serendipity['GET']['multiselect']) ? serendipity_db_bool($serendipity['GET']['multiselect']) : true,
        'sortParams'        => array('perpage', 'order', 'ordermode')
    );

    $media = array_merge($media, $smarty_vars);
    $media['files'] =& $file;

    if (is_array($paths) && count($paths) > 0) {
        $media['paths'] =& $paths;
    } else {
        $media['paths'] =& serendipity_getMediaPaths();
    }

    if (! isset($media['filter']['fileCategory']) ) { $media['filter']['fileCategory'] = null; }

    $serendipity['smarty']->assignByRef('media', $media);

    if ($enclose) {
        serendipity_smarty_fetch('MEDIA_ITEMS', 'admin/media_items.tpl');
        return serendipity_smarty_show('admin/media_pane.tpl');
    } else {
        serendipity_smarty_fetch('MEDIA_ITEMS', 'admin/media_items.tpl');
        return serendipity_smarty_show('admin/media_properties.tpl');
    }
}

/**
 * Convert a IPTC/EXIF/XMP item
 *
 * @param  string   The content
 * @param  string   The type of the content
 * @return string   The converted content
 *
 */
function serendipity_metaFieldConvert(&$item, $type) {
    switch($type) {
        case 'math':
            $parts = explode('/', $item);
            return ($parts[1] > 0) ? ($parts[0] / $parts[1]) : 0;
            break;

        case 'or':
            if ($item == '1') {
                return 'Landscape';
            } else {
                return 'Portrait';
            }

        case 'date':
            return strtotime($item);
            break;

        case 'date2':
            $parts = preg_split('&[ :]&', $item);
            return mktime($parts[3], $parts[4], $parts[5], $parts[1], $parts[2], $parts[0]);
            break;

        case 'IPTCdate':
            preg_match('@(\d{4})(\d{2})(\d{2})@',$item,$parts);
            return mktime(0, 0, 0, intval($parts[2]), intval($parts[3]), intval($parts[1]));
            break;

        case 'IPTCtime':
            preg_match('@(\d{2})(\d{2})(\d{2})([\+-])(\d{2})(\d{2})@',$item,$parts);
            $time = serendipity_strftime("%H:%M",mktime(intval($parts[1]), intval($parts[2]), intval($parts[3]), 0, 0, 0));
            $timezone = serendipity_strftime("%H:%M",mktime(intval($parts[5]), intval($parts[6]), 0, 0, 0, 0));
            return $time." GMT".$parts[4].$timezone;
            break;

        case 'rdf':
            if (preg_match('@<rdf:li[^>]*>(.*)</rdf:li>@i', $item, $ret)) {
                return $ret[1];
            }
            break;

        case 'text':
        default:
            return trim($item);
            break;
    }

    return '';
}

/**
 * Get the RAW media header data (XMP)
 *
 * @param  string   Filename
 * @return array    The raw media header data
 *
 * Inspired, but rewritten,  by "PHP JPEG Metadata Toolkit" from http://electronics.ozhiker.com.
 * Code is GPL so sadly we couldn't bundle that GREAT library.
 */
function serendipity_getMediaRaw($filename) {
    $abort = false;

    $f = @fopen($filename, 'rb');
    $ret = array();
    if (!$f) {
        return $ret;
    }

    $filedata = fread($f, 2);

    if ($filedata != "\xFF\xD8") {
        fclose($f);
        return $ret;
    }

    $filedata = fread($f, 2);

    if ($filedata[0] != "\xFF") {
        fclose($f);
        return $ret;
    }

    while (!$abort && !feof($f) && $filedata[1] != "\xD9") {
        if ((ord($filedata[1]) < 0xD0) || (ord($filedata[1]) > 0xD7)) {
            $ordret   = fread($f, 2);
            $ordstart = ftell($f);
            $int      = unpack('nsize', $ordret);

            if (ord($filedata[1]) == 225) {
                $content  = fread($f, $int['size'] - 2);

                if (substr($content, 0, 24) == 'http://ns.adobe.com/xap/') {
                    $ret[] = array(
                        'ord'      => ord($filedata[1]),
                        'ordstart' => $ordstart,
                        'int'      => $int,
                        'content'  => $content
                    );
                }
            } else {
                fseek($f, $int['size'] - 2, SEEK_CUR);
            }
        }

        if ($filedata[1] == "\xDA") {
            $abort = true;
        } else {
            $filedata = fread($f, 2);
            if ($filedata[0] != "\xFF") {
                fclose($f);
                return $ret;
            }
        }
    }

    fclose($f);

    return $ret;
}

/**
 * Get the IPTC/EXIF/XMP media metadata
 *
 * @param  string   Filename
 * @return array    The raw media header data
 *
 */
function &serendipity_getMetaData($file, &$info) {
    global $serendipity;

    # Fields taken from: http://demo.imagefolio.com/demo/ImageFolio31_files/skins/cool_blue/images/iptc.html
    static $IPTC_Fields = array(
    '2#005' => 'ObjectName',
    '2#025' => 'Keywords',
    '2#026' => 'LocationCode',
    '2#027' => 'LocationName',
    '2#030' => 'ReleaseDate',
    '2#035' => 'ReleaseTime',
    '2#037' => 'ExpirationDate',
    '2#038' => 'ExpirationTime',
    '2#055' => 'IPTCDateCreated',
    '2#060' => 'IPTCTimeCreated',
    '2#062' => 'DigitalDateCreated',
    '2#063' => 'DigitalTimeCreated',
    '2#065' => 'Software',
    '2#070' => 'SoftwareVersion',
    '2#080' => 'Photographer',
    '2#085' => 'Photographer Name',
    '2#090' => 'PhotoLocation',
    '2#092' => 'PhotoLocation2',
    '2#095' => 'PhotoState',
    '2#100' => 'PhotoCountryCode',
    '2#101' => 'PhotoCountry',
    '2#105' => 'Title',
    '2#110' => 'Credits',
    '2#115' => 'Source',
    '2#116' => 'Creator',
    '2#118' => 'Contact',
    '2#120' => 'Description',
    '2#131' => 'Orientation',
    '2#150' => 'AudioType',
    '2#151' => 'AudioSamplingRate',
    '2#152' => 'AudioSamplingResolution',
    '2#153' => 'AudioDuration'
    );

    static $ExifFields = array(
        'IFD0' => array(
            'Make'         => array('type' => 'text',  'name' => 'CameraMaker'),
            'Model'        => array('type' => 'text',  'name' => 'CameraModel'),
            'Orientation'  => array('type' => 'or',    'name' => 'Orientation'),
            'XResolution'  => array('type' => 'math',  'name' => 'XResolution'),
            'YResolution'  => array('type' => 'math',  'name' => 'YResolution'),
            'Software'     => array('type' => 'text',  'name' => 'Software'),
            'DateTime'     => array('type' => 'date2', 'name' => 'DateCreated'),
            'Artist'       => array('type' => 'text',  'name' => 'Creator'),
        ),

        'EXIF' => array(
            'ExposureTime'          => array('type' => 'math',  'name' => 'ExposureTime'),
            'ApertureValue'         => array('type' => 'math',  'name' => 'ApertureValue'),
            'MaxApertureValue'      => array('type' => 'math',  'name' => 'MaxApertureValue'),
            'ISOSpeedRatings'       => array('type' => 'text',  'name' => 'ISOSpeedRatings'),
            'DateTimeOriginal'      => array('type' => 'date2', 'name' => 'DateCreated'),
            'MeteringMode'          => array('type' => 'text',  'name' => 'MeteringMode'),
            'FNumber'               => array('type' => 'math',  'name' => 'FNumber'),
            'ExposureProgram'       => array('type' => 'text',  'name' => 'ExposureProgram'),
            'FocalLength'           => array('type' => 'math',  'name' => 'FocalLength'),
            'WhiteBalance'          => array('type' => 'text',  'name' => 'WhiteBalance'),
            'DigitalZoomRatio'      => array('type' => 'math',  'name' => 'DigitalZoomRatio'),
            'FocalLengthIn35mmFilm' => array('type' => 'text',  'name' => 'FocalLengthIn35mmFilm'),
            'Flash'                 => array('type' => 'text',  'name' => 'Flash'),
            'Fired'                 => array('type' => 'text',  'name' => 'FlashFired'),
            'RedEyeMode'            => array('type' => 'text',  'name' => 'RedEyeMode'),
        )
    );

    static $xmpPatterns = array(
        'tiff:Orientation'           => array('type' => 'or',   'name' => 'Orientation'),
        'tiff:XResolution'           => array('type' => 'math', 'name' => 'XResolution'),
        'tiff:YResolution'           => array('type' => 'math', 'name' => 'YResolution'),
        'tiff:Make'                  => array('type' => 'text', 'name' => 'CameraMaker'),
        'tiff:Model'                 => array('type' => 'text', 'name' => 'CameraModel'),
        'xap:ModifyDate'             => array('type' => 'date', 'name' => 'DateModified'),
        'xap:CreatorTool'            => array('type' => 'text', 'name' => 'Software'),
        'xap:CreateDate'             => array('type' => 'date', 'name' => 'DateCreated'),
        'xap:MetadataDate'           => array('type' => 'date', 'name' => 'DateMetadata'),

        'exif:ExposureTime'          => array('type' => 'math',  'name' => 'ExposureTime'),
        'exif:ApertureValue'         => array('type' => 'math',  'name' => 'ApertureValue'),
        'exif:MaxApertureValue'      => array('type' => 'math',  'name' => 'MaxApertureValue'),
        'exif:ISOSpeedRatings'       => array('type' => 'text',  'name' => 'ISOSpeedRatings'),
        'exif:DateTimeOriginal'      => array('type' => 'date',  'name' => 'DateCreated'),
        'exif:MeteringMode'          => array('type' => 'text',  'name' => 'MeteringMode'),
        'exif:FNumber'               => array('type' => 'math',  'name' => 'FNumber'),
        'exif:ExposureProgram'       => array('type' => 'text',  'name' => 'ExposureProgram'),
        'exif:FocalLength'           => array('type' => 'math',  'name' => 'FocalLength'),
        'exif:WhiteBalance'          => array('type' => 'text',  'name' => 'WhiteBalance'),
        'exif:DigitalZoomRatio'      => array('type' => 'math',  'name' => 'DigitalZoomRatio'),
        'exif:FocalLengthIn35mmFilm' => array('type' => 'text',  'name' => 'FocalLengthIn35mmFilm'),
        'exif:Fired'                 => array('type' => 'text',  'name' => 'FlashFired'),
        'exif:RedEyeMode'            => array('type' => 'text',  'name' => 'RedEyeMode'),

        'dc:title'                   => array('type' => 'rdf',   'name' => 'Title'),
        'dc:creator'                 => array('type' => 'rdf',   'name' => 'Creator'),
    );

    $ret = array();

    if (!$serendipity['mediaExif']) {
        return $ret;
    }

    if (!file_exists($file)) {
        return $ret;
    }

    if (function_exists('iptcparse') && is_array($info) && isset($info['APP13'])) {
        $iptc = iptcparse($info['APP13']);
        foreach($IPTC_Fields AS $field => $desc) {
            if ($iptc[$field]) {
                if (is_array($iptc[$field])) {
                    $ret['IPTC'][$desc] = trim(implode(';', $iptc[$field]));
                } else {
                    $ret['IPTC'][$desc] = trim($iptc[$field]);
                }

                switch ($desc) {
                    case 'IPTCDateCreated':
                        $ret['IPTC'][$desc] = serendipity_metaFieldConvert($ret['IPTC'][$desc],'IPTCdate');
                        break;
                    case 'IPTCTimeCreated':
                        $ret['IPTC'][$desc] = serendipity_metaFieldConvert($ret['IPTC'][$desc],'IPTCtime');
                        break;
                }
            }
        }
    }

    if (function_exists('exif_read_data') && is_array($info)) {
        $exif = @exif_read_data($file, 'FILE,COMPUTED,ANY_TAG,IFD0,COMMENT,EXIF', true, false);
        if (is_array($exif)) {
            foreach($ExifFields AS $Exifgroup => $ExifField) {
                foreach($ExifField AS $ExifName => $ExifItem) {
                    if (!isset($exif[$Exifgroup][$ExifName])) {
                        continue;
                    }
                    $ret['EXIF'][$ExifItem['name']] = serendipity_metaFieldConvert($exif[$Exifgroup][$ExifName], $ExifItem['type']);
                    if ($ret['EXIF'][$item['name']] == $ret['IPTC'][$item['name']]) {
                        unset($ret['IPTC'][$item['name']]);
                    }
                }
            }
        }
    }

    $xmp = serendipity_getMediaRaw($file);
    foreach($xmp AS $xmp_data) {
        if (empty($xmp_data['content'])) {
            continue;
        }
        foreach($xmpPatterns AS $lookup => $item) {
            if (preg_match('@<' . $lookup . '>(.*)</' . $lookup . '>@', $xmp_data['content'], $match)) {
                $ret['XMP'][$item['name']] = serendipity_metaFieldConvert($match[1], $item['type']);
                if ($ret['EXIF'][$item['name']] == $ret['XMP'][$item['name']]) {
                    unset($ret['EXIF'][$item['name']]);
                }
            }
        }
    }

    serendipity_plugin_api::hook_event('media_getproperties', $ret, $file);

    return $ret;
}

/**
 * Parses an existing filename and increases the filecount.
 *
 * @param  string   The (duplicate) filename
 * @param  string   The full path to the (duplicate) filename
 * @param  string   The directory of the (duplicate) filename
 * @param  boolean  Show new filename?
 * @return string   The new filename
 *
 */
function serendipity_imageAppend(&$tfile, &$target, $dir, $echo = true) {
    static $safe_bail = 20;

    $realname = $tfile;
    list($filebase, $extension) = serendipity_parseFileName($tfile);

    $cnum = 1;
    if (preg_match('@^(.*)([0-9]+)$@', $filebase, $match)) {
        $cnum     = $match[2];
        $filebase = $match[1];
    }

    $i = 0;
    while ($i <= $safe_bail && file_exists($dir . $filebase . $cnum . (empty($extension) ? '' : '.' . $extension))) {
        $cnum++;
    }

    // Check if the file STILL exists and append a MD5 if that's the case. That should be unique enough.
    if (file_exists($dir . $filebase . $cnum . (empty($extension) ? '' : '.' . $extension))) {
        $cnum = md5(time() . $filebase);
    }

    // Those variables are passed by reference!
    $tfile  = $filebase . $cnum . (empty($extension) ? '' : '.' . $extension);
    $target = $dir . $tfile;

    if ($echo) {
        echo '<span class="msg_success"><span class="icon-ok-circled" aria-hidden="true"></span> <strong>';
        printf(FILENAME_REASSIGNED . '<br />', serendipity_specialchars($tfile));
        echo "</strong></span>\n";
    }
    return $realname;
}

/**
 * Checks if an uploaded media item hits any configured limits.
 *
 * @param  string   The filename
 * @return boolean  TRUE when file is okay, FALSE when it is beyond limits
 *
 */
function serendipity_checkMediaSize($file) {
    global $serendipity;

    if (!empty($serendipity['maxFileSize'])) {
        if (filesize($file) > $serendipity['maxFileSize']) {
            echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ';
            printf(MEDIA_UPLOAD_SIZEERROR . '<br />', (int)$serendipity['maxFileSize']);
            echo "</span>\n";
            return false;
        }
    }

    if (!empty($serendipity['maxImgWidth']) || !empty($serendipity['maxImgHeight'])) {
        $dim = serendipity_getimagesize($file);
        if (!is_array($dim) || !isset($dim[0])) {
            return true;
        }

        if (!empty($serendipity['maxImgWidth'])) {
            if ($dim[0] > $serendipity['maxImgWidth']) {
                echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ';
                printf(MEDIA_UPLOAD_DIMERROR . '<br />', (int)$serendipity['maxImgWidth'], (int)$serendipity['maxImgHeight']);
                echo "</span>\n";
                return false;
            }
        }

        if (!empty($serendipity['maxImgHeight'])) {
            if ($dim[1] > $serendipity['maxImgHeight']) {
                echo '<span class="msg_error"><span class="icon-attention-circled" aria-hidden="true"></span> ';
                printf(MEDIA_UPLOAD_DIMERROR . '<br />', (int)$serendipity['maxImgWidth'], (int)$serendipity['maxImgHeight']);
                echo "</span>\n";
                return false;
            }
        }
    }

    return true;
}

/**
 * DEPRECATED. Use the functions serendipity_renameDir and serendipity_moveFile instead. This is only
 * a wrapper around those, for backwards compatibility
 *
 * Moves a media directory 
 *
 * @param  string   The old directory.
 *                  This can be NULL or (an empty / a) STRING for re-name/multiCheck move comparison events
 * @param  string   The new directory
 * @param  string   The type of what to remove (dir|file|filedir)
 * @param  string   An item id of a file
 * @param  array    Result of serendipity_fetchImageFromDatabase($id)
 * @return boolean
 *
 */
function serendipity_moveMediaDirectory($oldDir, $newDir, $type = 'dir', $item_id = null, $file = null) {
    global $serendipity;
    if ($type == 'dir') {
        serendipity_renameDir($oldDir, $newDir);
    } else {
        return serendipity_moveFileTo($item_id, $newDir);
    }
}

/**
 * Show the Media Library
 *
 * @access  public
 * @param   bool    default false
 * @param   array   $smarty_vars
 * @return  string  Image list
 */
function showMediaLibrary($addvar_check = false, $smarty_vars = array()) {
    global $serendipity;

    if (!serendipity_checkPermission('adminImagesView')) {
        return;
    }
    $output = '';

    // After upload, do not show the list to be able to proceed to
    // media selection.
    if ($addvar_check && !empty($GLOBALS['image_selector_addvars'])) {
        return true;
    }

    if (!isset($serendipity['thumbPerPage'])) {
        $serendipity['thumbPerPage'] = 2;
    }
    $smarty_vars = array(
        'textarea' => isset($serendipity['GET']['textarea']) ? $serendipity['GET']['textarea'] : false,
        'htmltarget' => isset($serendipity['GET']['htmltarget']) ? $serendipity['GET']['htmltarget'] : '',
        'filename_only' => isset($serendipity['GET']['filename_only']) ? $serendipity['GET']['filename_only'] : false,
    );

    if (isset($serendipity['GET']['showUpload'])) {
        $show_upload = $serendipity['GET']['showUpload'];
    } else {
        $show_upload = $serendipity['GET']['showUpload'] = false;
    }

    $output .= serendipity_displayImageList(
        isset($serendipity['GET']['page']) ? $serendipity['GET']['page'] : 1,
        $serendipity['thumbPerPage'],
        isset($serendipity['GET']['showMediaToolbar']) ? serendipity_db_bool($serendipity['GET']['showMediaToolbar']) : true,
        NULL,
        $show_upload,
        NULL,
        $smarty_vars
    );

    return $output;
}

/**
 * Gets all available media directories
 *
 * @return array
 *
 */
function &serendipity_getMediaPaths() {
    global $serendipity;

    $aExclude = array("CVS" => true, ".svn" => true, "_vti_cnf" => true); // add _vti_cnf to exclude possible added servers frontpage extensions
    serendipity_plugin_api::hook_event('backend_media_path_exclude_directories', $aExclude);

    $paths        = array();
    $aResultSet   = serendipity_traversePath(
        $serendipity['serendipityPath'] . $serendipity['uploadPath'],
        '',
        false,
        NULL,
        1,
        NULL,
        FALSE,
        $aExclude
    );

    foreach ($aResultSet AS $sKey => $sFile) {
        if ($sFile['directory']) {
            array_push($paths, $sFile);
        }
        unset($aResultSet[$sKey]);
    }
    serendipity_directoryACL($paths, 'read');

    usort($paths, 'serendipity_sortPath');

    return $paths;
}

/**
 * Checks whether a user has access to write into a directory
 *
 * @access public
 * @param   string Directory to check
 * @return  boolean
 */
function serendipity_checkDirUpload($dir) {
    global $serendipity;

    /*
    if (serendipity_checkPermission('adminImagesMaintainOthers')) {
        return true;
    }
    */

    $allowed  = serendipity_ACLGet(0, 'directory', 'write', $dir);
    $mygroups = serendipity_checkPermission(null, null, true);

    // Usergroup "0" always means that access is granted. If no array exists, no ACL restrictions have been set and all is fine.
    if (!is_array($allowed) || isset($allowed[0])) {
        return true;
    }

    if (!is_array($mygroups)) {
        return true;
    }

    foreach($mygroups AS $grpid => $grp) {
        if (isset($allowed[$grpid])) {
            return true;
            break;
        }
    }

    return false;
}