aboutsummaryrefslogtreecommitdiff
path: root/lib/filesystem/FileManager.php
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:07:19 +0200
committerJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:19:12 +0200
commita3da1483a9e689846179159355badfec8073dbec (patch)
tree770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /lib/filesystem/FileManager.php
current code from svn, revision 62608
Diffstat (limited to 'lib/filesystem/FileManager.php')
-rw-r--r--lib/filesystem/FileManager.php1958
1 files changed, 1958 insertions, 0 deletions
diff --git a/lib/filesystem/FileManager.php b/lib/filesystem/FileManager.php
new file mode 100644
index 0000000..22bfe42
--- /dev/null
+++ b/lib/filesystem/FileManager.php
@@ -0,0 +1,1958 @@
+<?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';
+ }
+
+ list($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.
+ */
+ 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 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 folder the folder where the files are inserted
+ * @param user_id the ID of the user who wants to upload files
+ *
+ * @return mixed[] 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'])) {
+ $error = [];
+ 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'));
+ }
+
+ //FILEREF METHODS
+
+ /**
+ * 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:
+
+ $data_file = null;
+
+ 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.')];
+ }
+
+ // FOLDER METHODS
+
+ /**
+ * 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::moveFileRef($file_ref, $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()
+ {
+ $result = [];
+ 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 $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_TLS_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);
+ list($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
+ *
+ * @param $range_id string id of Course Institute User
+ * @param null $user_id string
+ * @return array
+ */
+ public static function getUploadTypeConfig($range_id, $user_id = null)
+ {
+ if (is_null($user_id)) {
+ $user_id = $GLOBALS['user']->id;
+ }
+ $range_object = get_object_by_range_id($range_id);
+ $active_upload_type = null;
+ 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';
+ } else {
+ $status = $GLOBALS['perm']->get_perm($user_id);
+ $active_upload_type = "personalfiles";
+ }
+
+ if (!isset($GLOBALS['UPLOAD_TYPES'][$active_upload_type])) {
+ $active_upload_type = 'default';
+ }
+
+ $upload_type = $GLOBALS['UPLOAD_TYPES'][$active_upload_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);
+ //Icon auswaehlen
+ 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);
+ }
+}