Skip to content
Snippets Groups Projects
FileManager.php 70.9 KiB
Newer Older
<?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';
        }

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        [$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;
        }