<?php
/**
 * FileManager.php
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      André Noack <noack@data-quest.de>
 * @author      Moritz Strohm <strohm@data-quest.de>
 * @copyright   2016 Stud.IP Core-Group
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 */

/**
 * The FileManager class contains methods that faciliate the management of files
 * and folders. Furthermore its methods perform necessary additional checks
 * so that files and folders are managed in a correct manner.
 *
 * It is recommended to use the methods of this class for file and folder
 * management instead of writing own methods.
 */
class FileManager
{
    //FILE HELPER METHODS

    /**
     * Removes special characters from the file name (and by that cleaning
     * the file name) so that the file name which is returned by this method
     * works on every operating system.
     *
     * @param string $file_name The file name that shall be "cleaned".
     * @param bool $shorten_name True, if the file name shall be shortened to
     *     31 characters. False, if the full length shall be kept (default).
     *
     * @return string The "cleaned" file name.
     */
    public static function cleanFileName($file_name = null, $shorten_name = false)
    {
        if(!$file_name) {
            //If you put an empty string in, you will get an empty string out!
            return $file_name;
        }

        $bad_characters = [
            ':', chr(92), '/', '"', '>', '<', '*', '|', '?',
            ' ', '(', ')', '&', '[', ']', '#', chr(36), '\'',
            '*', ';', '^', '`', '{', '}', '|', '~', chr(255)
        ];

        $replacement_characters = [
            '', '', '', '', '', '', '', '', '',
            '_', '', '', '+', '', '', '', '', '',
            '', '-', '', '', '', '', '-', '', ''
        ];

        //All control characters shall be deleted:
        for($i = 0; $i < 0x20; $i++) {
            $bad_characters[] = chr($i);
            $replacement_characters[] = '';
        }

        $clean_file_name = str_replace(
            $bad_characters,
            $replacement_characters,
            $file_name
        );

        if($clean_file_name[0] == '.') {
            $clean_file_name = mb_substr($clean_file_name, 1, mb_strlen($clean_file_name));
        }

        if($shorten_name === true) {
            //If we have to shorten the file name we have to split it up
            //into file name and extension.

            $tmp_file_name = pathinfo($clean_file_name, PATHINFO_FILENAME);
            $file_extension = pathinfo($clean_file_name, PATHINFO_EXTENSION);

            $clean_file_name = mb_substr($tmp_file_name, 0, 28)
                . '.'
                . $file_extension;

        }

        return $clean_file_name;
    }


    /**
     * Returns the icon name for a given mime type.
     *
     * @param string $mime_type The mime type whose icon is requested.
     *
     * @return string The icon name for the mime type.
     */
    public static function getIconNameForMimeType($mime_type = null)
    {
        $application_category_icons = [
            'file-pdf'     => ['pdf'],
            'file-ppt'     => ['powerpoint','presentation'],
            'file-excel'   => ['excel', 'spreadsheet', 'csv'],
            'file-word'    => ['word', 'wordprocessingml', 'opendocument.text', 'rtf'],
            'file-archive' => ['zip', 'rar', 'arj', '7z'],
        ];

        if (!$mime_type) {
            //No mime type given: We can only assume it is a generic file.
            return 'file-generic';
        }

        [$category, $type] = explode('/', $mime_type, 2);

        switch($category) {
            case 'image':
                return 'file-pic';
            case 'audio':
                return 'file-audio';
            case 'video':
                return 'file-video';
            case 'text':
                if ($type === 'csv') {
                    //CSV files:
                    return 'file-excel';
                }
                //other text files:
                return 'file-text';
            case 'application':
                //loop through all application category icons
                //and return the icon name that matches the regular expression
                //for an application mime type:
                foreach ($application_category_icons as $icon_name => $type_name) {
                    if (preg_match('/' . implode('|', $type_name) . '/i', $type)) {
                        return $icon_name;
                    }
                }
        }

        //If code execution reaches this point, no special mime type icon
        //was detected.
        return 'file-generic';
    }

    /**
     * Returns the icon for a given mime type.
     *
     * @param string $mime_type  The mime type whose icon is requested.
     * @param string $role       The requested role
     * @param array  $attributes Optional additional attributes
     *
     * @return Icon The icon for the mime type.
     */
    public static function getIconForMimeType(
        $mime_type = null,
        $role = Icon::ROLE_CLICKABLE,
        $attributes = []
    )
    {
        $icon = self::getIconNameForMimeType($mime_type);
        return Icon::create($icon, $role, $attributes);
    }

    /**
     * Returns the icon for a given file ref.
     *
     * @param FileRef|stdClass $ref        The file ref whose icon is requested.
     * @param string           $role       The requested role
     * @param array            $attributes Optional additional attributes
     * @return Icon                        The icon for the file ref.
     */
    public static function getIconForFileRef($ref, $role = Icon::ROLE_CLICKABLE, array $attributes = [])
    {
        return self::getIconForMimeType($ref->mime_type);
    }

    /**
     * Builds a download URL for the file archive of an archived course.
     *
     * @param ArchivedCourse $archived_course An archived course whose file
     *     archive is requested.
     * @param bool $protected_archive True, if the protected file archive
     *     is requested. False, if the "readable for everyone" file archive
     *     is requested (default).
     *
     * @return string The download link for the file or an empty string on failure.
     */
    public static function getDownloadURLForArchivedCourse(
        ArchivedCourse $archived_course,
        $protected_archive = false
    )
    {
        $file_id = $protected_archive
                 ? $archived_course->archiv_protected_file_id
                 : $archived_course->archiv_file_id;

        if ($file_id) {
            $file_name = sprintf(
                '%s-%s.zip',
                $protected_archive ? _('Geschützte Dateisammlung') : _('Dateisammlung'),
                mb_substr($archived_course->name, 0, 200)
            );

            //file_id is set: file archive exists
            return URLHelper::getURL('sendfile.php', [
                'type'           => 1,
                'file_id'        => $file_id,
                'file_name'      => $file_name,
                'force_download' => true, //because archive files are ZIP files
            ], true);
        }

        //file_id is empty: no file archive available
        return '';
    }

    /**
     * Builds a download link for the file archive of an archived course.
     *
     * @param ArchivedCourse $archived_course An archived course whose file
     *     archive is requested.
     * @param bool $protected_archive True, if the protected file archive
     *     is requested. False, if the "readable for everyone" file archive
     *     is requested (default).
     *
     * @return string The download link for the file or an empty string on failure.
     */
    public static function getDownloadLinkForArchivedCourse(
        ArchivedCourse $archived_course,
        $protected_archive = false
    )
    {
        return htmlReady(
            self::getDownloadURLForArchivedCourse(
                $archived_course,
                $protected_archive
            )
        );
    }

    /**
     * Builds a download link for temporary files.
     */
    public static function getDownloadLinkForTemporaryFile(
        $temporary_file_name = null,
        $download_file_name = null
    )
    {
        return htmlReady(
            self::getDownloadURLForTemporaryFile(
                $temporary_file_name,
                $download_file_name
            )
        );
    }


    /**
     * Builds a download URL for temporary files.
     */
    public static function getDownloadURLForTemporaryFile(
        $temporary_file_name = null,
        $download_file_name = null
    )
    {
        return URLHelper::getURL('sendfile.php', [
            'token'          => Token::create(),
            'type'           => 4,
            'file_id'        => $temporary_file_name,
            'file_name'      => $download_file_name,
            'force_download' => true, //because temporary files have a reason for their name
        ], true);
    }

    //FILE METHODS

    /**
     * This is a helper method that checks an uploaded file for errors
     * which appeared during upload.
     * @param array $uploaded_file
     */
    public static function checkUploadedFileStatus($uploaded_file)
    {
        $errors = [];
        if ($uploaded_file['error'] === UPLOAD_ERR_INI_SIZE) {
            $errors[] = sprintf(_('Ein Systemfehler ist beim Upload aufgetreten. Fehlercode: %s.'), 'upload_max_filesize=' . ini_get('upload_max_filesize'));
        } elseif ($uploaded_file['error'] > 0) {
            $errors[] = sprintf(
                _('Ein Systemfehler ist beim Upload aufgetreten. Fehlercode: %s.'),
                $uploaded_file['error']
            );
        }
        return $errors;
    }

    /**
     * Handles uploading one or more files
     *
     * @param array $uploaded_files A two-dimensional array with file data for all uploaded files.
     *     The array has the following structure in the second dimension:
     *      [
     *          'name': The name of the file
     *          'error': An integer telling if there were errors. 0, if no errors occured.
     *          'type': The uploaded file's mime type.
     *          'tmp_name': Name of the temporary file that was created right after the upload.
     *          'size': Size of the uploaded file in bytes.
     *      ]
     * @param FolderType $folder the folder where the files are inserted
     * @param string $user_id the ID of the user who wants to upload files
     *
     * @return array Array with the created file objects and error strings
     */
    public static function handleFileUpload(array $uploaded_files, FolderType $folder, $user_id = null)
    {
        $user_id || $user_id = $GLOBALS['user']->id;
        $result = [];
        $error  = [];

        //check if user has write permissions for the folder:
        if (!$folder->isWritable($user_id)) {
            $error[] = _('Keine Schreibrechte für Zielordner!');
            return compact('error');
        }

        //Check if uploaded files[name] is an array.
        //This check is necessary to find out, if $uploaded_files is a
        //two-dimensional array. Each index of the first dimension
        //contains an array attribute for uploaded files, one entry per file.
        if (is_array($uploaded_files['name'])) {
            foreach ($uploaded_files['name'] as $key => $filename) {
                $uploaded_file = StandardFile::create([
                    'name'     => $filename,
                    'type'     => $uploaded_files['type'][$key] ?: get_mime_type($filename),
                    'size'     => $uploaded_files['size'][$key],
                    'tmp_name' => $uploaded_files['tmp_name'][$key],
                    'error'    =>  $uploaded_files['error'][$key]
                ]);

                if ($uploaded_file instanceof FileType) {
                    //validate the upload by looking at the folder where the
                    //uploaded file shall be stored:
                    if ($folder_error = $folder->validateUpload($uploaded_file, $user_id)) {
                        $error[] = $folder_error;
                        $uploaded_file->delete();
                        continue;
                    }

                    $new_reference = $folder->addFile($uploaded_file, $user_id);
                    if (!$new_reference){
                        $error[] = _('Ein Systemfehler ist beim Upload aufgetreten.');
                    } else {
                        $result['files'][] = $new_reference;
                    }
                } else {
                    $error = array_merge($error, $uploaded_file);
                }
            }
        }
        return array_merge($result, compact('error'));
    }

    /**
     * This method handles updating the File a FileRef is pointing to.
     *
     * The parameters $source, $user and $uploaded_file_data are required
     * for this method to work.
     *
     * @param FileRef $source The file reference pointing to a file that
     *     shall be updated.
     * @param User $user The user who wishes to update the file.
     * @param array $uploaded_file_data The data of the uploaded new version
     *     of the file that is going to be updated.
     * @param bool $update_filename True, if the file name of the File and the
     *     FileRef shall be set to the name of the uploaded new version
     *     of the file. False otherwise.
     * @param bool $update_other_references If other FileRefs pointing to the
     *     File that is going to be updated shall be updated too, set this
     *     to True. In case only the FileRef $source and its file shall be
     *     updated, set this to False. In the latter case the File will be
     *     copied and the copy gets updated.
     *
     * @return FileRef|string[] On success the updated $source FileRef is returned.
     *     On failure an array with error messages is returned.
     */
    public static function updateFileRef(
        FileRef $source,
        User $user,
        $uploaded_file_data = [],
        $update_filename = false,
        $update_other_references = false
    )
    {
        $errors = [];

        // Do some checks:
        $folder = $source->getFolderType();
        $source_file = $source->getFileType();
        if (!$source_file->isEditable($user->id)) {
            $errors[] = sprintf(
                _('Sie sind nicht dazu berechtigt, die Datei %s zu aktualisieren!'),
                $source->name
            );
            return $errors;
        }

        // Check if $uploaded_file_data has valid data in it:
        $upload_error = $folder->validateUpload($source_file, $user->id);
        if ($upload_error) {
            $errors[] = $upload_error;
            return $errors;
        }

        // Ok, checks are completed: We can start updating the file.

        // If we don't update other file references that point to the File instance
        // we must first copy the file and then link the $source FileRef to the
        // new file:

        if (!$source->file) {
            if (!$update_other_references) {
                if (!$update_filename) {
                    $uploaded_file_data['name'] = $source->name;
                } else {
                    if (!$folder->deleteFile($source->getId())){
                        $errors[] = _('Aktualisierte Datei konnte nicht ins Stud.IP Dateisystem übernommen werden!');
                        return $errors;
                    }
                }
                $new_reference = $folder->addFile($source_file);
                if (!$new_reference) {
                    $errors[] = _('Aktualisierte Datei konnte nicht ins Stud.IP Dateisystem übernommen werden!');
                    return $errors;
                }
            }
            return $new_reference;
        }


        if ($update_other_references) {
            // We want to update all file references. In that case we can just
            // use the $source FileRef's file directly.
            $data_file = $source->file;

            $connect_success = $data_file->connectWithDataFile($uploaded_file_data['tmp_name']);
            if (!$connect_success) {
                $errors[] = _('Aktualisierte Datei konnte nicht ins Stud.IP Dateisystem übernommen werden!');
                return $errors;
            }
            // moving the file was successful:
            // update the File object:
            $data_file->size      = filesize($data_file->getPath());
            $data_file->mime_type = get_mime_type($uploaded_file_data['name']);
            if ($update_filename) {
                $data_file->name = $uploaded_file_data['name'];
            }
            $data_file->user_id = $user->id;
            $data_file->store();
        } else {
            // If we want to keep the old version of the file in all other
            // File references we must create a new File object and link
            // the $source FileRef to it:

            $upload_errors = self::checkUploadedFileStatus($uploaded_file_data);

            if ($upload_errors) {
                $errors = array_merge($errors, $upload_errors);
            }

            $data_file = new File();
            $data_file->user_id = $user->id;
            $data_file->id      = $data_file->getNewId();

            $connect_success = $data_file->connectWithDataFile($uploaded_file_data['tmp_name']);
            if (!$connect_success) {
                $errors[] = _('Aktualisierte Datei konnte nicht ins Stud.IP Dateisystem übernommen werden!');
                return $errors;
            }

            // moving the file was successful:
            // update the File object:
            $data_file->size      = filesize($data_file->getPath());
            $data_file->mime_type = get_mime_type($uploaded_file_data['name']);
            if ($update_filename) {
                $data_file->name = $uploaded_file_data['name'];
            } else {
                $data_file->name = $source->file->name;
            }
            $data_file->store();

            $source->file = $data_file;
            $source->store();
        }

        if ($update_filename) {
            $source->name = $uploaded_file_data['name'];
            if ($source->isDirty()) {
                $source->store();
            }

            //We must find all FileRefs that point to $data_file
            //and change their name, too:

            $other_file_refs = FileRef::findBySql('file_id = :file_id AND id <> :source_id', [
                'file_id' => $source->file_id,
                'source_id' => $source->id
            ]);

            foreach ($other_file_refs as $other_file_ref) {
                $other_file_ref->name = $uploaded_file_data['name'];
                if ($other_file_ref->isDirty()) {
                    $other_file_ref->store();
                }
            }
        }

        // Update author
        FileRef::findEachBySQL(
            function ($ref) use ($user) {
                $ref->user_id = $user->id;
                $ref->store();
            },
            'file_id = :file_id',
            ['file_id' => $source->file_id]
         );

        //Everything went fine: Return the updated $source FileRef:
        return $source;
    }


    /**
     * This method handles editing file reference attributes.
     *
     * Checks that have to be made during the editing of a file reference are placed
     * in this method so that a controller can simply call this method
     * to change attributes of a file reference.
     *
     * At least one of the three parameters name, description and
     * content_terms_of_use_id must be set. Otherwise this method
     * will do nothing.
     *
     * @param FileRef $file_ref The file reference that shall be edited.
     * @param User $user The user who wishes to edit the file reference.
     * @param string|null $name The new name for the file reference
     * @param string|null $description The new description for the file reference.
     * @param string|null $content_terms_of_use_id The ID of the new ContentTermsOfUse object.
     * @param string|null $url The new URL for the file to link to.
     *     This is only regarded if the file_ref points to an URL instead
     *     of a file stored by Stud.IP.
     *
     * @return FileRef|string[] The edited FileRef object on success, string array with error messages on failure.
     */
    public static function editFileRef(
        FileRef $file_ref,
        User $user,
        $name = null,
        $description = null,
        $content_terms_of_use_id = null,
        $url = null
    )
    {
        if (!$name && !$description && !$content_terms_of_use_id) {
            //nothing to do, no errors:
            return $file_ref;
        }

        if (!$file_ref->folder) {
            return [_('Dateireferenz ist keinem Ordner zugeordnet!')];
        }

        $folder_type = $file_ref->folder->getTypedFolder();
        if (!$folder_type) {
            return [_('Ordnertyp konnte nicht ermittelt werden!')];
        }

        if (!$file_ref->getFileType()->isEditable($user->id)) {
            return [sprintf(
                _('Ungenügende Berechtigungen zum Bearbeiten der Datei %s!'),
                $file_ref->name
            )];
        }

        // check if name is set and is different from the current name
        // of the file reference:
        if ($name && $name !== $file_ref->name) {
            // name is special: we have to check if files/folders in
            // the file_ref's folder have the same name. If so, we must
            // make it unique.
            $folder = $file_ref->folder;

            if (!$folder) {
                return [sprintf(
                    _('Verzeichnis von Datei %s nicht gefunden!'),
                    $file_ref->name
                )];
            }

            $file_ref->name = $name;
        }

        if ($description !== null) {
            //description may be an empty string which is allowed here
            $file_ref->description = $description;
        }

        if ($content_terms_of_use_id !== null) {
            $content_terms_of_use = ContentTermsOfUse::find($content_terms_of_use_id);
            if (!$content_terms_of_use) {
                return [sprintf(
                    _('Inhalts-Nutzungsbedingungen mit ID %s nicht gefunden!'),
                    $content_terms_of_use_id
                )];
            }

            $file_ref->content_terms_of_use_id = $content_terms_of_use->id;
        }

        if ($file_ref->isLink() && $url !== null) {
            $file_ref->file->setURL($url);
            if ($file_ref->file->isDirty()) {
                $file_ref->file->store();
            }
            if ($file_ref->file->file_url->isDirty()) {
                $file_ref->file->file_url->store();
            }
        }

        if (!$file_ref->isDirty() || $file_ref->store()) {
            //everything went fine
            return $file_ref;
        }

        //error while saving the changes!
        return [sprintf(
            _('Fehler beim Speichern der Änderungen bei Datei %s'),
            $file_ref->name
        )];
    }

    /**
     * This method handles copying a file to a new folder.
     *
     * If the user (given by $user) is the owner of the file (by looking at the user_id
     * in the file reference) we can just make a new reference to that file.
     * Else, we must copy the file and its content.
     *
     * The file name is altered when a file with the identical name exists in
     * the destination folder. In that case, only the name in the FileRef object
     * of the file is altered and the File object's name is unchanged.
     *
     * @param FileType $source The file reference for the file that shall be copied.
     * @param FolderType $destination_folder The destination folder for the file.
     * @param User $user The user who wishes to copy the file.
     *
     * @return FileType|string[] The copied FileType object on success or an array with error messages on failure.
     */
    public static function copyFile(FileType $source, FolderType $destination_folder, User $user)
    {
        // first we have to make sure if the user has the permissions to read the source folder
        // and the permissions to write to the destination folder:

        $source_folder = $source->getFolderType();
        if (!$source_folder) {
            return [_('Ordnertyp des Quellordners konnte nicht ermittelt werden!')];
        }

        if (!$source_folder->isReadable($user->id) || !$destination_folder->isWritable($user->id)) {
            //the user is not permitted to read the source folder
            //or to write to the destination folder!
            return [
                sprintf(
                    _('Ungenügende Berechtigungen zum Kopieren der Datei %s in Ordner %s!'),
                    $source->getFilename(),
                    $destination_folder->name
                )
            ];
        }
        $error = $destination_folder->validateUpload($source, $user->id);
        if ($error && is_string($error)) {
            return [$error];
        }

        $newfile = $destination_folder->addFile($source);
        if (!$newfile) {
            return [_('Daten konnten nicht kopiert werden!')];
        }
        return $newfile;
    }

    /**
     * This method handles moving a file to a new folder.
     *
     * @param FileType $source The file reference for the file that shall be moved.
     * @param FolderType $destination_folder The destination folder.
     * @param User $user The user who wishes to move the file.
     *
     * @return FileRef|string[] $source FileRef object on success, Array with error messages on failure.
     */
    public static function moveFile(FileType $source, FolderType $destination_folder, User $user)
    {
        $source_folder = $source->getFolderType();
        if (!$source_folder) {
            return [_('Ordnertyp des Quellordners konnte nicht ermittelt werden!')];
        }

        if (!$source_folder->isReadable($user->id) || !$source->isWritable($user->id) || !$destination_folder->isWritable($user->id)) {
            //the user is not permitted to read the source folder
            //or to write to the destination folder!
            return [
                sprintf(
                    _('Ungenügende Berechtigungen zum Kopieren der Datei %s in Ordner %s!'),
                    $source->getFilename(),
                    $destination_folder->name
                )
            ];
        }

        $source_plugin = PluginManager::getInstance()->getPlugin($source_folder->range_id);
        if (!$source_plugin) {
            $source_plugin = PluginManager::getInstance()->getPlugin($source_folder->range_type);;
        }
        $destination_plugin = PluginManager::getInstance()->getPlugin($destination_folder->range_id);
        if (!$destination_plugin) {
            $destination_plugin = PluginManager::getInstance()->getPlugin($destination_folder->range_type);;
        }

        if (!$source_plugin && !$destination_plugin && $source instanceof StandardFile) {

            $error = $destination_folder->validateUpload($source, $user->id);
            if (!$error) {
                $source_fileref = FileRef::find($source->getId());
                $source_fileref->folder_id = $destination_folder->getId();
                if ($source_fileref->store()) {
                    $classname = get_class($source);
                    return new $classname($source_fileref);
                } else {
                    return [_('Datei konnte nicht gespeichert werden.')];
                }
            } else {
                return [$error];
            }

        } else {
            $copy = self::copyFile($source, $destination_folder, $user);
            if (!is_array($copy)) {
                $source_folder->deleteFile($source->getId());
            }
            return $copy;
        }

    }

    /**
     * This method handles deletign a file reference.
     *
     * @param FileRef $file_ref The file reference that shall be deleted
     * @param User $user The user who wishes to delete the file reference.
     *
     * @return FileRef|string[] The FileRef object that was deleted from the database on success
     * or an array with error messages on failure.
     */
    public static function deleteFileRef(FileRef $file_ref, User $user)
    {
        $folder_type = $file_ref->getFolderType();

        if (!$folder_type) {
            return [_('Ordnertyp des Quellordners konnte nicht ermittelt werden!')];
        }

        if (!$file_ref->getFileType()->isWritable($user->id)) {
            return [sprintf(
                _('Ungenügende Berechtigungen zum Löschen der Datei %s in Ordner %s!'),
                $file_ref->name
            )];
        }

        if ($file_ref->getFileType()->delete()) {
            return $file_ref;
        }

        return [_('Dateireferenz konnte nicht gelöscht werden.')];
    }

    /**
     * Handles the sub folder creation routine.
     *
     * @param FolderType $destination_folder The folder where the subfolder shall be linked.
     * @param User $user The user who wishes to create the subfolder.
     * @param string $folder_type_class_name The FolderType class name for the new folder
     * @param string $name The name for the new folder
     * @param string $description The description of the new folder
     *
     * @returns FolderType|string[] Either the FolderType object of the
     *     new folder or an Array with error messages.
     *
     */
    public static function createSubFolder(
        FolderType $destination_folder,
        User $user,
        $folder_type_class_name = null,
        $name = null,
        $description = null
    )
    {
        $errors = [];

        if (!$folder_type_class_name) {
            // folder_type_class_name is not set: we can't create a folder!
            return [_('Es wurde kein Ordnertyp angegeben!')];
        }

        // check if folder_type_class_name has a valid class:
        if (!is_subclass_of($folder_type_class_name, 'FolderType')) {
            return [sprintf(
                _('Die Klasse %s ist nicht von FolderType abgeleitet!'),
                $folder_type_class_name
            )];
        }

        if (!$name) {
            //name is not set: we can't create a folder!
            return [_('Es wurde kein Ordnername angegeben!')];
        }

        $sub_folder = new Folder();
        $sub_folder_type = new $folder_type_class_name($sub_folder);

        //set name and description of the new folder:
        $sub_folder->name = $name;
        if ($description) {
            $sub_folder->description = $description;
        }

        // check if the sub folder type is creatable in a StandardFolder,
        // if the destination folder is a StandardFolder:
        if (!in_array($folder_type_class_name, ['InboxFolder', 'OutboxFolder'])) {
            if (!$folder_type_class_name::availableInRange($destination_folder->range_id, $user->id))
            {
                $errors[] = sprintf(
                    _('Ein Ordner vom Typ %s kann nicht in einem Ordner vom Typ %s erzeugt werden!'),
                    get_class($sub_folder_type),
                    get_class($destination_folder)
                );
            }
        }

        if (!$destination_folder->isSubfolderAllowed($user->id)) {
            $errors[] = _('Sie sind nicht dazu berechtigt, einen Unterordner zu erstellen!');
        }

        // we can return here if we have found errors:
        if (!empty($errors)) {
            return $errors;
        }

        // check if all necessary attributes of the sub folder are set
        // and if they aren't set, set them here:

        // special case for inbox and outbox folders: these folder types
        // get a custom ID instead of a generic one, so it has to be set here!
        if ($folder_type_class_name === 'InboxFolder') {
            $sub_folder->id = md5('INBOX_' . $user->id);
        } elseif ($folder_type_class_name === 'OutboxFolder') {
            $sub_folder->id = md5('OUTBOX_' . $user->id);
        }

        $sub_folder->user_id     = $user->id;
        $sub_folder->range_id    = $destination_folder->range_id;
        $sub_folder->parent_id   = $destination_folder->getId();
        $sub_folder->range_type  = $destination_folder->range_type;
        $sub_folder->folder_type = get_class($sub_folder_type);
        $sub_folder->store();

        return $sub_folder_type; //no errors
    }

    /**
     * This method handles copying folders, including
     * copying the subfolders and files recursively.
     *
     * @param FolderType $source_folder The folder that shall be copied.
     * @param FolderType $destination_folder The destination folder.
     * @param User $user The user who wishes to copy the folder.
     *
     * @return FolderType|string[] The copy of the source_folder FolderType object on success
     * or an array with error messages on failure.
     */
    public static function copyFolder(FolderType $source_folder, FolderType $destination_folder, User $user)
    {
        $new_folder = null;

        if (!$destination_folder->isWritable($user->id)) {
            return [sprintf(
                _('Unzureichende Berechtigungen zum Kopieren von Ordner %s in Ordner %s!'),
                $source_folder->name,
                $destination_folder->name
            )];
        }

        //we have to check, if the source folder is a folder from a course.
        //If so, then only users with status dozent or tutor (or root) in that course
        //may copy the folder!
        if (!$source_folder->isReadable($user->id)) {
            return [sprintf(
                _('Unzureichende Berechtigungen zum Kopieren von Veranstaltungsordner %s in Ordner %s!'),
                $source_folder->name,
                $destination_folder->name
            )];
        }



        //The user has the permissions to copy the folder.

        //Now we must check if a folder is to be copied inside itself
        //or one of its subfolders. This is not allowed since it
        //leads to infinite recursion.

        $recursion_error = false;

        //First we check if the source folder is the destination folder:
        if ($destination_folder->getId() == $source_folder->getId()) {
            $recursion_error = true;
        }

        //After that we search the hierarchy of the destination folder
        //for the ID of the source folder:
        $parent = $destination_folder->getParent();
        while ($parent) {
            if ($parent->getId() == $source_folder->getId()) {
                $recursion_error = true;
                break;
            }
            $parent = $parent->getParent();
        }

        if ($recursion_error) {
            return [
                _('Ein Ordner kann nicht in sich selbst oder einen seiner Unterordner kopiert werden!')
            ];
        }


        //We must copy the source folder first.
        //The copy must be the same folder type like the destination folder.
        //Therefore we must first get the destination folder's FolderType class.
        $new_folder_class = get_class($source_folder);
        $destination_folder_type = in_array($new_folder_class, self::getAvailableFolderTypes($destination_folder->range_id, $user->id))
            ? $new_folder_class
            : "StandardFolder";
        $new_folder = new $destination_folder_type();
        $new_folder->name = $source_folder->name;
        $new_folder->user_id = $user->id;
        $new_folder->description = $source_folder->description;

        // Copy settings if applicable.
        foreach ($source_folder->copySettings() as $field => $content) {
            $new_folder->$field = $content;
        }

        $new_folder = $destination_folder->createSubfolder($new_folder);

        //now we go through all subfolders and copy them:
        foreach ($source_folder->getSubfolders() as $sub_folder) {
            $result = self::copyFolder($sub_folder, $new_folder, $user);
            if (!$result instanceof FolderType) {
                return $result;
            }
        }

        //now go through all files and copy them, too:
        foreach ($source_folder->getFiles() as $file) {
            $result = self::copyFile($file, $new_folder, $user);
            if (!$result instanceof FileType) {
                return $result;
            }
        }

        return $new_folder;
    }

    /**
     * This method handles moving folders, including
     * subfolders and files.
     *
     * @param FolderType $source_folder The folder that shall be moved.
     * @param FolderType $destination_folder The destination folder.
     * @param User $user The user who wishes to move the folder.
     *
     * @return FolderType|string[] The moved folder's FolderType object on success
     * or an array with error messages on failure.
     */
    public static function moveFolder(FolderType $source_folder, FolderType $destination_folder, User $user)
    {
        // Leave early, if folder was not actually moved
        if ($source_folder->parent_id === $destination_folder->id) {
            return $source_folder;
        }

        if (!$destination_folder->isWritable($user->id) || !$source_folder->isEditable($user->id)) {
            return [sprintf(
                _('Unzureichende Berechtigungen zum Verschieben von Ordner %s in Ordner %s!'),
                $source_folder->name,
                $destination_folder->name
            )];
        }

        //Check if the destination folder is a subfolder of the source folder
        //or if destination and source folder are identical:
        $recursion_error = false;

        if ($destination_folder->getId() == $source_folder->getId()) {
            $recursion_error = true;
        }

        $parent = $destination_folder->getParent();
        while ($parent) {
            if ($parent->getId() == $source_folder->getId()) {
                $recursion_error = true;
                break;
            }
            $parent = $parent->getParent();
        }

        if ($recursion_error) {
            return [
                _('Ein Ordner kann nicht in sich selbst oder einen seiner Unterordner verschoben werden!')
            ];
        }

        $new_folder = null;
        if ($source_folder instanceof StandardFolder) {
            //Standard folders just have to be put below the
            //destination folder.
            $new_folder = $destination_folder->createSubfolder($source_folder);

            $array_walker = function ($folder) use (&$array_walker, $destination_folder, $user) {
                $type = get_class($folder);
                if (!$type::availableInRange($destination_folder->range_id, $user->id)) {
                    $folder = new StandardFolder($folder);
                }
                $folder->range_id   = $destination_folder->range_id;
                $folder->range_type = $destination_folder->range_type;
                $folder->store();
                $sub_folders = $folder->getSubFolders();
                array_walk($sub_folders, $array_walker);

            };
            $sub_folders = $new_folder->getSubfolders();
            array_walk($sub_folders, $array_walker);
            return $new_folder;

        } else {
            //It is a plugin folder which needs special treatment.
            $new_folder = $destination_folder->createSubfolder(
                $source_folder
            );
        }
        if (!is_a($new_folder, "FolderType")) {
            return [_('Fehler beim Verschieben des Ordners.')];
        } else {
            //now we go through all subfolders and move them:
            foreach ($source_folder->getSubfolders() as $sub_folder) {
                $result = self::moveFolder($sub_folder, $new_folder, $user);
                if (!$result instanceof FolderType) {
                    //error
                    return $result;
                }
            }

            //now go through all files and move them, too:
            foreach ($source_folder->getFiles() as $file_ref) {
                if (!($file_ref instanceof FileRef)) {
                    $file_ref = FileRef::build((array) $file_ref, false);
                    $file_ref->setFolderType('foldertype', $source_folder);
                }
                $result = self::moveFile($file_ref->getFileType(), $new_folder, $user);
                if (!$result instanceof FileRef) {
                    //error
                    return $result;
                }
            }

            $source_folder->delete();
            return $new_folder;
        }
    }

    /**
     * This method helps with deleting a folder.
     *
     * @param FolderType $folder The folder that shall be deleted.
     * @param User $user The user who wishes to delete the folder.
     *
     * @return FolderType|string[] The deleted folder's FolderType object on success
     * or an array with error messages on failure.
     */
    public static function deleteFolder(FolderType $folder, User $user)
    {
        if (!$folder->isEditable($user->id)) {
            return [sprintf(
                    _('Unzureichende Berechtigungen zum Löschen von Ordner %s!'),
                    $folder->name
                )
            ];
        }

        if ($folder->delete()) {
            //everything went fine!
            return $folder;
        }

        //error occured!
        return [sprintf(
            _('Fehler beim Löschvorgang von Ordner %s!'),
            $folder->name
        )];
    }


    /**
     * returns the available folder types,
     * There are several types of folders in Stud.IP. This method returns
     * all available folder types.
     *
     * @return array with strings representing the class names of available folder types.
     *
     */
    public static function getFolderTypes()
    {
        foreach (scandir(__DIR__) as $filename) {
            $path = pathinfo($filename);
            if ($path['extension'] === 'php') {
                class_exists($path['filename']);
            }
        }
        $result = [];
        foreach (get_declared_classes() as $declared_class) {
            if (!is_a($declared_class, 'FolderType', true)) {
                continue;
            }
            $reflected_ft = new ReflectionClass($declared_class);
            $ft_sorter = $reflected_ft->getStaticPropertyValue('sorter', PHP_INT_MAX);
            if ($ft_sorter == 0 && $declared_class != 'StandardFolder') {
                $ft_sorter = PHP_INT_MAX;
            }
            $result[$declared_class] = $ft_sorter;
        }
        asort($result, SORT_NUMERIC);
        return array_keys($result);
    }

    /**
     * returns the available folder types, for given context and user
     *
     * @param string|SimpleORMap $range_id_or_object
     * @param string $user_id
     * @return array with strings representing the class names of available folder types.
     *
     */
    public static function getAvailableFolderTypes($range_id_or_object, $user_id)
    {
        $result = [];
        foreach (self::getFolderTypes() as $type) {
            if ($type::availableInRange($range_id_or_object, $user_id)) {
                $result[] = $type;
            }
        }
        return $result;
    }

    /**
     * Copies the content of a folder (files and subfolders) into a given
     * path in the operating system's file system.
     *
     * @param FolderType $folder The folder whose content shall be copied.
     * @param string $path The path in the operating system's file system
     *     where the content shall be copied into.
     * @param string $user_id The user who wishes to copy the content.
     * @param string $min_perms If set, the selection of subfolders and files
     *     is limited to those which are visible for users having
     *     the minimum permissions.
     * @param bool $ignore_perms If set to true, files are copied without checking
     *     the minimum permissions or the permissions of the user given by user_id.
     * @return bool True on success, false on error.
     */
    public static function copyFolderContentIntoPath(
        FolderType $folder,
        $path = null,
        $user_id = 'nobody',
        $min_perms = 'nobody',
        $ignore_perms = false
    )
    {
        if (!$path) {
            return false;
        }

        // loop through all subfolders, create a directory for each subfolder
        // and call this method recursively:
        foreach ($folder->getSubfolders() as $subfolder) {
            if ($subfolder->isReadable($user_id) || $ignore_perms)
            {
                //User has permissions to read the folder or permission checks
                //are ignored.

                $subfolder_path = $path . '/' . $subfolder->name;
                mkdir($subfolder_path, 0700);
                $success = self::copyFolderContentIntoPath(
                    $subfolder,
                    $subfolder_path,
                    $user_id,
                    $min_perms
                );

                if (!$success) {
                    return false;
                }
            }
        }

        // loop through all files and copy them to the folder path:
        foreach ($folder->getFiles() as $file_ref) {
            if ($folder->isFileDownloadable($file_ref, $user_id) || $ignore_perms) {
                //The user (given by user_id) has the required permissions
                //to download the file or the permission checks are
                //ignored.

                $file_path = $path . '/' . $file_ref->name;
                $success = copy($file_ref->file->getPath(), $file_path);
                if (!$success) {
                    return false;
                }
            }
        }

        //Everything went fine.
        return true;
    }

    /**
     * Counts the number of files inside a folder and its subfolders.
     * The search result can be limited to the files belonging to one user
     * and/or to the files which are readable for one user.
     *
     * @param FolderType $folder The folder whose files shall be counted.
     * @param bool $count_subfolders True, if files subfolders shall also
     *     be counted, too (default). False otherwise.
     * @param string $owner_id Optional user-ID to count only files of one
     *     user specified by the ID.
     * @param string $user_id Optional user-ID to count only files the user
     *     (specified by this user-ID) can read.
     *
     * @return int The amount of files inside the folder (and its subfolders).
     */
    public static function countFilesInFolder(
        FolderType $folder,
        $count_subfolders = true,
        $owner_id = null,
        $user_id = null
    )
    {
        $num_files = 0;

        if ($owner_id === null) {
            //If the owner_id is not set we can simply count the number of all files.
            $folder_files = $folder->getFiles();
            if ($user_id === null) {
                //If the user_id is also not set we can simply count all files in this folder:
                $num_files = count($folder_files);
            } else {
                //$user_id is set: We must check for each file if it is readable for the user
                //specified by $user_id:
                if ($folder->isReadable($user_id)) {
                    foreach ($folder_files as $folder_file) {
                        if ($folder->isFileDownloadable($folder_file->id, $user_id)) {
                            $num_files++;
                        }
                    }
                }
            }
        } else {
            //If the owner_id is set we must check who owns the file
            //and count only those files whose user_id matches the owner_id specified.
            foreach ($folder->getFiles() as $file) {
                if ($file->user_id === $owner_id) {
                    if ($user_id) {
                        //user-ID is set: only if the file is downloadable
                        //it will be counted!
                        if ($folder->isFileDownloadable($file->id, $user_id)) {
                            $num_files++;
                        }
                    } else {
                        //No user-ID set: we can count the file
                        $num_files++;
                    }
                }
            }
        }

        if ($count_subfolders) {
            //If files in subfolders shall be counted too,
            //we must call this method recursively.
            foreach ($folder->getSubFolders() as $subfolder) {
                $num_files += self::countFilesInFolder(
                    $subfolder,
                    $count_subfolders,
                    $owner_id,
                    $user_id
                );
            }
        }

        return $num_files;
    }



    /**
     * Creates a list of files and subfolders of a folder.
     *
     * @param FolderType $top_folder The folder whose content shall be retrieved.
     * @param string $user_id The ID of the user who wishes to get all
     *     files and subfolders of a folder.
     * @param bool $check_file_permissions Set to true, if file permissions
     *     shall be checked. Defaults to false.
     * @return mixed[] A mixed array with FolderType and FileType objects.
     */
    public static function getFolderFilesRecursive(
        FolderType $top_folder,
        $user_id,
        $check_file_permissions = false
    )
    {
        $files = [];
        $folders = [];
        $array_walker = function ($top_folder) use (
            &$array_walker, &$folders, &$files, $user_id, $check_file_permissions
        ) {
            if ($top_folder->isVisible($user_id)) {
                $folders[$top_folder->getId()] = $top_folder;
                if ($top_folder->isReadable($user_id)) {

                    if ($check_file_permissions) {
                        //We must check for each file if it is downloadable for the user
                        //specified by user_id:
                        $top_folder_file_refs = $top_folder->getFiles();
                        foreach ($top_folder_file_refs as $file_ref) {
                            if ($top_folder->isFileDownloadable($file_ref->id, $user_id)) {
                                $files[] = $file_ref;
                            }
                        }
                    } else {
                        $files = array_merge($files, $top_folder->getFiles());
                    }
                    array_walk($top_folder->getSubFolders(), $array_walker);
                }
            }
        };

        $top_folders = [$top_folder];
        array_walk($top_folders, $array_walker);
        return compact('files', 'folders');
    }

    /**
     * Creates a list of readable subfolders of a folder.
     *
     * @param FolderType $top_folder
     * @param string $user_id
     * @return FolderType[] assoc array ID => FolderType
     */
    public static function getReadableFolders(FolderType $top_folder, $user_id)
    {
        $folders = [];
        $array_walker = function ($top_folder) use (&$array_walker, &$folders,$user_id) {
            if ($top_folder->isReadable($user_id)) {
                $folders[$top_folder->getId()] = $top_folder;
                array_walk($top_folder->getSubFolders(), $array_walker);
            }
        };

        $top_folders = [$top_folder];
        array_walk($top_folders, $array_walker);
        return $folders;
    }

    /**
     * Creates a list of unreadable subfolders of a folder.
     * @deprecated use getReadableFolders() instead
     *
     * @param FolderType $top_folder
     * @param string $user_id
     * @return FolderType[] assoc array ID => FolderType
     */
    public static function getUnreadableFolders(FolderType $top_folder, $user_id)
    {
        $folders = [];
        $array_walker = function ($top_folder) use (&$array_walker, &$folders,$user_id) {
            if (!$top_folder->isReadable($user_id)) {
                $folders[$top_folder->getId()] = $top_folder;
            }
            array_walk($top_folder->getSubFolders(), $array_walker);
        };

        $top_folders = [$top_folder];
        array_walk($top_folders, $array_walker);
        return $folders;
    }

    /**
     * Returns a FolderType instance for a given folder-ID.
     * This method can also get FolderType instances which are defined
     * in a file system plugin.
     *
     * @param string $id The ID of a Folder object.
     * @param null $pluginclass The name of a Plugin's main class.
     * @return FolderType|null A FolderType object if it can be retrieved
     *     using the Folder-ID (and by option the plugin class name)
     *     or null in case no FolderType object can be created.
     */
    public static function getTypedFolder($id, $pluginclass = null)
    {
        if ($pluginclass === null) {
            $folder = Folder::find($id);
            if ($folder) {
                return $folder->getTypedFolder();
            }
        } else {
            $plugin = PluginManager::getInstance()->getPlugin($pluginclass);
            if ($plugin instanceof FilesystemPlugin) {
                $folder = $plugin->getFolder($id);
                if ($folder instanceof FolderType) {
                    return $folder;
                }
            }
        }
        return null;
    }

    /**
     * Retrieves additional data for an URL by looking at the HTTP header.
     *
     * @param string $url The URL from which additional data shall be fetched.
     * @param int $level The amount of redirects that have already been walked through.
     *     The $level parameter is only useful when this method calls itself recursively.
     *
     * @return array An array with additional data retrieved from the HTTP header.
     */
    public static function fetchURLMetadata($url, $level = 0)
    {
        if ($level > 5) {
            return ['response' => 'HTTP/1.0 400 Bad Request', 'response_code' => 400];
        }

        $url_parts = @parse_url($url);
        // filter out localhost and reserved or private IPs
        if (mb_stripos($url_parts['host'], 'localhost') !== false
            || mb_stripos($url_parts['host'], 'loopback') !== false
            || (filter_var($url_parts['host'], FILTER_VALIDATE_IP) !== false
                && (mb_strpos($url_parts['host'], '127') === 0
                    || filter_var(
                        $url_parts['host'],
                        FILTER_VALIDATE_IP,
                        FILTER_FLAG_IPV4
                        | FILTER_FLAG_NO_PRIV_RANGE
                        | FILTER_FLAG_NO_RES_RANGE
                    ) === false)
               )
        ) {
            return ['response' => 'HTTP/1.0 400 Bad Request', 'response_code' => 400];
        }

        // URL links to an ftp server
        if ($url_parts['scheme'] === 'ftp') {
            if (preg_match('/[^a-z0-9_.-]/i', $url_parts['host'])) { // exists umlauts ?
                $IDN = new Algo26\IdnaConvert\ToIdn();
                $out = $IDN->convert($url_parts['host']); // false by error
                $url_parts['host'] = $out ?: $url_parts['host'];
            }

            $ftp = @ftp_connect($url_parts['host'],$url_parts['port'] ?: 21, 10);
            if (!$ftp) {
                return ['response' => 'HTTP/1.0 502 Bad Gateway', 'response_code' => 502];
            }
            if (!$url_parts['user']) {
                $url_parts['user'] = 'anonymous';
            }
            if (!$url_parts['pass']) {
                $mailclass = new StudipMail();
                $url_parts['pass'] = $mailclass->getSenderEmail();
            }
            if (!@ftp_login($ftp, $url_parts["user"], $url_parts["pass"])) {
                ftp_quit($ftp);
                return ['response' => 'HTTP/1.0 403 Forbidden', 'response_code' => 403];
            }
            $parsed_link['Content-Length'] = ftp_size($ftp, $url_parts['path']);
            ftp_quit($ftp);
            if ($parsed_link['Content-Length'] != -1) {
                $parsed_link['HTTP/1.0 200 OK'] = 'HTTP/1.0 200 OK';
                $parsed_link['response_code'] = 200;
            } else {
                return ['response' => 'HTTP/1.0 404 Not Found', 'response_code' => 404];
            }
            $parsed_link['filename']     = basename($url_parts['path']);
            $parsed_link['Content-Type'] = get_mime_type($parsed_link['filename']);
            return $parsed_link;
        }

        // "Normal" url
        if (!empty($url_parts['path'])) {
            $documentpath = $url_parts['path'];
        } else {
            $documentpath = '/';
        }
        if (!empty($url_parts['query'])) {
            $documentpath .= '?' . $url_parts['query'];
        }
        $host = $url_parts['host'];
        $port = $url_parts['port'];
        $scheme = mb_strtolower($url_parts['scheme']);
        if (!in_array($scheme, ['http', 'https']) || !$host) {
            return ['response' => 'HTTP/1.0 400 Bad Request', 'response_code' => 400];
        }
        if ($scheme === 'https') {
            $ssl = true;
            if (empty($port)) {
                $port = 443;
            }
        } else {
            $ssl = false;
        }
        if (empty($port)) {
            $port = 80;
        }
        if (preg_match('/[^a-z0-9_.-]/i', $host)) { // exists umlauts ?
            $IDN = new Algo26\IdnaConvert\ToIdn();
            $out = $IDN->convert($host); // false by error
            $host = $out ?: $host;
        }
        if (Config::get()->HTTP_PROXY) {
            $proxy_context = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false,]]);
            $socket = @stream_socket_client('tcp://' . Config::get()->HTTP_PROXY, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $proxy_context);
            if ($ssl) {
                if ($socket) {
                    fputs($socket, 'CONNECT ' . $host . ':' . $port . " HTTP/1.0\r\nHost: $host\r\nUser-Agent: Stud.IP\r\n\r\n");
                    while (true) {
                        $s = rtrim(fgets($socket, 4096));
                        if (preg_match('/^$/', $s)) {
                            break;
                        }
                    }
                    stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
                }
            }
        } else {
            $errno = $errstr = '';
            $socket = @stream_socket_client(($ssl ? 'ssl://' : 'tcp://') . $host . ':' . $port, $errno, $errstr, 5, STREAM_CLIENT_CONNECT);
        }
        if (!$socket) {
            Log::error(__METHOD__ . ' - stream_socket_client(' . ($ssl ? 'ssl://' : 'tcp://') . $host . ':' . $port .') failed: ' . $errstr);
            return ['response' => 'HTTP/1.0 502 Bad Gateway', 'response_code' => 502];
        }

        $urlString = "GET {$documentpath} HTTP/1.0\r\nHost: {$host}\r\n";
        if ($url_parts['user'] && $url_parts['pass']) {
            $pass = $url_parts['pass'];
            $user = $url_parts['user'];
            $urlString .= "Authorization: Basic " . base64_encode("{$user}:{$pass}") . "\r\n";
        }
        $urlString .= sprintf("User-Agent: Stud.IP v%s File Crawler\r\n", $GLOBALS['SOFTWARE_VERSION']);
        $urlString .= "Connection: close\r\n\r\n";
        fputs($socket, $urlString);
        stream_set_timeout($socket, 5);
        $response = '';
        do {
            $response .= fgets($socket, 128);
            $info = stream_get_meta_data($socket);
        } while (!feof($socket) && !$info['timed_out'] && mb_strlen($response) < 1024);
        fclose($socket);

        $raw_header = explode("\n", trim($response));
        if (!preg_match("~^HTTP/[^\s]*\s(.*?)\s~", $raw_header[0], $status)) {
            return ['response' => 'HTTP/1.0 502 Bad Gateway', 'response_code' => 502];
        }

        $header = [
            'response_code' => (int)$status[1],
            'response'      => trim($raw_header[0]),
        ];

        for ($i = 0; $i < count($raw_header); $i += 1) {
            $parts = null;
            if (!trim($raw_header[$i])) {
                break;
            }
            $matches = preg_match('/^\S+:/', $raw_header[$i], $parts);
            if ($matches){
                $key   = trim(mb_substr($parts[0],0,-1));
                $value = trim(mb_substr($raw_header[$i], mb_strlen($parts[0])));
                $header[$key] = $value;
            } else {
                $header[trim($raw_header[$i])] = trim($raw_header[$i]);
            }
        }

        // Anderer Dateiname?
        $disposition_header = $header['Content-Disposition'] ?: $header['content-disposition'];
        if ($disposition_header) {
            $header_parts = explode(';', $disposition_header);
            foreach ($header_parts as $part) {
                $part = trim($part);
                [$key, $value] = explode('=', $part, 2);
                if (mb_strtolower($key) === 'filename') {
                    $header['filename'] = trim($value, '"');
                }
            }
        } else {
            $header['filename'] = basename($url_parts['path']);
        }

        // Weg über einen Locationheader:
        $location_header = $header['Location'] ?: $header['location'];
        if (in_array($header['response_code'], [300, 301, 302, 303, 305, 307]) && $location_header) {
            if (mb_strpos($location_header, 'http') !== 0) {
                $location_header = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $location_header;
            }
            $header = self::fetchURLMetadata($location_header, $level + 1);
        }
        return $header;
    }

    /**
     * Returns an INBOX folder for the given user.
     *
     * @param User $user The user whose inbox folder is requested.
     * @return FolderType|null Returns the inbox folder on success, null on failure.
     */
    public static function getInboxFolder(User $user)
    {
        $top_folder = Folder::findTopFolder($user->id, 'user');
        if (!$top_folder) {
            return null;
        }

        $top_folder = $top_folder->getTypedFolder();
        if (!$top_folder) {
            return null;
        }

        $inbox_folder = Folder::find(md5('INBOX_' . $user->id));

        if (!$inbox_folder) {
            //inbox folder doesn't exist: create it, if necessary.
            //We need an inbox folder if there is at least one received
            //message with at least one attachment.

            $inbox_folder = FileManager::createSubFolder(
                $top_folder,
                $user,
                'InboxFolder',
                'Inbox',
                InboxFolder::getTypeName()
            );

            if ($inbox_folder instanceof InboxFolder) {
                return $inbox_folder;
            }

            return null;
        }

        return $inbox_folder->getTypedFolder();
    }

    /**
     * Returns a FolderType object for the outbox folder of the given user.
     *
     * @param User $user The user whose outbox folder is requested.
     * @return FolderType|null Returns the inbox folder on success, null on failure.
     */
    public static function getOutboxFolder(User $user)
    {
        $top_folder = Folder::findTopFolder($user->id, 'user');
        if (!$top_folder) {
            return null;
        }

        $top_folder = $top_folder->getTypedFolder();
        if (!$top_folder) {
            return null;
        }

        $outbox_folder = Folder::find(md5('OUTBOX_' . $user->id));

        if (!$outbox_folder) {
            //inbox folder doesn't exist: create it, if necessary.
            //We need an inbox folder if there is at least one received
            //message with at least one attachment.

            $outbox_folder = FileManager::createSubFolder(
                $top_folder,
                $user,
                'OutboxFolder',
                'Outbox',
                OutboxFolder::getTypeName()
            );

            if ($outbox_folder instanceof OutboxFolder) {
                return $outbox_folder;
            }

            return null;
        }

        return $outbox_folder->getTypedFolder();
    }

    /**
     * returns config array for upload types and sizes for a given range id
     *
     * @param string      $range_id id of Course Institute User
     * @param string|null $user_id  Optional user id
     * @return array
     */
    public static function getUploadTypeConfig($range_id, $user_id = null)
    {
        if (is_null($user_id)) {
            $user_id = $GLOBALS['user']->id;
        }

        $status = $GLOBALS['perm']->get_perm($user_id);
        $active_upload_type = 'default';

        $range_object = get_object_by_range_id($range_id);
        if ($range_object instanceof Course) {
            $status = $GLOBALS['perm']->get_studip_perm($range_id, $user_id);
            $active_upload_type = $range_object->status;
        } elseif ($range_object instanceof Institute) {
            $status = $GLOBALS['perm']->get_studip_perm($range_id, $user_id);
            $active_upload_type = 'institute';
        } elseif ($range_object instanceof User) {
            $active_upload_type = 'personalfiles';
        } elseif (Message::exists($range_id)) {
            $active_upload_type = 'attachments';
        }

        return self::loadUploadTypeConfig($active_upload_type, $status);
    }

    /**
     * Loads the upload type configuration for a specific type and status.
     *
     * @param string $type
     * @param string $status
     *
     * @return array{type: string, file_types: array, file_size: int}
     */
    public static function loadUploadTypeConfig(string $type, string $status): array
    {
        if (!isset($GLOBALS['UPLOAD_TYPES'][$type])) {
            $type = 'default';
        }

        $upload_type = $GLOBALS['UPLOAD_TYPES'][$type];
        return [
            'type'       => $upload_type['type'],
            'file_types' => $upload_type['file_types'],
            'file_size'  => $upload_type['file_sizes'][$status],
        ];
    }

    /**
     * Create URL to a folder
     * @param FolderType $folder  the folder
     * @param bool $include_root
     * @return array array of FolderType
     */
    public static function getFullPath(FolderType $folder, $include_root = true)
    {
        $path = [$folder->getId() => $folder];
        $current = $folder;
        while ($current = $current->getParent()) {
            if ($current instanceof RootFolder && !$include_root) continue;
            $path[$current->getId()] = $current;
        }
        return array_reverse($path, true);
    }

    /**
     * Create URL to a folder
     * @param FolderType|Folder $folder  the folder
     * @return string URL to the folder's range
     */
    public static function getFolderURL($folder)
    {
        if (!$folder->range_type) {
            return null;
        }

        switch ($folder->range_type) {
            case 'course':
                $url = URLHelper::getURL(
                    'dispatch.php/course/files/index/'.$folder->id,
                    ['cid' => $folder->range_id]
                );
                break;

            case 'institute':
                $url = URLHelper::getURL(
                    'dispatch.php/institute/files/index/'.$folder->id,
                    ['cid' => $folder->range_id]
                );
                break;

            case 'message':
                $url = URLHelper::getURL('dispatch.php/messages/overview/'.$folder->range_id,
                    ['cid' => null]);
                break;

            case 'user':
                $url = URLHelper::getURL('dispatch.php/files/index/'.$folder->id,
                    ['cid' => null]);
                break;

            default:
                $url = URLHelper::getURL(
                    'dispatch.php/files/system/'.$folder->range_type.'/'.$folder->id,
                    ['cid' => null]
                );
        }
        return $url;
    }

    /**
     * Create link to a folder
     * @param FolderType|Folder $folder  the folder
     * @return string link to the folder's range
     */
    public static function getFolderLink($folder)
    {
        return htmlReady(self::getFolderURL($folder));
    }


    /**
     * Returns true if the mime-type of that FileType object starts with image/
     * @param FileType $file The file
     * @return bool True if it is an image else false
     */
    public static function fileIsImage(FileType $file)
    {
        $mimetype = $file->getMimeType();
        return mb_substr($mimetype, 0, 6) === "image/";
    }


    /**
     * Returns true if the mime-type of that FileType object starts with audio/
     * @param FileType $file The file
     * @return bool True if it is an audio file else false
     */
    public static function fileIsAudio(FileType $file)
    {
        $mimetype = $file->getMimeType();
        return mb_substr($mimetype, 0, 6) === "audio/";
    }


    /**
     * Returns true if the mime-type of that FileType object starts with video/
     * @param FileType $file The file
     * @return bool True if it is an video file else false
     */
    public static function fileIsVideo(FileType $file)
    {
        $mimetype = $file->getMimeType();
        return mb_substr($mimetype, 0, 6) === "video/";
    }


    /**
     * Retrieves the range-IDs for all courses and institutes a user has
     * access to.
     *
     * @param string $user_id The ID of the user.
     *
     * @param bool $with_personal_file_area Whether to include the user-ID
     *     of the user in the list of range-IDs (true) or not (false).
     *     Defaults to false.
     *
     * @returns string[] An array with all retrieved range-IDs of the user.
     */
    public static function getRangeIdsForFolders($user_id = null, $with_personal_file_area = true)
    {
        if (!$user_id) {
            return [];
        }

        //Get all courses first:
        $courses = Course::findByUser($user_id);
        //Get all institutes:
        $institutes = Institute::getMyInstitutes($user_id);

        //After that, collect the range-IDs:
        $range_ids = [];
        foreach ($courses as $course) {
            $range_ids[] = $course->id;
        }
        foreach ($institutes as $institute) {
            $range_ids[] = $institute->id;
        }

        if ($with_personal_file_area) {
            //Add the personal file area, too:
            $range_ids[] = $GLOBALS['user']->id;
        }
        return $range_ids;
    }

    public static function getFileIcon($filename, $role = Icon::ROLE_CLICKABLE) {
        $filename = mb_strtolower($filename);
        $extension = (mb_strrpos($filename, ".") === false)
            ? $filename
            : substr($filename, mb_strrpos($filename, ".") + 1);
        $extension = strtolower($extension);
        switch ($extension){
            case 'rtf':
            case 'doc':
            case 'docx':
            case 'odt':
                $icon = 'file-text';
                break;
            case 'xls':
            case 'xlsx':
            case 'ods':
            case 'csv':
            case 'ppt':
            case 'pptx':
            case 'odp':
                $icon = 'file-office';
                break;
            case 'zip':
            case 'tgz':
            case 'gz':
            case 'bz2':
                $icon = 'file-archive';
                break;
            case 'pdf':
                $icon = 'file-pdf';
                break;
            case 'gif':
            case 'jpg':
            case 'jpe':
            case 'jpeg':
            case 'png':
            case 'bmp':
                $icon = 'file-pic';
                break;
            default:
                $icon = 'file-generic';
                break;
        }
        return Icon::create($icon, $role);
    }
}