Пример #1
0
 /**
  * Validate incoming data.
  *
  * @param array $data
  * @param array $files
  * @return array
  */
 public function validation($data, $files)
 {
     $errors = array();
     $draftitemid = $data['files_filemanager'];
     if (file_is_draft_area_limit_reached($draftitemid, $this->_customdata['options']['areamaxbytes'])) {
         $errors['files_filemanager'] = get_string('userquotalimit', 'error');
     }
     return $errors;
 }
Пример #2
0
 function validation($data, $files)
 {
     $errors = [];
     $draftitemid = $data['files_filemanager'];
     if (file_is_draft_area_limit_reached($draftitemid, $this->_customdata['options']['areamaxbytes'])) {
         $errors['files_filemanager'] = get_string('userquotalimit', 'error');
     }
     if (!trim($data['cliptitle'])) {
         $errors['cliptitle'] = get_string('video_title_mandatory', 'opencast');
     }
     return $errors;
 }
Пример #3
0
 /**
  * Do the actual processing of the uploaded file
  * @param string $saveas_filename name to give to the file
  * @param int $maxbytes maximum file size
  * @param mixed $types optional array of file extensions that are allowed or '*' for all
  * @param string $savepath optional path to save the file to
  * @param int $itemid optional the ID for this item within the file area
  * @param string $license optional the license to use for this file
  * @param string $author optional the name of the author of this file
  * @param bool $overwriteexisting optional user has asked to overwrite the existing file
  * @param int $areamaxbytes maximum size of the file area.
  * @return object containing details of the file uploaded
  */
 public function process_upload($saveas_filename, $maxbytes, $types = '*', $savepath = '/', $itemid = 0, $license = null, $author = '', $overwriteexisting = false, $areamaxbytes = FILE_AREA_MAX_BYTES_UNLIMITED)
 {
     global $USER, $CFG;
     if (is_array($types) and in_array('*', $types) or $types == '*') {
         $this->mimetypes = '*';
     } else {
         foreach ($types as $type) {
             $this->mimetypes[] = mimeinfo('type', $type);
         }
     }
     if ($license == null) {
         $license = $CFG->sitedefaultlicense;
     }
     $record = new stdClass();
     $record->filearea = 'draft';
     $record->component = 'user';
     $record->filepath = $savepath;
     $record->itemid = $itemid;
     $record->license = $license;
     $record->author = $author;
     $context = context_user::instance($USER->id);
     $elname = 'repo_upload_file';
     $fs = get_file_storage();
     $sm = get_string_manager();
     if ($record->filepath !== '/') {
         $record->filepath = file_correct_filepath($record->filepath);
     }
     if (!isset($_FILES[$elname])) {
         throw new moodle_exception('nofile');
     }
     if (!empty($_FILES[$elname]['error'])) {
         switch ($_FILES[$elname]['error']) {
             case UPLOAD_ERR_INI_SIZE:
                 throw new moodle_exception('upload_error_ini_size', 'repository_upload');
                 break;
             case UPLOAD_ERR_FORM_SIZE:
                 throw new moodle_exception('upload_error_form_size', 'repository_upload');
                 break;
             case UPLOAD_ERR_PARTIAL:
                 throw new moodle_exception('upload_error_partial', 'repository_upload');
                 break;
             case UPLOAD_ERR_NO_FILE:
                 throw new moodle_exception('upload_error_no_file', 'repository_upload');
                 break;
             case UPLOAD_ERR_NO_TMP_DIR:
                 throw new moodle_exception('upload_error_no_tmp_dir', 'repository_upload');
                 break;
             case UPLOAD_ERR_CANT_WRITE:
                 throw new moodle_exception('upload_error_cant_write', 'repository_upload');
                 break;
             case UPLOAD_ERR_EXTENSION:
                 throw new moodle_exception('upload_error_extension', 'repository_upload');
                 break;
             default:
                 throw new moodle_exception('nofile');
         }
     }
     \core\antivirus\manager::scan_file($_FILES[$elname]['tmp_name'], $_FILES[$elname]['name'], true);
     // {@link repository::build_source_field()}
     $sourcefield = $this->get_file_source_info($_FILES[$elname]['name']);
     $record->source = self::build_source_field($sourcefield);
     if (empty($saveas_filename)) {
         $record->filename = clean_param($_FILES[$elname]['name'], PARAM_FILE);
     } else {
         $ext = '';
         $match = array();
         $filename = clean_param($_FILES[$elname]['name'], PARAM_FILE);
         if (strpos($filename, '.') === false) {
             // File has no extension at all - do not add a dot.
             $record->filename = $saveas_filename;
         } else {
             if (preg_match('/\\.([a-z0-9]+)$/i', $filename, $match)) {
                 if (isset($match[1])) {
                     $ext = $match[1];
                 }
             }
             $ext = !empty($ext) ? $ext : '';
             if (preg_match('#\\.(' . $ext . ')$#i', $saveas_filename)) {
                 // saveas filename contains file extension already
                 $record->filename = $saveas_filename;
             } else {
                 $record->filename = $saveas_filename . '.' . $ext;
             }
         }
     }
     // Check the file has some non-null contents - usually an indication that a user has
     // tried to upload a folder by mistake
     if (!$this->check_valid_contents($_FILES[$elname]['tmp_name'])) {
         throw new moodle_exception('upload_error_invalid_file', 'repository_upload', '', $record->filename);
     }
     if ($this->mimetypes != '*') {
         // check filetype
         $filemimetype = file_storage::mimetype($_FILES[$elname]['tmp_name'], $record->filename);
         if (!in_array($filemimetype, $this->mimetypes)) {
             throw new moodle_exception('invalidfiletype', 'repository', '', get_mimetype_description(array('filename' => $_FILES[$elname]['name'])));
         }
     }
     if (empty($record->itemid)) {
         $record->itemid = 0;
     }
     if ($maxbytes !== -1 && filesize($_FILES[$elname]['tmp_name']) > $maxbytes) {
         $maxbytesdisplay = display_size($maxbytes);
         throw new file_exception('maxbytesfile', (object) array('file' => $record->filename, 'size' => $maxbytesdisplay));
     }
     if (file_is_draft_area_limit_reached($record->itemid, $areamaxbytes, filesize($_FILES[$elname]['tmp_name']))) {
         throw new file_exception('maxareabytes');
     }
     $record->contextid = $context->id;
     $record->userid = $USER->id;
     if (repository::draftfile_exists($record->itemid, $record->filepath, $record->filename)) {
         $existingfilename = $record->filename;
         $unused_filename = repository::get_unused_filename($record->itemid, $record->filepath, $record->filename);
         $record->filename = $unused_filename;
         $stored_file = $fs->create_file_from_pathname($record, $_FILES[$elname]['tmp_name']);
         if ($overwriteexisting) {
             repository::overwrite_existing_draftfile($record->itemid, $record->filepath, $existingfilename, $record->filepath, $record->filename);
             $record->filename = $existingfilename;
         } else {
             $event = array();
             $event['event'] = 'fileexists';
             $event['newfile'] = new stdClass();
             $event['newfile']->filepath = $record->filepath;
             $event['newfile']->filename = $unused_filename;
             $event['newfile']->url = moodle_url::make_draftfile_url($record->itemid, $record->filepath, $unused_filename)->out(false);
             $event['existingfile'] = new stdClass();
             $event['existingfile']->filepath = $record->filepath;
             $event['existingfile']->filename = $existingfilename;
             $event['existingfile']->url = moodle_url::make_draftfile_url($record->itemid, $record->filepath, $existingfilename)->out(false);
             return $event;
         }
     } else {
         $stored_file = $fs->create_file_from_pathname($record, $_FILES[$elname]['tmp_name']);
     }
     return array('url' => moodle_url::make_draftfile_url($record->itemid, $record->filepath, $record->filename)->out(false), 'id' => $record->itemid, 'file' => $record->filename);
 }
Пример #4
0
 /**
  * This function is used to copy a moodle file to draft area.
  *
  * It DOES NOT check if the user is allowed to access this file because the actual file
  * can be located in the area where user does not have access to but there is an alias
  * to this file in the area where user CAN access it.
  * {@link file_is_accessible} should be called for alias location before calling this function.
  *
  * @param string $source The metainfo of file, it is base64 encoded php serialized data
  * @param stdClass|array $filerecord contains itemid, filepath, filename and optionally other
  *      attributes of the new file
  * @param int $maxbytes maximum allowed size of file, -1 if unlimited. If size of file exceeds
  *      the limit, the file_exception is thrown.
  * @param int $areamaxbytes the maximum size of the area. A file_exception is thrown if the
  *      new file will reach the limit.
  * @return array The information about the created file
  */
 public function copy_to_area($source, $filerecord, $maxbytes = -1, $areamaxbytes = FILE_AREA_MAX_BYTES_UNLIMITED)
 {
     global $USER;
     $fs = get_file_storage();
     if ($this->has_moodle_files() == false) {
         throw new coding_exception('Only repository used to browse moodle files can use repository::copy_to_area()');
     }
     $user_context = context_user::instance($USER->id);
     $filerecord = (array) $filerecord;
     // make sure the new file will be created in user draft area
     $filerecord['component'] = 'user';
     $filerecord['filearea'] = 'draft';
     $filerecord['contextid'] = $user_context->id;
     $draftitemid = $filerecord['itemid'];
     $new_filepath = $filerecord['filepath'];
     $new_filename = $filerecord['filename'];
     // the file needs to copied to draft area
     $stored_file = self::get_moodle_file($source);
     if ($maxbytes != -1 && $stored_file->get_filesize() > $maxbytes) {
         throw new file_exception('maxbytes');
     }
     // Validate the size of the draft area.
     if (file_is_draft_area_limit_reached($draftitemid, $areamaxbytes, $stored_file->get_filesize())) {
         throw new file_exception('maxareabytes');
     }
     if (repository::draftfile_exists($draftitemid, $new_filepath, $new_filename)) {
         // create new file
         $unused_filename = repository::get_unused_filename($draftitemid, $new_filepath, $new_filename);
         $filerecord['filename'] = $unused_filename;
         $fs->create_file_from_storedfile($filerecord, $stored_file);
         $event = array();
         $event['event'] = 'fileexists';
         $event['newfile'] = new stdClass();
         $event['newfile']->filepath = $new_filepath;
         $event['newfile']->filename = $unused_filename;
         $event['newfile']->url = moodle_url::make_draftfile_url($draftitemid, $new_filepath, $unused_filename)->out();
         $event['existingfile'] = new stdClass();
         $event['existingfile']->filepath = $new_filepath;
         $event['existingfile']->filename = $new_filename;
         $event['existingfile']->url = moodle_url::make_draftfile_url($draftitemid, $new_filepath, $new_filename)->out();
         return $event;
     } else {
         $fs->create_file_from_storedfile($filerecord, $stored_file);
         $info = array();
         $info['itemid'] = $draftitemid;
         $info['file'] = $new_filename;
         $info['title'] = $new_filename;
         $info['contextid'] = $user_context->id;
         $info['url'] = moodle_url::make_draftfile_url($draftitemid, $new_filepath, $new_filename)->out();
         $info['filesize'] = $stored_file->get_filesize();
         return $info;
     }
 }
Пример #5
0
/**
 * Saves files from a draft file area to a real one (merging the list of files).
 * Can rewrite URLs in some content at the same time if desired.
 *
 * @category files
 * @global stdClass $USER
 * @param int $draftitemid the id of the draft area to use. Normally obtained
 *      from file_get_submitted_draft_itemid('elementname') or similar.
 * @param int $contextid This parameter and the next two identify the file area to save to.
 * @param string $component
 * @param string $filearea indentifies the file area.
 * @param int $itemid helps identifies the file area.
 * @param array $options area options (subdirs=>false, maxfiles=-1, maxbytes=0)
 * @param string $text some html content that needs to have embedded links rewritten
 *      to the @@PLUGINFILE@@ form for saving in the database.
 * @param bool $forcehttps force https urls.
 * @return string|null if $text was passed in, the rewritten $text is returned. Otherwise NULL.
 */
function file_save_draft_area_files($draftitemid, $contextid, $component, $filearea, $itemid, array $options = null, $text = null, $forcehttps = false)
{
    global $USER;
    $usercontext = context_user::instance($USER->id);
    $fs = get_file_storage();
    $options = (array) $options;
    if (!isset($options['subdirs'])) {
        $options['subdirs'] = false;
    }
    if (!isset($options['maxfiles'])) {
        $options['maxfiles'] = -1;
        // unlimited
    }
    if (!isset($options['maxbytes']) || $options['maxbytes'] == USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
        $options['maxbytes'] = 0;
        // unlimited
    }
    if (!isset($options['areamaxbytes'])) {
        $options['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED;
        // Unlimited.
    }
    $allowreferences = true;
    if (isset($options['return_types']) && !($options['return_types'] & FILE_REFERENCE)) {
        // we assume that if $options['return_types'] is NOT specified, we DO allow references.
        // this is not exactly right. BUT there are many places in code where filemanager options
        // are not passed to file_save_draft_area_files()
        $allowreferences = false;
    }
    // Check if the draft area has exceeded the authorised limit. This should never happen as validation
    // should have taken place before, unless the user is doing something nauthly. If so, let's just not save
    // anything at all in the next area.
    if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) {
        return null;
    }
    $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
    $oldfiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'id');
    // One file in filearea means it is empty (it has only top-level directory '.').
    if (count($draftfiles) > 1 || count($oldfiles) > 1) {
        // we have to merge old and new files - we want to keep file ids for files that were not changed
        // we change time modified for all new and changed files, we keep time created as is
        $newhashes = array();
        $filecount = 0;
        foreach ($draftfiles as $file) {
            if (!$options['subdirs'] && $file->get_filepath() !== '/') {
                continue;
            }
            if (!$allowreferences && $file->is_external_file()) {
                continue;
            }
            if (!$file->is_directory()) {
                if ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize()) {
                    // oversized file - should not get here at all
                    continue;
                }
                if ($options['maxfiles'] != -1 and $options['maxfiles'] <= $filecount) {
                    // more files - should not get here at all
                    continue;
                }
                $filecount++;
            }
            $newhash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $file->get_filepath(), $file->get_filename());
            $newhashes[$newhash] = $file;
        }
        // Loop through oldfiles and decide which we need to delete and which to update.
        // After this cycle the array $newhashes will only contain the files that need to be added.
        foreach ($oldfiles as $oldfile) {
            $oldhash = $oldfile->get_pathnamehash();
            if (!isset($newhashes[$oldhash])) {
                // delete files not needed any more - deleted by user
                $oldfile->delete();
                continue;
            }
            $newfile = $newhashes[$oldhash];
            // Now we know that we have $oldfile and $newfile for the same path.
            // Let's check if we can update this file or we need to delete and create.
            if ($newfile->is_directory()) {
                // Directories are always ok to just update.
            } else {
                if (($source = @unserialize($newfile->get_source())) && isset($source->original)) {
                    // File has the 'original' - we need to update the file (it may even have not been changed at all).
                    $original = file_storage::unpack_reference($source->original);
                    if ($original['filename'] !== $oldfile->get_filename() || $original['filepath'] !== $oldfile->get_filepath()) {
                        // Very odd, original points to another file. Delete and create file.
                        $oldfile->delete();
                        continue;
                    }
                } else {
                    // The same file name but absence of 'original' means that file was deteled and uploaded again.
                    // By deleting and creating new file we properly manage all existing references.
                    $oldfile->delete();
                    continue;
                }
            }
            // status changed, we delete old file, and create a new one
            if ($oldfile->get_status() != $newfile->get_status()) {
                // file was changed, use updated with new timemodified data
                $oldfile->delete();
                // This file will be added later
                continue;
            }
            // Updated author
            if ($oldfile->get_author() != $newfile->get_author()) {
                $oldfile->set_author($newfile->get_author());
            }
            // Updated license
            if ($oldfile->get_license() != $newfile->get_license()) {
                $oldfile->set_license($newfile->get_license());
            }
            // Updated file source
            // Field files.source for draftarea files contains serialised object with source and original information.
            // We only store the source part of it for non-draft file area.
            $newsource = $newfile->get_source();
            if ($source = @unserialize($newfile->get_source())) {
                $newsource = $source->source;
            }
            if ($oldfile->get_source() !== $newsource) {
                $oldfile->set_source($newsource);
            }
            // Updated sort order
            if ($oldfile->get_sortorder() != $newfile->get_sortorder()) {
                $oldfile->set_sortorder($newfile->get_sortorder());
            }
            // Update file timemodified
            if ($oldfile->get_timemodified() != $newfile->get_timemodified()) {
                $oldfile->set_timemodified($newfile->get_timemodified());
            }
            // Replaced file content
            if (!$oldfile->is_directory() && ($oldfile->get_contenthash() != $newfile->get_contenthash() || $oldfile->get_filesize() != $newfile->get_filesize() || $oldfile->get_referencefileid() != $newfile->get_referencefileid() || $oldfile->get_userid() != $newfile->get_userid())) {
                $oldfile->replace_file_with($newfile);
            }
            // unchanged file or directory - we keep it as is
            unset($newhashes[$oldhash]);
        }
        // Add fresh file or the file which has changed status
        // the size and subdirectory tests are extra safety only, the UI should prevent it
        foreach ($newhashes as $file) {
            $file_record = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid, 'timemodified' => time());
            if ($source = @unserialize($file->get_source())) {
                // Field files.source for draftarea files contains serialised object with source and original information.
                // We only store the source part of it for non-draft file area.
                $file_record['source'] = $source->source;
            }
            if ($file->is_external_file()) {
                $repoid = $file->get_repository_id();
                if (!empty($repoid)) {
                    $file_record['repositoryid'] = $repoid;
                    $file_record['reference'] = $file->get_reference();
                }
            }
            $fs->create_file_from_storedfile($file_record, $file);
        }
    }
    // note: do not purge the draft area - we clean up areas later in cron,
    //       the reason is that user might press submit twice and they would loose the files,
    //       also sometimes we might want to use hacks that save files into two different areas
    if (is_null($text)) {
        return null;
    } else {
        return file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps);
    }
}
Пример #6
0
                 $fileinfo = $repo->copy_to_area($reference, $record, $maxbytes, $areamaxbytes);
                 echo json_encode($fileinfo);
                 die;
             } else {
                 // Download file to moodle.
                 $downloadedfile = $repo->get_file($reference, $saveas_filename);
                 if (empty($downloadedfile['path'])) {
                     $err->error = get_string('cannotdownload', 'repository');
                     die(json_encode($err));
                 }
                 // Check if exceed maxbytes.
                 if ($maxbytes != -1 && filesize($downloadedfile['path']) > $maxbytes) {
                     throw new file_exception('maxbytes');
                 }
                 // Check if we exceed the max bytes of the area.
                 if (file_is_draft_area_limit_reached($itemid, $areamaxbytes, filesize($downloadedfile['path']))) {
                     throw new file_exception('maxareabytes');
                 }
                 $info = repository::move_to_filepool($downloadedfile['path'], $record);
                 if (empty($info)) {
                     $info['e'] = get_string('error', 'moodle');
                 }
             }
         }
         echo json_encode($info);
         die;
     }
     break;
 case 'upload':
     $result = $repo->upload($saveas_filename, $maxbytes);
     echo json_encode($result);
 /**
  * Process a message received and validated by the Inbound Message processor.
  *
  * @throws \core\message\inbound\processing_failed_exception
  * @param \stdClass $record The Inbound Message record
  * @param \stdClass $data The message data packet
  * @return bool Whether the message was successfully processed.
  */
 public function process_message(\stdClass $record, \stdClass $data)
 {
     global $USER, $CFG;
     $context = \context_user::instance($USER->id);
     if (!has_capability('moodle/user:manageownfiles', $context)) {
         throw new \core\message\inbound\processing_failed_exception('emailtoprivatefilesdenied', 'moodle', $data);
     }
     // Initial setup.
     $component = 'user';
     $filearea = 'private';
     $itemid = 0;
     $license = $CFG->sitedefaultlicense;
     $author = fullname($USER);
     // Determine the quota space for this user.
     $maxbytes = $CFG->userquota;
     if (has_capability('moodle/user:ignoreuserquota', $context)) {
         $maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS;
     }
     // Keep track of files which were uploaded, and which were skipped.
     $skippedfiles = array();
     $uploadedfiles = array();
     $failedfiles = array();
     $fs = get_file_storage();
     foreach ($data->attachments as $attachmenttype => $attachments) {
         foreach ($attachments as $attachment) {
             mtrace("--- Processing attachment '{$attachment->filename}'");
             if (file_is_draft_area_limit_reached($itemid, $maxbytes, $attachment->filesize)) {
                 // The user quota will be exceeded if this file is included.
                 $skippedfiles[] = $attachment;
                 mtrace("---- Skipping attacment. User will be over quota.");
                 continue;
             }
             // Create a new record for this file.
             $record = new \stdClass();
             $record->filearea = $filearea;
             $record->component = $component;
             $record->filepath = '/';
             $record->itemid = $itemid;
             $record->license = $license;
             $record->author = $author;
             $record->contextid = $context->id;
             $record->userid = $USER->id;
             $record->filename = $fs->get_unused_filename($context->id, $record->component, $record->filearea, $record->itemid, $record->filepath, $attachment->filename);
             mtrace("--> Attaching {$record->filename} to " . "/{$record->contextid}/{$record->component}/{$record->filearea}/" . "{$record->itemid}{$record->filepath}{$record->filename}");
             if ($fs->create_file_from_string($record, $attachment->content)) {
                 // File created successfully.
                 mtrace("---- File uploaded successfully as {$record->filename}.");
                 $uploadedfiles[] = $attachment;
             } else {
                 mtrace("---- Skipping attacment. Unknown failure during creation.");
                 $failedfiles[] = $attachment;
             }
         }
     }
     // TODO send the user a confirmation e-mail.
     // Note, some files may have failed because the user has been pushed over quota. This does not constitute a failure.
     return true;
 }
Пример #8
0
 $record->userid = $USER->id;
 $record->contextid = $user_context->id;
 $record->sortorder = 0;
 if ($repo->has_moodle_files()) {
     $fileinfo = $repo->copy_to_area($reference, $record, $maxbytes, $areamaxbytes);
     redirect($home_url, get_string('downloadsucc', 'repository'));
 } else {
     $thefile = $repo->get_file($reference, $filename);
     if (!empty($thefile['path'])) {
         $filesize = filesize($thefile['path']);
         if ($maxbytes != -1 && $filesize > $maxbytes) {
             unlink($thefile['path']);
             print_error('maxbytes');
         }
         // Ensure the file will not make the area exceed its size limit.
         if (file_is_draft_area_limit_reached($record->itemid, $areamaxbytes, $filesize)) {
             unlink($thefile['path']);
             print_error('maxareabytes');
         }
         try {
             $info = repository::move_to_filepool($thefile['path'], $record);
             redirect($home_url, get_string('downloadsucc', 'repository'));
         } catch (moodle_exception $e) {
             // inject target URL
             $e->link = $PAGE->url->out();
             echo $OUTPUT->header();
             // hack: we need the embedded header here, standard error printing would not use it
             throw $e;
         }
     } else {
         print_error('cannotdownload', 'repository');
Пример #9
0
/**
 * Saves files from a draft file area to a real one (merging the list of files).
 * Can rewrite URLs in some content at the same time if desired.
 *
 * @category files
 * @global stdClass $USER
 * @param int $draftitemid the id of the draft area to use. Normally obtained
 *      from file_get_submitted_draft_itemid('elementname') or similar.
 * @param int $contextid This parameter and the next two identify the file area to save to.
 * @param string $component
 * @param string $filearea indentifies the file area.
 * @param int $itemid helps identifies the file area.
 * @param array $options area options (subdirs=>false, maxfiles=-1, maxbytes=0)
 * @param string $text some html content that needs to have embedded links rewritten
 *      to the @@PLUGINFILE@@ form for saving in the database.
 * @param bool $forcehttps force https urls.
 * @return string|null if $text was passed in, the rewritten $text is returned. Otherwise NULL.
 */
function file_save_draft_area_files($draftitemid, $contextid, $component, $filearea, $itemid, array $options = null, $text = null, $forcehttps = false)
{
    global $USER;
    $usercontext = context_user::instance($USER->id);
    $fs = get_file_storage();
    $options = (array) $options;
    if (!isset($options['subdirs'])) {
        $options['subdirs'] = false;
    }
    if (!isset($options['maxfiles'])) {
        $options['maxfiles'] = -1;
        // unlimited
    }
    if (!isset($options['maxbytes']) || $options['maxbytes'] == USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
        $options['maxbytes'] = 0;
        // unlimited
    }
    if (!isset($options['areamaxbytes'])) {
        $options['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED;
        // Unlimited.
    }
    $allowreferences = true;
    if (isset($options['return_types']) && !($options['return_types'] & FILE_REFERENCE)) {
        // we assume that if $options['return_types'] is NOT specified, we DO allow references.
        // this is not exactly right. BUT there are many places in code where filemanager options
        // are not passed to file_save_draft_area_files()
        $allowreferences = false;
    }
    // Check if the draft area has exceeded the authorised limit. This should never happen as validation
    // should have taken place before, unless the user is doing something nauthly. If so, let's just not save
    // anything at all in the next area.
    if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) {
        return null;
    }
    $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
    $oldfiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'id');
    if (count($draftfiles) < 2) {
        // means there are no files - one file means root dir only ;-)
        $fs->delete_area_files($contextid, $component, $filearea, $itemid);
    } else {
        if (count($oldfiles) < 2) {
            $filecount = 0;
            // there were no files before - one file means root dir only ;-)
            foreach ($draftfiles as $file) {
                $file_record = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid);
                if (!$options['subdirs']) {
                    if ($file->get_filepath() !== '/' or $file->is_directory()) {
                        continue;
                    }
                }
                if ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize()) {
                    // oversized file - should not get here at all
                    continue;
                }
                if ($options['maxfiles'] != -1 and $options['maxfiles'] <= $filecount) {
                    // more files - should not get here at all
                    break;
                }
                if (!$file->is_directory()) {
                    $filecount++;
                }
                if ($file->is_external_file()) {
                    if (!$allowreferences) {
                        continue;
                    }
                    $repoid = $file->get_repository_id();
                    if (!empty($repoid)) {
                        $file_record['repositoryid'] = $repoid;
                        $file_record['reference'] = $file->get_reference();
                    }
                }
                file_restore_source_field_from_draft_file($file);
                $fs->create_file_from_storedfile($file_record, $file);
            }
        } else {
            // we have to merge old and new files - we want to keep file ids for files that were not changed
            // we change time modified for all new and changed files, we keep time created as is
            $newhashes = array();
            foreach ($draftfiles as $file) {
                $newhash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $file->get_filepath(), $file->get_filename());
                file_restore_source_field_from_draft_file($file);
                $newhashes[$newhash] = $file;
            }
            $filecount = 0;
            foreach ($oldfiles as $oldfile) {
                $oldhash = $oldfile->get_pathnamehash();
                if (!isset($newhashes[$oldhash])) {
                    // delete files not needed any more - deleted by user
                    $oldfile->delete();
                    continue;
                }
                $newfile = $newhashes[$oldhash];
                // status changed, we delete old file, and create a new one
                if ($oldfile->get_status() != $newfile->get_status()) {
                    // file was changed, use updated with new timemodified data
                    $oldfile->delete();
                    // This file will be added later
                    continue;
                }
                // Updated author
                if ($oldfile->get_author() != $newfile->get_author()) {
                    $oldfile->set_author($newfile->get_author());
                }
                // Updated license
                if ($oldfile->get_license() != $newfile->get_license()) {
                    $oldfile->set_license($newfile->get_license());
                }
                // Updated file source
                if ($oldfile->get_source() != $newfile->get_source()) {
                    $oldfile->set_source($newfile->get_source());
                }
                // Updated sort order
                if ($oldfile->get_sortorder() != $newfile->get_sortorder()) {
                    $oldfile->set_sortorder($newfile->get_sortorder());
                }
                // Update file timemodified
                if ($oldfile->get_timemodified() != $newfile->get_timemodified()) {
                    $oldfile->set_timemodified($newfile->get_timemodified());
                }
                // Replaced file content
                if ($oldfile->get_contenthash() != $newfile->get_contenthash() || $oldfile->get_filesize() != $newfile->get_filesize()) {
                    $oldfile->replace_content_with($newfile);
                    // push changes to all local files that are referencing this file
                    $fs->update_references_to_storedfile($oldfile);
                }
                // unchanged file or directory - we keep it as is
                unset($newhashes[$oldhash]);
                if (!$oldfile->is_directory()) {
                    $filecount++;
                }
            }
            // Add fresh file or the file which has changed status
            // the size and subdirectory tests are extra safety only, the UI should prevent it
            foreach ($newhashes as $file) {
                $file_record = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid, 'timemodified' => time());
                if (!$options['subdirs']) {
                    if ($file->get_filepath() !== '/' or $file->is_directory()) {
                        continue;
                    }
                }
                if ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize()) {
                    // oversized file - should not get here at all
                    continue;
                }
                if ($options['maxfiles'] != -1 and $options['maxfiles'] <= $filecount) {
                    // more files - should not get here at all
                    break;
                }
                if (!$file->is_directory()) {
                    $filecount++;
                }
                if ($file->is_external_file()) {
                    if (!$allowreferences) {
                        continue;
                    }
                    $repoid = $file->get_repository_id();
                    if (!empty($repoid)) {
                        $file_record['repositoryid'] = $repoid;
                        $file_record['reference'] = $file->get_reference();
                    }
                }
                $fs->create_file_from_storedfile($file_record, $file);
            }
        }
    }
    // note: do not purge the draft area - we clean up areas later in cron,
    //       the reason is that user might press submit twice and they would loose the files,
    //       also sometimes we might want to use hacks that save files into two different areas
    if (is_null($text)) {
        return null;
    } else {
        return file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps);
    }
}