diff --git a/admin/repository.php b/admin/repository.php index c3409a5b5a0..a3af2ddc588 100644 --- a/admin/repository.php +++ b/admin/repository.php @@ -1,12 +1,27 @@ . require_once(dirname(dirname(__FILE__)) . '/config.php'); require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir . '/adminlib.php'); -$repository = optional_param('repos', '', PARAM_FORMAT); -$action = optional_param('action', '', PARAM_ALPHA); -$sure = optional_param('sure', '', PARAM_ALPHA); +$repository = optional_param('repos', '', PARAM_ALPHANUMEXT); +$action = optional_param('action', '', PARAM_ACTION); +$sure = optional_param('sure', '', PARAM_ALPHA); +$downloadcontents = optional_param('downloadcontents', '', PARAM_ALPHA); $display = true; // fall through to normal display @@ -42,6 +57,10 @@ $configstr = get_string('manage', 'repository'); $return = true; +if (!empty($action)) { + require_sesskey(); +} + /** * Helper function that generates a moodle_url object * relevant to the repository @@ -152,10 +171,10 @@ if (($action == 'edit') || ($action == 'new')) { // Display instances list and creation form if ($action == 'edit') { - $instanceoptionnames = repository::static_function($repository, 'get_instance_option_names'); - if (!empty($instanceoptionnames)) { - repository::display_instances_list(get_context_instance(CONTEXT_SYSTEM), $repository); - } + $instanceoptionnames = repository::static_function($repository, 'get_instance_option_names'); + if (!empty($instanceoptionnames)) { + repository::display_instances_list(context_system::instance(), $repository); + } } } } else if ($action == 'show') { @@ -185,7 +204,14 @@ if (($action == 'edit') || ($action == 'new')) { if (!confirm_sesskey()) { print_error('confirmsesskeybad', '', $baseurl); } - if ($repositorytype->delete()) { + + if (!empty($downloadcontents) and $downloadcontents == 'yes') { + $downloadcontents = true; + } else { + $downloadcontents = false; + } + + if ($repositorytype->delete($downloadcontents)) { redirect($baseurl); } else { print_error('instancenotdeleted', 'repository', $baseurl); @@ -193,7 +219,34 @@ if (($action == 'edit') || ($action == 'new')) { exit; } else { echo $OUTPUT->header(); - echo $OUTPUT->confirm(get_string('confirmremove', 'repository', $repositorytype->get_readablename()), $sesskeyurl . '&action=delete&repos=' . $repository . '&sure=yes', $baseurl); + + $message = get_string('confirmremove', 'repository', $repositorytype->get_readablename()); + + $output = $OUTPUT->box_start('generalbox', 'notice'); + $output .= html_writer::tag('p', $message); + + $removeurl = new moodle_url($sesskeyurl); + $removeurl->params(array( + 'action' =>'delete', + 'repos' => $repository, + 'sure' => 'yes', + )); + + $removeanddownloadurl = new moodle_url($sesskeyurl); + $removeanddownloadurl->params(array( + 'action' =>'delete', + 'repos'=> $repository, + 'sure' => 'yes', + 'downloadcontents' => 'yes', + )); + + $output .= $OUTPUT->single_button($removeurl, get_string('continueuninstall', 'repository')); + $output .= $OUTPUT->single_button($removeanddownloadurl, get_string('continueuninstallanddownload', 'repository')); + $output .= $OUTPUT->single_button($baseurl, get_string('cancel')); + $output .= $OUTPUT->box_end(); + + echo $output; + $return = false; } } else if ($action == 'moveup') { @@ -255,7 +308,7 @@ if (($action == 'edit') || ($action == 'new')) { // Calculate number of instances in order to display them for the Moodle administrator if (!empty($instanceoptionnames)) { $params = array(); - $params['context'] = array(get_system_context()); + $params['context'] = array(context_system::instance()); $params['onlyvisible'] = false; $params['type'] = $typename; $admininstancenumber = count(repository::static_function($typename, 'get_instances', $params)); diff --git a/admin/repositoryinstance.php b/admin/repositoryinstance.php index e75193d0be4..f2b6e7f8e4f 100644 --- a/admin/repositoryinstance.php +++ b/admin/repositoryinstance.php @@ -1,18 +1,35 @@ . require_once(dirname(dirname(__FILE__)) . '/config.php'); require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir . '/adminlib.php'); +require_sesskey(); + // id of repository $edit = optional_param('edit', 0, PARAM_INT); -$new = optional_param('new', '', PARAM_FORMAT); +$new = optional_param('new', '', PARAM_PLUGIN); $hide = optional_param('hide', 0, PARAM_INT); $delete = optional_param('delete', 0, PARAM_INT); $sure = optional_param('sure', '', PARAM_ALPHA); $type = optional_param('type', '', PARAM_PLUGIN); +$downloadcontents = optional_param('downloadcontents', '', PARAM_ALPHA); -$context = get_context_instance(CONTEXT_SYSTEM); +$context = context_system::instance(); $pagename = 'repositorycontroller'; @@ -24,16 +41,20 @@ if ($edit){ $pagename = 'repositoryinstancenew'; } -admin_externalpage_setup($pagename); -require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); +admin_externalpage_setup($pagename, '', null, new moodle_url('/admin/repositoryinstances.php')); +require_capability('moodle/site:config', $context); + +$baseurl = new moodle_url("/$CFG->admin/repositoryinstance.php", array('sesskey'=>sesskey())); + +$parenturl = new moodle_url("/$CFG->admin/repository.php", array( + 'sesskey'=>sesskey(), + 'action'=>'edit', +)); -$sesskeyurl = "$CFG->wwwroot/$CFG->admin/repositoryinstance.php?sesskey=" . sesskey(); -$baseurl = "$CFG->wwwroot/$CFG->admin/repository.php?session=". sesskey() .'&action=edit&repos='; if ($new) { - $baseurl .= $new; -} -else { - $baseurl .= $type; + $parenturl->param('repos', $new); +} else { + $parenturl->param('repos', $type); } $return = true; @@ -48,7 +69,7 @@ if (!empty($edit) || !empty($new)) { $typeid = $instance->options['typeid']; } else { $plugin = $new; - $typeid = $new; + $typeid = null; $instance = null; } @@ -57,12 +78,9 @@ if (!empty($edit) || !empty($new)) { // end setup, begin output if ($mform->is_cancelled()){ - redirect($baseurl); + redirect($parenturl); exit; } else if ($fromform = $mform->get_data()){ - if (!confirm_sesskey()) { - print_error('confirmsesskeybad', '', $baseurl); - } if ($edit) { $settings = array(); $settings['name'] = $fromform->name; @@ -77,13 +95,13 @@ if (!empty($edit) || !empty($new)) { } $success = $instance->set_option($settings); } else { - $success = repository::static_function($plugin, 'create', $plugin, 0, get_system_context(), $fromform); + $success = repository::static_function($plugin, 'create', $plugin, 0, $context, $fromform); $data = data_submitted(); } if ($success) { - redirect($baseurl); + redirect($parenturl); } else { - print_error('instancenotsaved', 'repository', $baseurl); + print_error('instancenotsaved', 'repository', $parenturl); } exit; } else { @@ -95,9 +113,6 @@ if (!empty($edit) || !empty($new)) { $return = false; } } else if (!empty($hide)) { - if (!confirm_sesskey()) { - print_error('confirmsesskeybad', '', $baseurl); - } $instance = repository::get_type_by_typename($hide); $instance->hide(); $return = true; @@ -108,25 +123,43 @@ if (!empty($edit) || !empty($new)) { throw new repository_exception('readonlyinstance', 'repository'); } if ($sure) { - if (!confirm_sesskey()) { - print_error('confirmsesskeybad', '', $baseurl); - } - if ($instance->delete()) { - $deletedstr = get_string('instancedeleted', 'repository'); - redirect($baseurl, $deletedstr, 3); + if (!empty($downloadcontents) and $downloadcontents == 'yes') { + $downloadcontents = true; } else { - print_error('instancenotdeleted', 'repository', $baseurl); + $downloadcontents = false; + } + if ($instance->delete($downloadcontents)) { + $deletedstr = get_string('instancedeleted', 'repository'); + redirect($parenturl, $deletedstr, 3); + } else { + print_error('instancenotdeleted', 'repository', $parenturl); } exit; } echo $OUTPUT->header(); - echo $OUTPUT->confirm(get_string('confirmdelete', 'repository', $instance->name), "$sesskeyurl&type=$type'&delete=$delete'&sure=yes", "$CFG->wwwroot/$CFG->admin/repositoryinstance.php?session=". sesskey()); + echo $OUTPUT->box_start('generalbox', 'notice'); + $continueurl = new moodle_url($baseurl, array( + 'type' => $type, + 'delete' => $delete, + 'sure' => 'yes', + )); + $continueanddownloadurl = new moodle_url($continueurl, array( + 'downloadcontents' => 'yes' + )); + $message = get_string('confirmdelete', 'repository', $instance->name); + echo html_writer::tag('p', $message); + + echo $OUTPUT->single_button($continueurl, get_string('continueuninstall', 'repository')); + echo $OUTPUT->single_button($continueanddownloadurl, get_string('continueuninstallanddownload', 'repository')); + echo $OUTPUT->single_button($parenturl, get_string('cancel')); + + echo $OUTPUT->box_end(); + $return = false; } if (!empty($return)) { - - redirect($baseurl); + redirect($parenturl); } echo $OUTPUT->footer(); diff --git a/admin/tool/unittest/coveragefile.php b/admin/tool/unittest/coveragefile.php index 8c17b60e3eb..d3c8737a4e8 100644 --- a/admin/tool/unittest/coveragefile.php +++ b/admin/tool/unittest/coveragefile.php @@ -60,9 +60,8 @@ if (!isset($args[0]) || !in_array($args[0], $alloweddirs)) { print_error('invalidarguments'); } -// only serve some controlled extensions -$allowedextensions = array('text/html', 'text/css', 'image/gif', 'application/x-javascript'); -if (!in_array(mimeinfo('type', $filepath), $allowedextensions)) { +// only serve some controlled extensions/mimetypes +if (!file_extension_in_typegroup($filepath, array('web_file', 'web_image'), true)) { print_error('invalidarguments'); } diff --git a/backup/backupfilesedit_form.php b/backup/backupfilesedit_form.php index 7906a080d1a..36a4030da07 100644 --- a/backup/backupfilesedit_form.php +++ b/backup/backupfilesedit_form.php @@ -27,7 +27,7 @@ class backup_files_edit_form extends moodleform { function definition() { $mform =& $this->_form; $contextid = $this->_customdata['contextid']; - $options = array('subdirs'=>0, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL); + $options = array('subdirs'=>0, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL | FILE_REFERENCE); $mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options); $mform->addElement('hidden', 'contextid', $this->_customdata['contextid']); $mform->addElement('hidden', 'currentcontext', $this->_customdata['currentcontext']); diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index e7b6018cdf3..d1be602bc8f 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -1404,7 +1404,7 @@ class backup_final_files_structure_step extends backup_structure_step { 'contenthash', 'contextid', 'component', 'filearea', 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'timecreated', 'timemodified', - 'source', 'author', 'license', 'sortorder')); + 'source', 'author', 'license', 'sortorder', 'reference', 'repositoryid')); // Build the tree @@ -1412,9 +1412,12 @@ class backup_final_files_structure_step extends backup_structure_step { // Define sources - $file->set_source_sql("SELECT f.* + $file->set_source_sql("SELECT f.*, r.repositoryid, r.reference FROM {files} f - JOIN {backup_ids_temp} bi ON f.id = bi.itemid + JOIN {files_reference} r + ON r.id = f.referencefileid + JOIN {backup_ids_temp} bi + ON f.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'filefinal'", array(backup::VAR_BACKUPID)); @@ -1442,6 +1445,8 @@ class backup_main_structure_step extends backup_structure_step { $info['backup_date'] = time(); $info['backup_uniqueid']= $this->get_backupid(); $info['mnet_remoteusers']=backup_controller_dbops::backup_includes_mnet_remote_users($this->get_backupid()); + $info['include_file_references_to_external_content'] = + backup_controller_dbops::backup_includes_file_references($this->get_backupid()); $info['original_wwwroot']=$CFG->wwwroot; $info['original_site_identifier_hash'] = md5(get_site_identifier()); $info['original_course_id'] = $this->get_courseid(); @@ -1461,7 +1466,7 @@ class backup_main_structure_step extends backup_structure_step { $information = new backup_nested_element('information', null, array( 'name', 'moodle_version', 'moodle_release', 'backup_version', - 'backup_release', 'backup_date', 'mnet_remoteusers', 'original_wwwroot', + 'backup_release', 'backup_date', 'mnet_remoteusers', 'include_file_references_to_external_content', 'original_wwwroot', 'original_site_identifier_hash', 'original_course_id', 'original_course_fullname', 'original_course_shortname', 'original_course_startdate', 'original_course_contextid', 'original_system_contextid')); @@ -1584,8 +1589,12 @@ class backup_store_backup_file extends backup_execution_step { // Calculate the zip fullpath (in OS temp area it's always backup.mbz) $zipfile = $basepath . '/backup.mbz'; + $has_file_references = backup_controller_dbops::backup_includes_file_references($this->get_backupid()); // Perform storage and return it (TODO: shouldn't be array but proper result object) - return array('backup_destination' => backup_helper::store_backup_file($this->get_backupid(), $zipfile)); + return array( + 'backup_destination' => backup_helper::store_backup_file($this->get_backupid(), $zipfile), + 'include_file_references_to_external_content' => $has_file_references + ); } } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index fda860c9cf1..4d610e5cafb 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -586,11 +586,23 @@ class restore_load_included_files extends restore_structure_step { return array($file); } - // Processing functions go here + /** + * Processing functions go here + * + * @param array $data one file record including repositoryid and reference + */ public function process_file($data) { $data = (object)$data; // handy + $isreference = !empty($data->repositoryid); + $issamesite = $this->task->is_samesite(); + + // If it's not samesite, we skip file refernces + if (!$issamesite && $isreference) { + return; + } + // load it if needed: // - it it is one of the annotated inforef files (course/section/activity/block) // - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever) @@ -601,6 +613,7 @@ class restore_load_included_files extends restore_structure_step { $data->component == 'grouping' || $data->component == 'grade' || $data->component == 'question' || substr($data->component, 0, 5) == 'qtype'); if ($isfileref || $iscomponent) { + // Process files restore_dbops::set_backup_files_record($this->get_restoreid(), $data); } } diff --git a/backup/util/dbops/backup_controller_dbops.class.php b/backup/util/dbops/backup_controller_dbops.class.php index c77aeb6e4c4..ee98987a41d 100644 --- a/backup/util/dbops/backup_controller_dbops.class.php +++ b/backup/util/dbops/backup_controller_dbops.class.php @@ -409,6 +409,27 @@ abstract class backup_controller_dbops extends backup_dbops { return (int)(bool)$count; } + /** + * Given the backupid, detect if the backup contains references to external contents + * + * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} + * @return int + */ + public static function backup_includes_file_references($backupid) { + global $CFG, $DB; + + $sql = "SELECT count(r.repositoryid) + FROM {files} f + JOIN {files_reference} r + ON r.id = f.referencefileid + JOIN {backup_ids_temp} bi + ON f.id = bi.itemid + WHERE bi.backupid = ? + AND bi.itemname = 'filefinal'"; + $count = $DB->count_records_sql($sql, array($backupid)); + return (int)(bool)$count; + } + /** * Given the courseid, return some course related information we want to transport * diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 25f2e533d1c..e50c6e607df 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -685,6 +685,9 @@ abstract class restore_dbops { $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $rec) { $file = (object)unserialize(base64_decode($rec->info)); + + $isreference = !empty($file->repositoryid); + // ignore root dirs (they are created automatically) if ($file->filepath == '/' && $file->filename == '.') { continue; @@ -697,10 +700,12 @@ abstract class restore_dbops { $fs->create_directory($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->userid); continue; } + // arrived here, file found // Find file in backup pool $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash); - if (!file_exists($backuppath)) { + + if (!file_exists($backuppath) && !$isreference) { throw new restore_dbops_exception('file_not_found_in_pool', $file); } if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) { @@ -717,7 +722,11 @@ abstract class restore_dbops { 'author' => $file->author, 'license' => $file->license, 'sortorder' => $file->sortorder); - $fs->create_file_from_pathname($file_record, $backuppath); + if ($isreference) { + $fs->create_file_from_reference($file_record, $file->repositoryid, $file->reference); + } else { + $fs->create_file_from_pathname($file_record, $backuppath); + } } } $rs->close(); @@ -1204,6 +1213,7 @@ abstract class restore_dbops { public static function set_backup_files_record($restoreid, $filerec) { global $DB; + // Store external files info in `info` field $filerec->info = base64_encode(serialize($filerec)); // Serialize the whole rec in info $filerec->backupid = $restoreid; $DB->insert_record('backup_files_temp', $filerec); diff --git a/backup/util/helper/backup_file_manager.class.php b/backup/util/helper/backup_file_manager.class.php index fb6477a6996..9941af7f4b6 100644 --- a/backup/util/helper/backup_file_manager.class.php +++ b/backup/util/helper/backup_file_manager.class.php @@ -73,6 +73,10 @@ class backup_file_manager { $fs = get_file_storage(); $file = $fs->get_file_instance($filerecorid); + // If the file is external file, skip copying. + if ($file->is_external_file()) { + return; + } // Calculate source and target paths (use same subdirs strategy for both) $targetfilepath = self::get_backup_storage_base_dir($backupid) . '/' . diff --git a/backup/util/helper/backup_general_helper.class.php b/backup/util/helper/backup_general_helper.class.php index 73b5d3b4014..b0eb2e237d2 100644 --- a/backup/util/helper/backup_general_helper.class.php +++ b/backup/util/helper/backup_general_helper.class.php @@ -145,6 +145,7 @@ abstract class backup_general_helper extends backup_helper { $info->original_course_startdate= $infoarr['original_course_startdate']; $info->original_course_contextid= $infoarr['original_course_contextid']; $info->original_system_contextid= $infoarr['original_system_contextid']; + $info->include_file_references_to_external_content = $infoarr['include_file_references_to_external_content']; $info->type = $infoarr['details']['detail'][0]['type']; $info->format = $infoarr['details']['detail'][0]['format']; $info->mode = $infoarr['details']['detail'][0]['mode']; diff --git a/backup/util/ui/backup_ui_stage.class.php b/backup/util/ui/backup_ui_stage.class.php index 2cc20455259..1ce9053dd52 100644 --- a/backup/util/ui/backup_ui_stage.class.php +++ b/backup/util/ui/backup_ui_stage.class.php @@ -470,6 +470,9 @@ class backup_ui_stage_complete extends backup_ui_stage_final { $output = ''; $output .= $renderer->box_start(); + if (!empty($this->results['include_file_references_to_external_content'])) { + $output .= $renderer->notification(get_string('filereferencesincluded', 'backup'), 'notifyproblem'); + } $output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess'); $output .= $renderer->continue_button($restorerul); $output .= $renderer->box_end(); diff --git a/backup/util/ui/renderer.php b/backup/util/ui/renderer.php index 7afcdfd368a..9e0ab9dd4f9 100644 --- a/backup/util/ui/renderer.php +++ b/backup/util/ui/renderer.php @@ -92,6 +92,16 @@ class core_backup_renderer extends plugin_renderer_base { $html .= $this->backup_detail_pair(get_string('originalwwwroot', 'backup'), html_writer::tag('span', $details->original_wwwroot, array('class'=>'originalwwwroot')). html_writer::tag('span', '['.$details->original_site_identifier_hash.']', array('class'=>'sitehash sub-detail'))); + if (!empty($details->include_file_references_to_external_content)) { + $message = ''; + if (backup_general_helper::backup_is_samesite($details)) { + $message = $yestick . ' ' . get_string('filereferencessamesite', 'backup'); + } else { + $message = $notick . ' ' . get_string('filereferencesnotsamesite', 'backup'); + } + $html .= $this->backup_detail_pair(get_string('includefilereferences', 'backup'), $message); + } + $html .= html_writer::end_tag('div'); $html .= html_writer::start_tag('div', array('class'=>'backup-section settings-section')); diff --git a/blocks/activity_modules/block_activity_modules.php b/blocks/activity_modules/block_activity_modules.php index c290b2444f4..2aa257865c9 100644 --- a/blocks/activity_modules/block_activity_modules.php +++ b/blocks/activity_modules/block_activity_modules.php @@ -1,5 +1,8 @@ libdir . '/filelib.php'); + class block_activity_modules extends block_list { function init() { $this->title = get_string('pluginname', 'block_activity_modules'); @@ -50,7 +53,7 @@ class block_activity_modules extends block_list { foreach ($modfullnames as $modname => $modfullname) { if ($modname === 'resources') { - $icon = ' '; + $icon = $OUTPUT->pix_icon(file_extension_icon('.htm'), '', 'moodle', array('class' => 'icon')). ' '; $this->content->items[] = ''.$icon.$modfullname.''; } else { $icon = ' '; diff --git a/blocks/private_files/edit.php b/blocks/private_files/edit.php index 5a7667bbd76..67b1e70d5bc 100644 --- a/blocks/private_files/edit.php +++ b/blocks/private_files/edit.php @@ -45,7 +45,7 @@ $PAGE->set_pagelayout('mydashboard'); $PAGE->set_pagetype('user-private-files'); $data = new stdClass(); -$options = array('subdirs'=>1, 'maxbytes'=>$CFG->userquota, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL); +$options = array('subdirs'=>1, 'maxbytes'=>$CFG->userquota, 'maxfiles'=>-1, 'accepted_types'=>'*'); file_prepare_standard_filemanager($data, 'files', $options, $context, 'user', 'private', 0); $mform = new block_private_files_form(null, array('data'=>$data, 'options'=>$options)); diff --git a/blocks/private_files/renderer.php b/blocks/private_files/renderer.php index 5c27971e55a..eb109faa1b8 100644 --- a/blocks/private_files/renderer.php +++ b/blocks/private_files/renderer.php @@ -65,14 +65,13 @@ class block_private_files_renderer extends plugin_renderer_base { } $result = ''; diff --git a/blog/locallib.php b/blog/locallib.php index 9eb1e8fe621..6ac89440782 100644 --- a/blog/locallib.php +++ b/blog/locallib.php @@ -513,10 +513,7 @@ class blog_entry { $ffurl = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.SYSCONTEXTID.'/blog/attachment/'.$this->id.'/'.$filename); $mimetype = $file->get_mimetype(); - $icon = mimeinfo_from_type("icon", $mimetype); - $type = mimeinfo_from_type("type", $mimetype); - - $image = $OUTPUT->pix_icon("f/$icon", $filename, 'moodle', array('class'=>'icon')); + $image = $OUTPUT->pix_icon(file_file_icon($file), $filename, 'moodle', array('class'=>'icon')); if ($return == "html") { $output .= html_writer::link($ffurl, $image); @@ -526,7 +523,7 @@ class blog_entry { $output .= "$strattachment $filename:\n$ffurl\n"; } else { - if (in_array($type, array('image/gif', 'image/jpeg', 'image/png'))) { // Image attachments don't get printed as links + if (file_mimetype_in_typegroup($file->get_mimetype(), 'web_image')) { // Image attachments don't get printed as links $imagereturn .= '
'; } else { $imagereturn .= html_writer::link($ffurl, $image); diff --git a/course/lib.php b/course/lib.php index 9a64052c153..8e073737607 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1518,13 +1518,6 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, //Accessibility: for files get description via icon, this is very ugly hack! $altname = ''; $altname = $mod->modfullname; - if (!empty($customicon)) { - $archetype = plugin_supports('mod', $mod->modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); - if ($archetype == MOD_ARCHETYPE_RESOURCE) { - $mimetype = mimeinfo_from_icon('type', $customicon); - $altname = get_mimetype_description($mimetype); - } - } // Avoid unnecessary duplication: if e.g. a forum name already // includes the word forum (or Forum, etc) then it is unhelpful // to include that in the accessible description that is added. diff --git a/draftfile.php b/draftfile.php index baeceaf7af7..ede57069134 100644 --- a/draftfile.php +++ b/draftfile.php @@ -36,6 +36,7 @@ if (isguestuser()) { } $relativepath = get_file_argument(); +$preview = optional_param('preview', null, PARAM_ALPHANUM); // relative path must start with '/' if (!$relativepath) { @@ -84,4 +85,4 @@ if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->get_filename() == // finally send the file // ======================================== session_get_instance()->write_close(); // unlock session during fileserving -send_stored_file($file, 0, false, true); // force download - security first! +send_stored_file($file, 0, false, true, array('preview' => $preview)); // force download - security first! diff --git a/files/coursefilesedit_form.php b/files/coursefilesedit_form.php index e5498dcec18..fadd387413d 100644 --- a/files/coursefilesedit_form.php +++ b/files/coursefilesedit_form.php @@ -21,7 +21,7 @@ class coursefiles_edit_form extends moodleform { function definition() { $mform =& $this->_form; $contextid = $this->_customdata['contextid']; - $options = array('subdirs'=>1, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL); + $options = array('subdirs'=>1, 'maxfiles'=>-1, 'accepted_types'=>'*'); $mform->addElement('filemanager', 'files_filemanager', '', null, $options); $mform->addElement('hidden', 'contextid', $this->_customdata['contextid']); $this->set_data($this->_customdata['data']); diff --git a/files/filebrowser_ajax.php b/files/filebrowser_ajax.php index 2d651e95765..a8b638b5c54 100644 --- a/files/filebrowser_ajax.php +++ b/files/filebrowser_ajax.php @@ -70,10 +70,10 @@ switch ($action) { if ($child->is_directory()) { $fileitem['isdir'] = true; $fileitem['url'] = $url->out(false); - $fileitem['icon'] = $OUTPUT->pix_icon('f/folder', get_string('icon')); + $fileitem['icon'] = $OUTPUT->pix_icon(file_folder_icon(), get_string('icon')); } else { $fileitem['url'] = $child->get_url(); - $fileitem['icon'] = $OUTPUT->pix_icon('f/'.mimeinfo('icon', $child->get_visible_name()), get_string('icon')); + $fileitem['icon'] = $OUTPUT->pix_icon(file_file_icon($child), get_string('icon')); } $tree[] = $fileitem; } diff --git a/files/renderer.php b/files/renderer.php index 15971636267..e6f0a225cd6 100644 --- a/files/renderer.php +++ b/files/renderer.php @@ -56,25 +56,31 @@ class core_files_renderer extends plugin_renderer_base { $html .= $this->output->box_start(); $table = new html_table(); - $table->head = array(get_string('filename', 'backup'), get_string('size'), get_string('modified')); - $table->align = array('left', 'right', 'right'); + $table->head = array(get_string('name'), get_string('lastmodified'), get_string('size', 'repository'), get_string('type', 'repository')); + $table->align = array('left', 'left', 'left', 'left'); $table->width = '100%'; $table->data = array(); foreach ($tree->tree as $file) { - if (!empty($file['isdir'])) { - $table->data[] = array( - html_writer::link($file['url'], $this->output->pix_icon('f/folder', 'icon') . ' ' . $file['filename']), - '', - $file['filedate'], - ); - } else { - $table->data[] = array( - html_writer::link($file['url'], $this->output->pix_icon('f/'.mimeinfo('icon', $file['filename']), get_string('icon')) . ' ' . $file['filename']), - $file['filesize'], - $file['filedate'], - ); + $filedate = $filesize = $filetype = ''; + if ($file['filedate']) { + $filedate = userdate($file['filedate'], get_string('strftimedatetimeshort', 'langconfig')); } + if (empty($file['isdir'])) { + if ($file['filesize']) { + $filesize = display_size($file['filesize']); + } + $fileicon = file_file_icon($file, 24); + $filetype = get_mimetype_description($file); + } else { + $fileicon = file_folder_icon(24); + } + $table->data[] = array( + html_writer::link($file['url'], $this->output->pix_icon($fileicon, get_string('icon')) . ' ' . $file['filename']), + $filedate, + $filesize, + $filetype + ); } $html .= html_writer::table($table); @@ -82,8 +88,812 @@ class core_files_renderer extends plugin_renderer_base { $html .= $this->output->box_end(); return $html; } -} + /** + * Prints the file manager and initializes all necessary libraries + * + *
+     * $fm = new form_filemanager($options);
+     * $output = get_renderer('core', 'files');
+     * echo $output->render($fm);
+     * 
+ * + * @param form_filemanager $fm File manager to render + * @return string HTML fragment + */ + public function render_form_filemanager($fm) { + static $filemanagertemplateloaded; + $html = $this->fm_print_generallayout($fm); + $module = array( + 'name'=>'form_filemanager', + 'fullpath'=>'/lib/form/filemanager.js', + 'requires' => array('core_filepicker', 'base', 'io-base', 'node', 'json', 'core_dndupload', 'panel', 'resize-plugin', 'dd-plugin'), + 'strings' => array( + array('error', 'moodle'), array('info', 'moodle'), array('confirmdeletefile', 'repository'), + array('draftareanofiles', 'repository'), array('entername', 'repository'), array('enternewname', 'repository'), + array('invalidjson', 'repository'), array('popupblockeddownload', 'repository'), + array('unknownoriginal', 'repository'), array('confirmdeletefolder', 'repository'), + array('confirmdeletefilewithhref', 'repository'), array('confirmrenamefolder', 'repository'), + array('confirmrenamefile', 'repository') + ) + ); + if (empty($filemanagertemplateloaded)) { + $filemanagertemplateloaded = true; + $this->page->requires->js_init_call('M.form_filemanager.set_templates', + array($this->filemanager_js_templates()), true, $module); + } + $this->page->requires->js_init_call('M.form_filemanager.init', array($fm->options), true, $module); + + // non javascript file manager + $html .= ''; + + + return $html; + } + + /** + * Returns html for displaying one file manager + * + * The main element in HTML must have id="filemanager-{$client_id}" and + * class="filemanager fm-loading"; + * After all necessary code on the page (both html and javascript) is loaded, + * the class fm-loading will be removed and added class fm-loaded; + * The main element (class=filemanager) will be assigned the following classes: + * 'fm-maxfiles' - when filemanager has maximum allowed number of files; + * 'fm-nofiles' - when filemanager has no files at all (although there might be folders); + * 'fm-noitems' - when current view (folder) has no items - neither files nor folders; + * 'fm-updating' - when current view is being updated (usually means that loading icon is to be displayed); + * 'fm-nomkdir' - when 'Make folder' action is unavailable (empty($fm->options->subdirs) == true) + * + * Element with class 'filemanager-container' will be holding evens for dnd upload (dragover, etc.). + * It will have class: + * 'dndupload-ready' - when a file is being dragged over the browser + * 'dndupload-over' - when file is being dragged over this filepicker (additional to 'dndupload-ready') + * 'dndupload-uploading' - during the upload process (note that after dnd upload process is + * over, the file manager will refresh the files list and therefore will have for a while class + * fm-updating. Both waiting processes should look similar so the images don't jump for user) + * + * If browser supports Drag-and-drop, the body element will have class 'dndsupported', + * otherwise - 'dndnotsupported'; + * + * Element with class 'fp-content' will be populated with files list; + * Element with class 'fp-btn-add' will hold onclick event for adding a file (opening filepicker); + * Element with class 'fp-btn-mkdir' will hold onclick event for adding new folder; + * Element with class 'fp-btn-download' will hold onclick event for download action; + * + * Element with class 'fp-path-folder' is a template for one folder in path toolbar. + * It will hold mouse click event and will be assigned classes first/last/even/odd respectfully. + * Parent element will receive class 'empty' when there are no folders to be displayed; + * The content of subelement with class 'fp-path-folder-name' will be substituted with folder name; + * + * Element with class 'fp-viewbar' will have the class 'enabled' or 'disabled' when view mode + * can be changed or not; + * Inside element with class 'fp-viewbar' there are expected elements with classes + * 'fp-vb-icons', 'fp-vb-tree' and 'fp-vb-details'. They will handle onclick events to switch + * between the view modes, the last clicked element will have the class 'checked'; + * + * @param form_filemanager $fm + * @return string + */ + private function fm_print_generallayout($fm) { + global $OUTPUT; + $options = $fm->options; + $client_id = $options->client_id; + $straddfile = get_string('addfile', 'repository'); + $strmakedir = get_string('makeafolder', 'moodle'); + $strdownload = get_string('downloadfolder', 'repository'); + $strloading = get_string('loading', 'repository'); + $strnofilesattached = get_string('nofilesattached', 'repository'); + $strdroptoupload = get_string('droptoupload', 'moodle'); + $icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).''; + $restrictions = $this->fm_print_restrictions($fm); + $strdndenabled = get_string('dndenabled_insentence', 'moodle').$OUTPUT->help_icon('dndenabled'); + $strdndenabledinbox = get_string('dndenabled_inbox', 'moodle'); + $loading = get_string('loading', 'repository'); + + $html = ' +
+
+ '.$restrictions.' + - '.$strdndenabled.' +
+
+ +
+ +
+
+
'.$icon_progress.'
+
+
+
+
'.$strnofilesattached.' + '.$strdndenabledinbox.'
+
+
'.$strdroptoupload.'
+
'.$icon_progress.'
+
+
'.$icon_progress.'
+
+
'; + return preg_replace('/\{\!\}/', '', $html); + } + + /** + * FileManager JS template for displaying one file in 'icon view' mode. + * + * Except for elements described in fp_js_template_iconfilename, this template may also + * contain element with class 'fp-contextmenu'. If context menu is available for this + * file, the top element will receive the additional class 'fp-hascontextmenu' and + * the element with class 'fp-contextmenu' will hold onclick event for displaying + * the context menu. + * + * @see fp_js_template_iconfilename() + * @return string + */ + private function fm_js_template_iconfilename() { + $rv = ' +
+ +
+
+
+
+
+
+
+
+
+ '.$this->pix_icon('i/menu', '▶').' +
'; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FileManager JS template for displaying file name in 'table view' and 'tree view' modes. + * + * Except for elements described in fp_js_template_listfilename, this template may also + * contain element with class 'fp-contextmenu'. If context menu is available for this + * file, the top element will receive the additional class 'fp-hascontextmenu' and + * the element with class 'fp-contextmenu' will hold onclick event for displaying + * the context menu. + * + * @todo MDL-32736 remove onclick="return false;" + * @see fp_js_template_listfilename() + * @return string + */ + private function fm_js_template_listfilename() { + $rv = ' + + + + + + '.$this->pix_icon('i/menu', '▶').' +'; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FileManager JS template for displaying 'Make new folder' dialog. + * + * Must be wrapped in an element, CSS for this element must define width and height of the window; + * + * Must have one input element with type="text" (for users to enter the new folder name); + * + * content of element with class 'fp-dlg-curpath' will be replaced with current path where + * new folder is about to be created; + * elements with classes 'fp-dlg-butcreate' and 'fp-dlg-butcancel' will hold onclick events; + * + * @return string + */ + private function fm_js_template_mkdir() { + $rv = ' +
+

New folder name:

+
+ '.get_string('create').' + '.get_string('cancel').' +
'; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FileManager JS template for error/info message displayed as a separate popup window. + * + * @see fp_js_template_message() + * @return string + */ + private function fm_js_template_message() { + return $this->fp_js_template_message(); + } + + /** + * FileManager JS template for window with file information/actions. + * + * All content must be enclosed in one element, CSS for this class must define width and + * height of the window; + * + * Thumbnail image will be added as content to the element with class 'fp-thumbnail'; + * + * Inside the window the elements with the following classnames must be present: + * 'fp-saveas', 'fp-author', 'fp-license', 'fp-path'. Inside each of them must be + * one input element (or select in case of fp-license and fp-path). They may also have labels. + * The elements will be assign with class 'uneditable' and input/select element will become + * disabled if they are not applicable for the particular file; + * + * There may be present elements with classes 'fp-original', 'fp-datemodified', 'fp-datecreated', + * 'fp-size', 'fp-dimensions', 'fp-reflist'. They will receive additional class 'fp-unknown' if + * information is unavailable. If there is information available, the content of embedded + * element with class 'fp-value' will be substituted with the value; + * + * The value of Original ('fp-original') is loaded in separate request. When it is applicable + * but not yet loaded the 'fp-original' element receives additional class 'fp-loading'; + * + * The value of 'Aliases/Shortcuts' ('fp-reflist') is also loaded in separate request. When it + * is applicable but not yet loaded the 'fp-original' element receives additional class + * 'fp-loading'. The string explaining that XX references exist will replace content of element + * 'fp-refcount'. Inside '.fp-reflist .fp-value' each reference will be enclosed in
  • ; + * + * Elements with classes 'fp-file-update', 'fp-file-download', 'fp-file-delete', 'fp-file-zip', + * 'fp-file-unzip', 'fp-file-setmain' and 'fp-file-cancel' will hold corresponding onclick + * events (there may be several elements with class 'fp-file-cancel'); + * + * When confirm button is pressed and file is being selected, the top element receives + * additional class 'loading'. It is removed when response from server is received. + * + * When any of the input fields is changed, the top element receives class 'fp-changed'; + * When current file can be set as main - top element receives class 'fp-cansetmain'; + * When current file is folder/zip/file - top element receives respectfully class + * 'fp-folder'/'fp-zip'/'fp-file'; + * + * @return string + */ + private function fm_js_template_fileselectlayout() { + $strloading = get_string('loading', 'repository'); + $icon_progress = $this->pix_icon('i/loading_small', $strloading).''; + $rv = ' +
    +
    + +

    '.get_string('loading', 'repository').'

    +
    +
    +
    '.get_string('download').' + '.get_string('delete').' + '.get_string('setmainfile', 'repository').' + '.get_string('zip', 'editor').' + '.get_string('unzip').' +
    +
    + + + + + + + + + + + + + +
    :
    :
    :
    :
    :'.$icon_progress.' '.$strloading.'
    :

    '.$icon_progress.' '.$strloading.'

      +
      +

      +
      +

      + '.get_string('update', 'moodle').' + '.get_string('cancel').' +

      +
      +
      +
      '.get_string('lastmodified', 'moodle').':
      +
      '.get_string('datecreated', 'repository').':
      +
      '.get_string('size', 'repository').':
      +
      '.get_string('dimensions', 'repository').':
      +
      +
      '; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FileManager JS template for popup confirm dialogue window. + * + * Must have one top element, CSS for this element must define width and height of the window; + * + * content of element with class 'fp-dlg-text' will be replaced with dialog text; + * elements with classes 'fp-dlg-butconfirm' and 'fp-dlg-butcancel' will + * hold onclick events; + * + * @return string + */ + private function fm_js_template_confirmdialog() { + $rv = ' +
      +
      + '.get_string('ok').' + '.get_string('cancel').' +
      '; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * Returns all FileManager JavaScript templates as an array. + * + * @return array + */ + public function filemanager_js_templates() { + $class_methods = get_class_methods($this); + $templates = array(); + foreach ($class_methods as $method_name) { + if (preg_match('/^fm_js_template_(.*)$/', $method_name, $matches)) + $templates[$matches[1]] = $this->$method_name(); + } + return $templates; + } + + /** + * Displays restrictions for the file manager + * + * @param form_filemanager $fm + * @return string + */ + private function fm_print_restrictions($fm) { + $maxbytes = display_size($fm->options->maxbytes); + if (empty($options->maxfiles) || $options->maxfiles == -1) { + $maxsize = get_string('maxfilesize', 'moodle', $maxbytes); + //$string['maxfilesize'] = 'Maximum size for new files: {$a}'; + } else { + $strparam = (object)array('size' => $maxbytes, 'attachments' => $options->maxfiles); + $maxsize = get_string('maxsizeandattachments', 'moodle', $strparam); + //$string['maxsizeandattachments'] = 'Maximum size for new files: {$a->size}, maximum attachments: {$a->attachments}'; + } + // TODO MDL-32020 also should say about 'File types accepted' + return ''. $maxsize. ''; + } + + /** + * Template for FilePicker with general layout (not QuickUpload). + * + * Must have one top element containing everything else (recommended
      ), + * CSS for this element must define width and height of the filepicker window. Or CSS must + * define min-width, max-width, min-height and max-height and in this case the filepicker + * window will be resizeable; + * + * Element with class 'fp-viewbar' will have the class 'enabled' or 'disabled' when view mode + * can be changed or not; + * Inside element with class 'fp-viewbar' there are expected elements with classes + * 'fp-vb-icons', 'fp-vb-tree' and 'fp-vb-details'. They will handle onclick events to switch + * between the view modes, the last clicked element will have the class 'checked'; + * + * Element with class 'fp-repo' is a template for displaying one repository. Other repositories + * will be attached as siblings (classes first/last/even/odd will be added respectfully). + * The currently selected repostory will have class 'active'. Contents of element with class + * 'fp-repo-name' will be replaced with repository name, source of image with class + * 'fp-repo-icon' will be replaced with repository icon; + * + * Element with class 'fp-content' is obligatory and will hold the current contents; + * + * Element with class 'fp-paging' will contain page navigation (will be deprecated soon); + * + * Element with class 'fp-path-folder' is a template for one folder in path toolbar. + * It will hold mouse click event and will be assigned classes first/last/even/odd respectfully. + * Parent element will receive class 'empty' when there are no folders to be displayed; + * The content of subelement with class 'fp-path-folder-name' will be substituted with folder name; + * + * Element with class 'fp-toolbar' will have class 'empty' if all 'Back', 'Search', 'Refresh', + * 'Logout', 'Manage' and 'Help' are unavailable for this repo; + * + * Inside fp-toolbar there are expected elements with classes fp-tb-back, fp-tb-search, + * fp-tb-refresh, fp-tb-logout, fp-tb-manage and fp-tb-help. Each of them will have + * class 'enabled' or 'disabled' if particular repository has this functionality. + * Element with class 'fp-tb-search' must contain empty form inside, it's contents will + * be substituted with the search form returned by repository (in the most cases it + * is generated with template core_repository_renderer::repository_default_searchform); + * Other elements must have either or
      + + '; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FilePicker JS template to display during loading process (inside element with class 'fp-content'). + * + * @return string + */ + private function fp_js_template_loading() { + return ' +
      +
      + +

      '.get_string('loading', 'repository').'

      +
      +
      '; + } + + /** + * FilePicker JS template for error (inside element with class 'fp-content'). + * + * must have element with class 'fp-error', its content will be replaced with error text + * and the error code will be assigned as additional class to this element + * used errors: invalidjson, nofilesavailable, norepositoriesavailable + * + * @return string + */ + private function fp_js_template_error() { + $rv = ' +
      '; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FilePicker JS template for error/info message displayed as a separate popup window. + * + * Must be wrapped in one element, CSS for this element must define + * width and height of the window. It will be assigned with an additional class 'fp-msg-error' + * or 'fp-msg-info' depending on message type; + * + * content of element with class 'fp-msg-text' will be replaced with error/info text; + * + * element with class 'fp-msg-butok' will hold onclick event + * + * @return string + */ + private function fp_js_template_message() { + $rv = ' +'; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FilePicker JS template for popup dialogue window asking for action when file with the same name already exists. + * + * Must have one top element, CSS for this element must define width and height of the window; + * + * content of element with class 'fp-dlg-text' will be replaced with dialog text; + * elements with classes 'fp-dlg-butoverwrite', 'fp-dlg-butrename' and 'fp-dlg-butcancel' will + * hold onclick events; + * + * content of element with class 'fp-dlg-butrename' will be substituted with appropriate string + * (Note that it may have long text) + * + * @return string + */ + private function fp_js_template_processexistingfile() { + $rv = ' +'; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * FilePicker JS template for repository login form including templates for each element type + * + * Must contain one
      element with templates for different input types inside: + * Elements with classes 'fp-login-popup', 'fp-login-textarea', 'fp-login-select' and + * 'fp-login-input' are templates for displaying respective login form elements. Inside + * there must be exactly one element with type

      +
      +
      +'; + return preg_replace('/\{\!\}/', '', $rv); + } + + /** + * Returns all FilePicker JavaScript templates as an array. + * + * @return array + */ + public function filepicker_js_templates() { + $class_methods = get_class_methods($this); + $templates = array(); + foreach ($class_methods as $method_name) { + if (preg_match('/^fp_js_template_(.*)$/', $method_name, $matches)) + $templates[$matches[1]] = $this->$method_name(); + } + return $templates; + } + + /** + * Returns HTML for default repository searchform to be passed to Filepicker + * + * This will be used as contents for search form defined in generallayout template + * (form with id {TOOLSEARCHID}). + * Default contents is one text input field with name="s" + */ + public function repository_default_searchform() { + $str = ''; + return $str; + } +} /** * Data structure representing a general moodle file tree viewer @@ -148,8 +958,9 @@ class files_tree_viewer implements renderable { $fileitem = array( 'params' => $params, 'filename' => $child->get_visible_name(), - 'filedate' => $filedate ? userdate($filedate) : '', - 'filesize' => $filesize ? display_size($filesize) : '' + 'mimetype' => $child->get_mimetype(), + 'filedate' => $filedate ? $filedate : '', + 'filesize' => $filesize ? $filesize : '' ); $url = new moodle_url('/files/index.php', $params); if ($child->is_directory()) { diff --git a/grade/lib.php b/grade/lib.php index 185fed52039..e1937be2ce7 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -1049,6 +1049,7 @@ class grade_structure { */ public function get_element_icon(&$element, $spacerifnone=false) { global $CFG, $OUTPUT; + require_once $CFG->libdir.'/filelib.php'; switch ($element['type']) { case 'item': @@ -1114,7 +1115,7 @@ class grade_structure { case 'category': $strcat = get_string('category', 'grades'); - return ''.s($strcat).''; } diff --git a/lang/en/backup.php b/lang/en/backup.php index 3cb7ec138f9..fa809254ddb 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -117,6 +117,9 @@ $string['errorinvalidformat'] = 'Unknown backup format'; $string['errorinvalidformatinfo'] = 'The selected file is not a valid Moodle backup file and can\'t be restored.'; $string['executionsuccess'] = 'The backup file was successfully created.'; $string['filename'] = 'Filename'; +$string['filereferencesincluded'] = 'File references to external contents included in backup package, they won\'t work on other sites.'; +$string['filereferencessamesite'] = 'Backup is from the same site, file references can be restored'; +$string['filereferencesnotsamesite'] = 'Backup is from other site, file references cannot be restored'; $string['generalactivities'] = 'Include activities'; $string['generalanonymize'] = 'Anonymise information'; $string['generalbackdefaults'] = 'General backup defaults'; @@ -148,6 +151,7 @@ $string['includeditems'] = 'Included items:'; $string['includesection'] = 'Section {$a}'; $string['includeuserinfo'] = 'User data'; $string['loglifetime'] = 'Keep logs for'; +$string['includefilereferences'] = 'File references to external contents'; $string['locked'] = 'Locked'; $string['lockedbypermission'] = 'You don\'t have sufficient permissions to change this setting'; $string['lockedbyconfig'] = 'This setting has been locked by the default backup settings'; diff --git a/lang/en/error.php b/lang/en/error.php index 75fb0be028c..b40859b4e18 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -224,6 +224,7 @@ $string['errorsettinguserpref'] = 'Error setting user preference'; $string['errorunzippingfiles'] = 'Error unzipping files'; $string['expiredkey'] = 'Expired key'; $string['externalauthpassworderror'] = 'Non-empty password for external authentication'; +$string['externalfilenolocation'] = 'External file has no location path'; $string['failtoloadblocks'] = 'One or more blocks are registered in the database, but they all failed to load!'; $string['fieldrequired'] = '"{$a}" is a required field'; $string['fileexists'] = 'File exists'; @@ -323,6 +324,7 @@ $string['invalidsection'] = 'Course module record contains invalid section'; $string['invalidsesskey'] = 'Incorrect sesskey submitted, form not accepted!'; $string['invalidshortname'] = 'That\'s an invalid short course name'; $string['invalidstatedetected'] = 'Something has gone wrong: {$a}. This should never normally happen.'; +$string['invalidsourcefield'] = 'Draft file\'s source field is invalid'; $string['invalidurl'] = 'Invalid URL'; $string['invaliduser'] = 'Invalid user'; $string['invaliduserid'] = 'Invalid user id'; diff --git a/lang/en/mimetypes.php b/lang/en/mimetypes.php index 17d224e7c4d..9cb966dd3cb 100644 --- a/lang/en/mimetypes.php +++ b/lang/en/mimetypes.php @@ -18,6 +18,19 @@ /** * Strings for component 'mimetypes', language 'en', branch 'MOODLE_20_STABLE' * + * Strings are used to display human-readable name of mimetype. Some mimetypes share the same + * string. The following attributes are passed in the parameter when processing the string: + * $a->ext - filename extension in lower case + * $a->EXT - filename extension, capitalized + * $a->Ext - filename extension with first capital letter + * $a->mimetype - file mimetype + * $a->mimetype1 - first chunk of mimetype (before /) + * $a->mimetype2 - second chunk of mimetype (after /) + * $a->Mimetype, $a->MIMETYPE, $a->Mimetype1, $a->Mimetype2, $a->MIMETYPE1, $a->MIMETYPE2 + * - the same with capitalized first/all letters + * + * @see get_mimetypes_array() + * @see get_mimetype_description() * @package mimetypes * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -25,15 +38,19 @@ $string['application/msword'] = 'Word document'; $string['application/pdf'] = 'PDF document'; +$string['application/vnd.moodle.backup'] = 'Moodle backup'; $string['application/vnd.ms-excel'] = 'Excel spreadsheet'; $string['application/vnd.ms-powerpoint'] = 'Powerpoint presentation'; -$string['application/zip'] = 'zip archive'; -$string['audio/mp3'] = 'MP3 audio file'; -$string['audio/wav'] = 'sound file'; -$string['document/unknown'] = 'file'; -$string['image/bmp'] = 'uncompressed BMP image'; -$string['image/gif'] = 'GIF image'; -$string['image/jpeg'] = 'JPEG image'; -$string['image/png'] = 'PNG image'; -$string['text/plain'] = 'text file'; +$string['application/vnd.openxmlformats-officedocument.presentationml.presentation'] = 'Powerpoint presentation'; +$string['application/vnd.openxmlformats-officedocument.presentationml.slideshow'] = 'Powerpoint slideshow'; +$string['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] = 'Excel spreadsheet'; +$string['application/vnd.openxmlformats-officedocument.spreadsheetml.template'] = 'Excel template'; +$string['application/vnd.openxmlformats-officedocument.wordprocessingml.document'] = 'Word document'; +$string['archive'] = 'Archive ({$a->EXT})'; +$string['audio'] = 'Audio file ({$a->EXT})'; +$string['default'] = '{$a->mimetype}'; +$string['document/unknown'] = 'File'; +$string['image'] = 'Image ({$a->MIMETYPE2})'; +$string['text/html'] = 'HTML document'; +$string['text/plain'] = 'Text file'; $string['text/rtf'] = 'RTF document'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 64ae940b588..073d3eb405f 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1249,6 +1249,7 @@ $string['olduserdirectory'] = 'This is the OLD users directory, and is no longer $string['opentoguests'] = 'Guest access'; $string['optional'] = 'optional'; $string['order'] = 'Order'; +$string['originalpath'] = 'Original path'; $string['orphanedactivities'] = 'Orphaned activities'; $string['other'] = 'Other'; $string['outline'] = 'Outline'; diff --git a/lang/en/repository.php b/lang/en/repository.php index 0e84fcd4c64..42c8658591e 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -61,9 +61,15 @@ $string['commonrepositorysettings'] = 'Common repository settings'; $string['configallowexternallinks'] = 'This option enables all users to choose whether or not external media is copied into Moodle or not. If this is off then media is always copied into Moodle (this is usually best for overall data integrity and security). If this is on then users can choose each time they add media to a text.'; $string['configcacheexpire'] = 'The amount of time that file listings are cached locally (in seconds) when browsing external repositories.'; $string['configsaved'] = 'Configuration saved!'; -$string['confirmdelete'] = 'Are you sure you want to delete this repository - {$a}?'; +$string['confirmdelete'] = 'Are you sure you want to delete this repository - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to moodle, but it could take long time to process.'; $string['confirmdeletefile'] = 'Are you sure you want to delete this file?'; -$string['confirmremove'] = 'Are you sure you want to remove this repository plugin, its options and all of its instances - {$a}?'; +$string['confirmrenamefile'] = 'Are you sure you want to rename/move this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.'; +$string['confirmdeletefilewithhref'] = 'Are you sure you want to delete this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.'; +$string['confirmdeletefolder'] = 'Are you sure you want to delete this folder? All files and subfolders will be deleted.'; +$string['confirmremove'] = 'Are you sure you want to remove this repository plugin, its options and all of its instances - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to moodle, but it could take long time to process.'; +$string['confirmrenamefolder'] = ' Are you sure you want to move/rename this folder? Any alias/shortcut files that reference files in this folder will be converted into true copies.'; +$string['continueuninstall'] = 'Continue'; +$string['continueuninstallanddownload'] = 'Continue and download'; $string['copying'] = 'Copying'; $string['create'] = 'Create'; $string['createfolderfail'] = 'Fail to create this folder'; @@ -72,8 +78,11 @@ $string['createinstance'] = 'Create a repository instance'; $string['createrepository'] = 'Create a repository instance'; $string['createxxinstance'] = 'Create "{$a}" instance'; $string['date'] = 'Date'; +$string['datecreated'] = 'Created'; $string['deleted'] = 'Repository deleted'; $string['deleterepository'] = 'Delete this repository'; +$string['detailview'] = 'View details'; +$string['dimensions'] = 'Dimensions'; $string['disabled'] = 'Disabled'; $string['download'] = 'Download'; $string['downloadfolder'] = 'Download all'; @@ -102,11 +111,15 @@ $string['filenotnull'] = 'You must select a file to upload.'; $string['filesaved'] = 'The file has been saved'; $string['filepicker'] = 'File picker'; $string['filesizenull'] = 'File size cannot be determined'; +$string['folderexists'] = 'Folder name already being used, please use another name'; +$string['foldernotfound'] = 'Folder not found'; +$string['folderrecurse'] = 'Folder can not be moved to it\s own subfolder'; $string['getfile'] = 'Select this file'; $string['hidden'] = 'Hidden'; $string['choosealink'] = 'Choose a link...'; $string['chooselicense'] = 'Choose license'; $string['iconview'] = 'View as icons'; +$string['imagesize'] = '{$a->width} x {$a->height} px'; $string['instance'] = 'instance'; $string['instancedeleted'] = 'Instance deleted'; $string['instances'] = 'Repository instances'; @@ -117,6 +130,7 @@ $string['invalidjson'] = 'Invalid JSON string'; $string['invalidplugin'] = 'Invalid repository {$a} plug-in'; $string['invalidfiletype'] = '{$a} filetype cannot be accepted.'; $string['invalidrepositoryid'] = 'Invalid repository ID'; +$string['invalidparams'] = 'Invalid parameters'; $string['isactive'] = 'Active?'; $string['keyword'] = 'Keyword'; $string['linkexternal'] = 'Link external'; @@ -124,6 +138,9 @@ $string['listview'] = 'View as list'; $string['loading'] = 'Loading...'; $string['login'] = 'Login'; $string['logout'] = 'Logout'; +$string['makefileinternal'] = 'Make a copy of the file'; +$string['makefilelink'] = 'Link to the file directly'; +$string['makefilereference'] = 'Create an alias/shortcut to the file'; $string['manage'] = 'Manage repositories'; $string['manageurl'] = 'Manage'; $string['manageuserrepository'] = 'Manage individual repository'; @@ -139,6 +156,7 @@ $string['norepositoriesavailable'] = 'Sorry, none of your current repositories c $string['norepositoriesexternalavailable'] = 'Sorry, none of your current repositories can return external files.'; $string['notyourinstances'] = 'You can not view/edit repository instances of another user'; $string['off'] = 'Enabled but hidden'; +$string['original'] = 'Original'; $string['openpicker'] = 'Choose a file...'; $string['operation'] = 'Operation'; $string['on'] = 'Enabled and visible'; @@ -150,10 +168,12 @@ $string['popup'] = 'Click "Login" button to login'; $string['popupblockeddownload'] = 'The downloading window is blocked, please allow the popup window, and try again.'; $string['preview'] = 'Preview'; $string['readonlyinstance'] = 'You cannot edit/delete a read-only instance'; +$string['referencesexist'] = 'There are {$a} alias/shortcut files that use this file as their source'; +$string['referenceslist'] = 'Aliases/Shortcuts'; $string['refresh'] = 'Refresh'; $string['refreshnonjsfilepicker'] = 'Please close this window and refresh non-javascript file picker'; $string['removed'] = 'Repository removed'; -$string['renameto'] = 'Rename to'; +$string['renameto'] = 'Rename to "{$a}"'; $string['repositories'] = 'Repositories'; $string['repository'] = 'Repository'; $string['repositorycourse'] = 'Course repositories'; @@ -175,10 +195,14 @@ $string['submit'] = 'Submit'; $string['sync'] = 'Sync'; $string['thumbview'] = 'View as icons'; $string['title'] = 'Choose a file...'; +$string['type'] = 'Type'; $string['typenotvisible'] = 'Type not visible'; +$string['unknownoriginal'] = 'Unknown'; $string['upload'] = 'Upload this file'; $string['uploading'] = 'Uploading...'; $string['uploadsucc'] = 'The file has been uploaded successfully'; +$string['undisclosedreference'] = '(Undisclosed)'; +$string['uselatestfile'] = 'Use latest file'; $string['usercontextrepositorydisabled'] = 'You cannot edit this repository in user context'; $string['usenonjsfilemanager'] = 'Open file manager in new window'; $string['usenonjsfilepicker'] = 'Open file picker in new window'; diff --git a/lib/cronlib.php b/lib/cronlib.php index b05e34f87c9..0e47a655c32 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -403,9 +403,7 @@ function cron_run() { cron_execute_plugin_type('format', 'course formats'); cron_execute_plugin_type('profilefield', 'profile fields'); cron_execute_plugin_type('webservice', 'webservices'); - // TODO: Repository lib.php files are messed up (include many other files, etc), so it is - // currently not possible to implement repository plugin cron using this infrastructure - // cron_execute_plugin_type('repository', 'repository plugins'); + cron_execute_plugin_type('repository', 'repository plugins'); cron_execute_plugin_type('qbehaviour', 'question behaviours'); cron_execute_plugin_type('qformat', 'question import/export formats'); cron_execute_plugin_type('qtype', 'question types'); @@ -462,6 +460,9 @@ function cron_run() { $fs = get_file_storage(); $fs->cron(); + mtrace("Clean up cached external files"); + // 1 week + cache_file::cleanup(array(), 60 * 60 * 24 * 7); mtrace("Cron script completed correctly"); diff --git a/lib/db/install.xml b/lib/db/install.xml index 1d83d84ead5..0463430e996 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -2326,7 +2326,7 @@ - +
      @@ -2346,12 +2346,16 @@ - + + + + - + + @@ -2359,7 +2363,20 @@
      - +
      + + + + + + + + + + + +
      + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index b263a39e735..c74c7248216 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -592,5 +592,61 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2012051100.03); } + if ($oldversion < 2012052100.00) { + + // Define field referencefileid to be added to files. + $table = new xmldb_table('files'); + + // Define field referencefileid to be added to files. + $field = new xmldb_field('referencefileid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'sortorder'); + + // Conditionally launch add field referencefileid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field referencelastsync to be added to files. + $field = new xmldb_field('referencelastsync', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'referencefileid'); + + // Conditionally launch add field referencelastsync. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field referencelifetime to be added to files. + $field = new xmldb_field('referencelifetime', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'referencelastsync'); + + // Conditionally launch add field referencelifetime. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $key = new xmldb_key('referencefileid', XMLDB_KEY_FOREIGN, array('referencefileid'), 'files_reference', array('id')); + // Launch add key referencefileid + $dbman->add_key($table, $key); + + // Define table files_reference to be created. + $table = new xmldb_table('files_reference'); + + // Adding fields to table files_reference. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('repositoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('lastsync', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('lifetime', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('reference', XMLDB_TYPE_TEXT, null, null, null, null, null); + + // Adding keys to table files_reference. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('repositoryid', XMLDB_KEY_FOREIGN, array('repositoryid'), 'repository_instances', array('id')); + + // Conditionally launch create table for files_reference + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached + upgrade_main_savepoint(true, 2012052100.00); + } + return true; } diff --git a/lib/filebrowser/file_info.php b/lib/filebrowser/file_info.php index 44069114762..e53640b3076 100644 --- a/lib/filebrowser/file_info.php +++ b/lib/filebrowser/file_info.php @@ -226,6 +226,20 @@ abstract class file_info { return 0; } + /** + * Returns the localised human-readable name of the file together with + * virtual path + * + * @return string + */ + public function get_readable_fullname() { + $fpath = array(); + for ($parent = $this; $parent; $parent = $parent->get_parent()) { + array_unshift($fpath, $parent->get_visible_name()); + } + return join('/', $fpath); + } + /** * Create new directory, may throw exception - make sure * params are valid. diff --git a/lib/filebrowser/file_info_context_user.php b/lib/filebrowser/file_info_context_user.php index 48d76e19643..badd5a39b57 100644 --- a/lib/filebrowser/file_info_context_user.php +++ b/lib/filebrowser/file_info_context_user.php @@ -201,7 +201,7 @@ class file_info_context_user extends file_info { return null; } } - $urlbase = $CFG->wwwroot.'/pluginfile.php'; + $urlbase = $CFG->wwwroot.'/draftfile.php'; return new file_info_stored($this->browser, $this->context, $storedfile, $urlbase, get_string('areauserdraft', 'repository'), true, true, true, true); } diff --git a/lib/filebrowser/file_info_stored.php b/lib/filebrowser/file_info_stored.php index 787787ee6a0..a5a37a3d908 100644 --- a/lib/filebrowser/file_info_stored.php +++ b/lib/filebrowser/file_info_stored.php @@ -187,6 +187,16 @@ class file_info_stored extends file_info { return $this->lf->get_filesize(); } + /** + * Returns width, height and mimetype of the stored image, or false + * + * @see stored_file::get_imageinfo() + * @return array|false + */ + public function get_imageinfo() { + return $this->lf->get_imageinfo(); + } + /** * Returns mimetype * diff --git a/lib/filelib.php b/lib/filelib.php index f10d8ca5d63..b3fa8f24dc5 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -374,7 +374,25 @@ function file_prepare_draft_area(&$draftitemid, $contextid, $component, $fileare if (!$options['subdirs'] and ($file->is_directory() or $file->get_filepath() !== '/')) { continue; } - $fs->create_file_from_storedfile($file_record, $file); + $draftfile = $fs->create_file_from_storedfile($file_record, $file); + // XXX: This is a hack for file manager (MDL-28666) + // File manager needs to know the original file information before copying + // to draft area, so we append these information in mdl_files.source field + // {@link file_storage::search_references()} + // {@link file_storage::search_references_count()} + $sourcefield = $file->get_source(); + $newsourcefield = new stdClass; + $newsourcefield->source = $sourcefield; + $original = new stdClass; + $original->contextid = $contextid; + $original->component = $component; + $original->filearea = $filearea; + $original->itemid = $itemid; + $original->filename = $file->get_filename(); + $original->filepath = $file->get_filepath(); + $newsourcefield->original = file_storage::pack_reference($original); + $draftfile->set_source(serialize($newsourcefield)); + // End of file manager hack } } if (!is_null($text)) { @@ -548,13 +566,13 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') { $data->path[] = array('name'=>get_string('files'), 'path'=>'/'); // will be used to build breadcrumb - $trail = ''; + $trail = '/'; if ($filepath !== '/') { $filepath = file_correct_filepath($filepath); $parts = explode('/', $filepath); foreach ($parts as $part) { if ($part != '' && $part != null) { - $trail .= ('/'.$part.'/'); + $trail .= ($part.'/'); $data->path[] = array('name'=>$part, 'path'=>$trail); } } @@ -569,27 +587,47 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') { $item->filepath = $file->get_filepath(); $item->fullname = trim($item->filename, '/'); $filesize = $file->get_filesize(); + $item->size = $filesize ? $filesize : null; $item->filesize = $filesize ? display_size($filesize) : ''; - $icon = mimeinfo_from_type('icon', $file->get_mimetype()); - $item->icon = $OUTPUT->pix_url('f/' . $icon)->out(); $item->sortorder = $file->get_sortorder(); - - if ($icon == 'zip') { - $item->type = 'zip'; - } else { - $item->type = 'file'; + $item->author = $file->get_author(); + $item->license = $file->get_license(); + $item->datemodified = $file->get_timemodified(); + $item->datecreated = $file->get_timecreated(); + $item->isref = $file->is_external_file(); + // find the file this draft file was created from and count all references in local + // system pointing to that file + $source = unserialize($file->get_source()); + if (isset($source->original)) { + $item->refcount = $fs->search_references_count($source->original); } if ($file->is_directory()) { $item->filesize = 0; - $item->icon = $OUTPUT->pix_url('f/folder')->out(); + $item->icon = $OUTPUT->pix_url(file_folder_icon(24))->out(false); $item->type = 'folder'; $foldername = explode('/', trim($item->filepath, '/')); $item->fullname = trim(array_pop($foldername), '/'); + $item->thumbnail = $OUTPUT->pix_url(file_folder_icon(90))->out(false); } else { // do NOT use file browser here! - $item->url = moodle_url::make_draftfile_url($draftitemid, $item->filepath, $item->filename)->out(); + $item->mimetype = get_mimetype_description($file); + if (file_mimetype_in_typegroup($item->mimetype, 'archive')) { + $item->type = 'zip'; + } else { + $item->type = 'file'; + } + $itemurl = moodle_url::make_draftfile_url($draftitemid, $item->filepath, $item->filename); + $item->url = $itemurl->out(); + $item->icon = $OUTPUT->pix_url(file_file_icon($file, 24))->out(false); + $item->thumbnail = $OUTPUT->pix_url(file_file_icon($file, 90))->out(false); + if ($imageinfo = $file->get_imageinfo()) { + $item->realthumbnail = $itemurl->out(false, array('preview' => 'thumb', 'oid' => $file->get_timemodified())); + $item->realicon = $itemurl->out(false, array('preview' => 'tinyicon', 'oid' => $file->get_timemodified())); + $item->image_width = $imageinfo['width']; + $item->image_height = $imageinfo['height']; + } } $list[] = $item; } @@ -631,6 +669,24 @@ function file_get_submitted_draft_itemid($elname) { return $param; } +/** + * Restore the original source field from draft files + * + * @param stored_file $storedfile This only works with draft files + * @return stored_file + */ +function file_restore_source_field_from_draft_file($storedfile) { + $source = unserialize($storedfile->get_source()); + if (!empty($source)) { + if (is_object($source)) { + $restoredsource = $source->source; + $storedfile->set_source($restoredsource); + } else { + throw new moodle_exception('invalidsourcefield', 'error'); + } + } + return $storedfile; +} /** * 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. @@ -694,6 +750,16 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea if (!$file->is_directory()) { $filecount++; } + + if ($file->is_external_file()) { + $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); } @@ -715,14 +781,50 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea $oldfile->delete(); continue; } + $newfile = $newhashes[$oldhash]; - if ($oldfile->get_contenthash() != $newfile->get_contenthash() or $oldfile->get_sortorder() != $newfile->get_sortorder() - or $oldfile->get_status() != $newfile->get_status() or $oldfile->get_license() != $newfile->get_license() - or $oldfile->get_author() != $newfile->get_author() or $oldfile->get_source() != $newfile->get_source()) { + // 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; } + + file_restore_source_field_from_draft_file($newfile); + // Replaced file content + if ($oldfile->get_contenthash() != $newfile->get_contenthash()) { + $oldfile->replace_content_with($newfile); + } + // 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 size + if ($oldfile->get_filesize() != $newfile->get_filesize()) { + $oldfile->set_filesize($newfile->get_filesize()); + } + + // Update file timemodified + if ($oldfile->get_timemodified() != $newfile->get_timemodified()) { + $oldfile->set_timemodified($newfile->get_timemodified()); + } + // unchanged file or directory - we keep it as is unset($newhashes[$oldhash]); if (!$oldfile->is_directory()) { @@ -730,7 +832,7 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea } } - // now add new/changed files + // 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) { if (!$options['subdirs']) { @@ -749,6 +851,15 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea if (!$file->is_directory()) { $filecount++; } + + 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); } } @@ -1210,7 +1321,32 @@ function download_file_content_write_handler($received, $ch, $data) { } /** - * Returns a list of information about file t ypes based on extensions + * Returns a list of information about file types based on extensions. + * + * The following elements expected in value array for each extension: + * 'type' - mimetype + * 'icon' - location of the icon file. If value is FILENAME, then either pix/f/FILENAME.gif + * or pix/f/FILENAME.png must be present in moodle and contain 16x16 filetype icon; + * also files with bigger sizes under names + * FILENAME-24, FILENAME-32, FILENAME-64, FILENAME-128, FILENAME-256 are recommended. + * 'groups' (optional) - array of filetype groups this filetype extension is part of; + * commonly used in moodle the following groups: + * - web_image - image that can be included as in HTML + * - image - image that we can parse using GD to find it's dimensions, also used for portfolio format + * - video - file that can be imported as video in text editor + * - audio - file that can be imported as audio in text editor + * - archive - we can extract files from this archive + * - spreadsheet - used for portfolio format + * - document - used for portfolio format + * - presentation - used for portfolio format + * 'string' (optional) - the name of the string from lang/en/mimetypes.php that displays + * human-readable description for this filetype; + * Function {@link get_mimetype_description()} first looks at the presence of string for + * particular mimetype (value of 'type'), if not found looks for string specified in 'string' + * attribute, if not found returns the value of 'type'; + * 'defaulticon' (boolean, optional) - used by function {@link file_mimetype_icon()} to find + * an icon for mimetype. If an entry with 'defaulticon' is not found for a particular mimetype, + * this function will return first found icon; Especially usefull for types such as 'text/plain' * * @category files * @return array List of information about file types based on extensions. @@ -1218,179 +1354,179 @@ function download_file_content_write_handler($received, $ch, $data) { * from 'element name' to data. Current element names are 'type' and 'icon'. * Unknown types should use the 'xxx' entry which includes defaults. */ -function get_mimetypes_array() { +function &get_mimetypes_array() { static $mimearray = array ( 'xxx' => array ('type'=>'document/unknown', 'icon'=>'unknown'), - '3gp' => array ('type'=>'video/quicktime', 'icon'=>'video'), - 'aac' => array ('type'=>'audio/aac', 'icon'=>'audio'), - 'ai' => array ('type'=>'application/postscript', 'icon'=>'image'), - 'aif' => array ('type'=>'audio/x-aiff', 'icon'=>'audio'), - 'aiff' => array ('type'=>'audio/x-aiff', 'icon'=>'audio'), - 'aifc' => array ('type'=>'audio/x-aiff', 'icon'=>'audio'), + '3gp' => array ('type'=>'video/quicktime', 'icon'=>'mov', 'groups'=>array('video'), 'string'=>'video'), + 'aac' => array ('type'=>'audio/aac', 'icon'=>'mp3', 'groups'=>array('audio'), 'string'=>'audio'), + 'ai' => array ('type'=>'application/postscript', 'icon'=>'eps', 'groups'=>array('image'), 'string'=>'image'), + 'aif' => array ('type'=>'audio/x-aiff', 'icon'=>'audio', 'groups'=>array('audio'), 'string'=>'audio'), + 'aiff' => array ('type'=>'audio/x-aiff', 'icon'=>'audio', 'groups'=>array('audio'), 'string'=>'audio'), + 'aifc' => array ('type'=>'audio/x-aiff', 'icon'=>'audio', 'groups'=>array('audio'), 'string'=>'audio'), 'applescript' => array ('type'=>'text/plain', 'icon'=>'text'), 'asc' => array ('type'=>'text/plain', 'icon'=>'text'), 'asm' => array ('type'=>'text/plain', 'icon'=>'text'), - 'au' => array ('type'=>'audio/au', 'icon'=>'audio'), - 'avi' => array ('type'=>'video/x-ms-wm', 'icon'=>'avi'), - 'bmp' => array ('type'=>'image/bmp', 'icon'=>'image'), + 'au' => array ('type'=>'audio/au', 'icon'=>'audio', 'groups'=>array('audio'), 'string'=>'audio'), + 'avi' => array ('type'=>'video/x-ms-wm', 'icon'=>'avi', 'groups'=>array('video'), 'string'=>'video'), + 'bmp' => array ('type'=>'image/bmp', 'icon'=>'bmp', 'groups'=>array('image'), 'string'=>'image'), 'c' => array ('type'=>'text/plain', 'icon'=>'text'), 'cct' => array ('type'=>'shockwave/director', 'icon'=>'flash'), 'cpp' => array ('type'=>'text/plain', 'icon'=>'text'), 'cs' => array ('type'=>'application/x-csh', 'icon'=>'text'), - 'css' => array ('type'=>'text/css', 'icon'=>'text'), - 'csv' => array ('type'=>'text/csv', 'icon'=>'excel'), - 'dv' => array ('type'=>'video/x-dv', 'icon'=>'video'), + 'css' => array ('type'=>'text/css', 'icon'=>'text', 'groups'=>array('web_file')), + 'csv' => array ('type'=>'text/csv', 'icon'=>'xlsx', 'groups'=>array('spreadsheet')), + 'dv' => array ('type'=>'video/x-dv', 'icon'=>'mov', 'groups'=>array('video'), 'string'=>'video'), 'dmg' => array ('type'=>'application/octet-stream', 'icon'=>'dmg'), - 'doc' => array ('type'=>'application/msword', 'icon'=>'word'), - 'docx' => array ('type'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'icon'=>'docx'), - 'docm' => array ('type'=>'application/vnd.ms-word.document.macroEnabled.12', 'icon'=>'docm'), - 'dotx' => array ('type'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'icon'=>'dotx'), - 'dotm' => array ('type'=>'application/vnd.ms-word.template.macroEnabled.12', 'icon'=>'dotm'), + 'doc' => array ('type'=>'application/msword', 'icon'=>'docx', 'groups'=>array('document')), + 'docx' => array ('type'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'icon'=>'docx', 'groups'=>array('document')), + 'docm' => array ('type'=>'application/vnd.ms-word.document.macroEnabled.12', 'icon'=>'docx'), + 'dotx' => array ('type'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'icon'=>'docx'), + 'dotm' => array ('type'=>'application/vnd.ms-word.template.macroEnabled.12', 'icon'=>'docx'), 'dcr' => array ('type'=>'application/x-director', 'icon'=>'flash'), - 'dif' => array ('type'=>'video/x-dv', 'icon'=>'video'), + 'dif' => array ('type'=>'video/x-dv', 'icon'=>'video', 'groups'=>array('mov'), 'string'=>'video'), 'dir' => array ('type'=>'application/x-director', 'icon'=>'flash'), 'dxr' => array ('type'=>'application/x-director', 'icon'=>'flash'), - 'eps' => array ('type'=>'application/postscript', 'icon'=>'pdf'), + 'eps' => array ('type'=>'application/postscript', 'icon'=>'eps'), 'fdf' => array ('type'=>'application/pdf', 'icon'=>'pdf'), - 'flv' => array ('type'=>'video/x-flv', 'icon'=>'video'), - 'f4v' => array ('type'=>'video/mp4', 'icon'=>'video'), - 'gif' => array ('type'=>'image/gif', 'icon'=>'image'), - 'gtar' => array ('type'=>'application/x-gtar', 'icon'=>'zip'), - 'tgz' => array ('type'=>'application/g-zip', 'icon'=>'zip'), - 'gz' => array ('type'=>'application/g-zip', 'icon'=>'zip'), - 'gzip' => array ('type'=>'application/g-zip', 'icon'=>'zip'), + 'flv' => array ('type'=>'video/x-flv', 'icon'=>'flash', 'groups'=>array('video'), 'string'=>'video'), + 'f4v' => array ('type'=>'video/mp4', 'icon'=>'flash', 'groups'=>array('video'), 'string'=>'video'), + 'gif' => array ('type'=>'image/gif', 'icon'=>'gif', 'groups'=>array('image', 'web_image'), 'string'=>'image'), + 'gtar' => array ('type'=>'application/x-gtar', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive'), + 'tgz' => array ('type'=>'application/g-zip', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive'), + 'gz' => array ('type'=>'application/g-zip', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive'), + 'gzip' => array ('type'=>'application/g-zip', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive'), 'h' => array ('type'=>'text/plain', 'icon'=>'text'), 'hpp' => array ('type'=>'text/plain', 'icon'=>'text'), - 'hqx' => array ('type'=>'application/mac-binhex40', 'icon'=>'zip'), - 'htc' => array ('type'=>'text/x-component', 'icon'=>'text'), - 'html' => array ('type'=>'text/html', 'icon'=>'html'), + 'hqx' => array ('type'=>'application/mac-binhex40', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive'), + 'htc' => array ('type'=>'text/x-component', 'icon'=>'html'), + 'html' => array ('type'=>'text/html', 'icon'=>'html', 'groups'=>array('web_file')), 'xhtml'=> array ('type'=>'application/xhtml+xml', 'icon'=>'html'), - 'htm' => array ('type'=>'text/html', 'icon'=>'html'), - 'ico' => array ('type'=>'image/vnd.microsoft.icon', 'icon'=>'image'), + 'htm' => array ('type'=>'text/html', 'icon'=>'html', 'groups'=>array('web_file')), + 'ico' => array ('type'=>'image/vnd.microsoft.icon', 'icon'=>'image', 'groups'=>array('image'), 'string'=>'image'), 'ics' => array ('type'=>'text/calendar', 'icon'=>'text'), 'isf' => array ('type'=>'application/inspiration', 'icon'=>'isf'), 'ist' => array ('type'=>'application/inspiration.template', 'icon'=>'isf'), 'java' => array ('type'=>'text/plain', 'icon'=>'text'), - 'jcb' => array ('type'=>'text/xml', 'icon'=>'jcb'), - 'jcl' => array ('type'=>'text/xml', 'icon'=>'jcl'), - 'jcw' => array ('type'=>'text/xml', 'icon'=>'jcw'), - 'jmt' => array ('type'=>'text/xml', 'icon'=>'jmt'), - 'jmx' => array ('type'=>'text/xml', 'icon'=>'jmx'), - 'jpe' => array ('type'=>'image/jpeg', 'icon'=>'image'), - 'jpeg' => array ('type'=>'image/jpeg', 'icon'=>'image'), - 'jpg' => array ('type'=>'image/jpeg', 'icon'=>'image'), - 'jqz' => array ('type'=>'text/xml', 'icon'=>'jqz'), - 'js' => array ('type'=>'application/x-javascript', 'icon'=>'text'), + 'jcb' => array ('type'=>'text/xml', 'icon'=>'text'), + 'jcl' => array ('type'=>'text/xml', 'icon'=>'text'), + 'jcw' => array ('type'=>'text/xml', 'icon'=>'text'), + 'jmt' => array ('type'=>'text/xml', 'icon'=>'text'), + 'jmx' => array ('type'=>'text/xml', 'icon'=>'text'), + 'jpe' => array ('type'=>'image/jpeg', 'icon'=>'jpeg', 'groups'=>array('image', 'web_image'), 'string'=>'image'), + 'jpeg' => array ('type'=>'image/jpeg', 'icon'=>'jpeg', 'groups'=>array('image', 'web_image'), 'string'=>'image'), + 'jpg' => array ('type'=>'image/jpeg', 'icon'=>'jpeg', 'groups'=>array('image', 'web_image'), 'string'=>'image'), + 'jqz' => array ('type'=>'text/xml', 'icon'=>'xml'), + 'js' => array ('type'=>'application/x-javascript', 'icon'=>'text', 'groups'=>array('web_file')), 'latex'=> array ('type'=>'application/x-latex', 'icon'=>'text'), 'm' => array ('type'=>'text/plain', 'icon'=>'text'), 'mbz' => array ('type'=>'application/vnd.moodle.backup', 'icon'=>'moodle'), - 'mov' => array ('type'=>'video/quicktime', 'icon'=>'video'), - 'movie'=> array ('type'=>'video/x-sgi-movie', 'icon'=>'video'), - 'm3u' => array ('type'=>'audio/x-mpegurl', 'icon'=>'audio'), - 'mp3' => array ('type'=>'audio/mp3', 'icon'=>'audio'), - 'mp4' => array ('type'=>'video/mp4', 'icon'=>'video'), - 'm4v' => array ('type'=>'video/mp4', 'icon'=>'video'), - 'm4a' => array ('type'=>'audio/mp4', 'icon'=>'audio'), - 'mpeg' => array ('type'=>'video/mpeg', 'icon'=>'video'), - 'mpe' => array ('type'=>'video/mpeg', 'icon'=>'video'), - 'mpg' => array ('type'=>'video/mpeg', 'icon'=>'video'), + 'mov' => array ('type'=>'video/quicktime', 'icon'=>'mov', 'groups'=>array('video'), 'string'=>'video'), + 'movie'=> array ('type'=>'video/x-sgi-movie', 'icon'=>'mov', 'groups'=>array('video'), 'string'=>'video'), + 'm3u' => array ('type'=>'audio/x-mpegurl', 'icon'=>'mp3', 'groups'=>array('audio'), 'string'=>'audio'), + 'mp3' => array ('type'=>'audio/mp3', 'icon'=>'mp3', 'groups'=>array('audio'), 'string'=>'audio'), + 'mp4' => array ('type'=>'video/mp4', 'icon'=>'mpeg', 'groups'=>array('video'), 'string'=>'video'), + 'm4v' => array ('type'=>'video/mp4', 'icon'=>'mpeg', 'groups'=>array('video'), 'string'=>'video'), + 'm4a' => array ('type'=>'audio/mp4', 'icon'=>'mp3', 'groups'=>array('audio'), 'string'=>'audio'), + 'mpeg' => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video'), 'string'=>'video'), + 'mpe' => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video'), 'string'=>'video'), + 'mpg' => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video'), 'string'=>'video'), - 'odt' => array ('type'=>'application/vnd.oasis.opendocument.text', 'icon'=>'odt'), - 'ott' => array ('type'=>'application/vnd.oasis.opendocument.text-template', 'icon'=>'odt'), - 'oth' => array ('type'=>'application/vnd.oasis.opendocument.text-web', 'icon'=>'odt'), - 'odm' => array ('type'=>'application/vnd.oasis.opendocument.text-master', 'icon'=>'odm'), + 'odt' => array ('type'=>'application/vnd.oasis.opendocument.text', 'icon'=>'odt', 'groups'=>array('odt')), + 'ott' => array ('type'=>'application/vnd.oasis.opendocument.text-template', 'icon'=>'odt', 'groups'=>array('odt')), + 'oth' => array ('type'=>'application/vnd.oasis.opendocument.text-web', 'icon'=>'odt', 'groups'=>array('odh')), + 'odm' => array ('type'=>'application/vnd.oasis.opendocument.text-master', 'icon'=>'odh'), 'odg' => array ('type'=>'application/vnd.oasis.opendocument.graphics', 'icon'=>'odg'), 'otg' => array ('type'=>'application/vnd.oasis.opendocument.graphics-template', 'icon'=>'odg'), 'odp' => array ('type'=>'application/vnd.oasis.opendocument.presentation', 'icon'=>'odp'), 'otp' => array ('type'=>'application/vnd.oasis.opendocument.presentation-template', 'icon'=>'odp'), - 'ods' => array ('type'=>'application/vnd.oasis.opendocument.spreadsheet', 'icon'=>'ods'), - 'ots' => array ('type'=>'application/vnd.oasis.opendocument.spreadsheet-template', 'icon'=>'ods'), + 'ods' => array ('type'=>'application/vnd.oasis.opendocument.spreadsheet', 'icon'=>'ods', 'groups'=>array('spreadsheet')), + 'ots' => array ('type'=>'application/vnd.oasis.opendocument.spreadsheet-template', 'icon'=>'ods', 'groups'=>array('spreadsheet')), 'odc' => array ('type'=>'application/vnd.oasis.opendocument.chart', 'icon'=>'odc'), 'odf' => array ('type'=>'application/vnd.oasis.opendocument.formula', 'icon'=>'odf'), 'odb' => array ('type'=>'application/vnd.oasis.opendocument.database', 'icon'=>'odb'), - 'odi' => array ('type'=>'application/vnd.oasis.opendocument.image', 'icon'=>'odi'), - 'oga' => array ('type'=>'audio/ogg', 'icon'=>'audio'), - 'ogg' => array ('type'=>'audio/ogg', 'icon'=>'audio'), - 'ogv' => array ('type'=>'video/ogg', 'icon'=>'video'), + 'odi' => array ('type'=>'application/vnd.oasis.opendocument.image', 'icon'=>'odg'), + 'oga' => array ('type'=>'audio/ogg', 'icon'=>'wma', 'groups'=>array('audio'), 'string'=>'audio'), + 'ogg' => array ('type'=>'audio/ogg', 'icon'=>'wma', 'groups'=>array('audio'), 'string'=>'audio'), + 'ogv' => array ('type'=>'video/ogg', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'), - 'pct' => array ('type'=>'image/pict', 'icon'=>'image'), + 'pct' => array ('type'=>'image/pict', 'icon'=>'image', 'groups'=>array('image'), 'string'=>'image'), 'pdf' => array ('type'=>'application/pdf', 'icon'=>'pdf'), 'php' => array ('type'=>'text/plain', 'icon'=>'text'), - 'pic' => array ('type'=>'image/pict', 'icon'=>'image'), - 'pict' => array ('type'=>'image/pict', 'icon'=>'image'), - 'png' => array ('type'=>'image/png', 'icon'=>'image'), + 'pic' => array ('type'=>'image/pict', 'icon'=>'image', 'groups'=>array('image'), 'string'=>'image'), + 'pict' => array ('type'=>'image/pict', 'icon'=>'image', 'groups'=>array('image'), 'string'=>'image'), + 'png' => array ('type'=>'image/png', 'icon'=>'png', 'groups'=>array('image', 'web_image'), 'string'=>'image'), - 'pps' => array ('type'=>'application/vnd.ms-powerpoint', 'icon'=>'powerpoint'), - 'ppt' => array ('type'=>'application/vnd.ms-powerpoint', 'icon'=>'powerpoint'), + 'pps' => array ('type'=>'application/vnd.ms-powerpoint', 'icon'=>'pptx', 'groups'=>array('presentation')), + 'ppt' => array ('type'=>'application/vnd.ms-powerpoint', 'icon'=>'pptx', 'groups'=>array('presentation')), 'pptx' => array ('type'=>'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'icon'=>'pptx'), - 'pptm' => array ('type'=>'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'icon'=>'pptm'), - 'potx' => array ('type'=>'application/vnd.openxmlformats-officedocument.presentationml.template', 'icon'=>'potx'), - 'potm' => array ('type'=>'application/vnd.ms-powerpoint.template.macroEnabled.12', 'icon'=>'potm'), - 'ppam' => array ('type'=>'application/vnd.ms-powerpoint.addin.macroEnabled.12', 'icon'=>'ppam'), - 'ppsx' => array ('type'=>'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'icon'=>'ppsx'), - 'ppsm' => array ('type'=>'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', 'icon'=>'ppsm'), + 'pptm' => array ('type'=>'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'icon'=>'pptx'), + 'potx' => array ('type'=>'application/vnd.openxmlformats-officedocument.presentationml.template', 'icon'=>'pptx'), + 'potm' => array ('type'=>'application/vnd.ms-powerpoint.template.macroEnabled.12', 'icon'=>'pptx'), + 'ppam' => array ('type'=>'application/vnd.ms-powerpoint.addin.macroEnabled.12', 'icon'=>'pptx'), + 'ppsx' => array ('type'=>'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'icon'=>'pptx'), + 'ppsm' => array ('type'=>'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', 'icon'=>'pptx'), 'ps' => array ('type'=>'application/postscript', 'icon'=>'pdf'), - 'qt' => array ('type'=>'video/quicktime', 'icon'=>'video'), - 'ra' => array ('type'=>'audio/x-realaudio-plugin', 'icon'=>'audio'), - 'ram' => array ('type'=>'audio/x-pn-realaudio-plugin', 'icon'=>'audio'), + 'qt' => array ('type'=>'video/quicktime', 'icon'=>'mov', 'groups'=>array('video'), 'string'=>'video'), + 'ra' => array ('type'=>'audio/x-realaudio-plugin', 'icon'=>'audio', 'groups'=>array('audio'), 'string'=>'audio'), + 'ram' => array ('type'=>'audio/x-pn-realaudio-plugin', 'icon'=>'audio', 'groups'=>array('audio'), 'string'=>'audio'), 'rhb' => array ('type'=>'text/xml', 'icon'=>'xml'), - 'rm' => array ('type'=>'audio/x-pn-realaudio-plugin', 'icon'=>'audio'), - 'rmvb' => array ('type'=>'application/vnd.rn-realmedia-vbr', 'icon'=>'video'), - 'rtf' => array ('type'=>'text/rtf', 'icon'=>'text'), + 'rm' => array ('type'=>'audio/x-pn-realaudio-plugin', 'icon'=>'audio', 'groups'=>array('audio'), 'string'=>'audio'), + 'rmvb' => array ('type'=>'application/vnd.rn-realmedia-vbr', 'icon'=>'video', 'groups'=>array('video'), 'string'=>'video'), + 'rtf' => array ('type'=>'text/rtf', 'icon'=>'text', 'groups'=>array('document')), 'rtx' => array ('type'=>'text/richtext', 'icon'=>'text'), - 'rv' => array ('type'=>'audio/x-pn-realaudio-plugin', 'icon'=>'video'), + 'rv' => array ('type'=>'audio/x-pn-realaudio-plugin', 'icon'=>'audio', 'groups'=>array('video'), 'string'=>'video'), 'sh' => array ('type'=>'application/x-sh', 'icon'=>'text'), - 'sit' => array ('type'=>'application/x-stuffit', 'icon'=>'zip'), + 'sit' => array ('type'=>'application/x-stuffit', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive'), 'smi' => array ('type'=>'application/smil', 'icon'=>'text'), 'smil' => array ('type'=>'application/smil', 'icon'=>'text'), 'sqt' => array ('type'=>'text/xml', 'icon'=>'xml'), - 'svg' => array ('type'=>'image/svg+xml', 'icon'=>'image'), - 'svgz' => array ('type'=>'image/svg+xml', 'icon'=>'image'), + 'svg' => array ('type'=>'image/svg+xml', 'icon'=>'image', 'groups'=>array('image'), 'string'=>'image'), + 'svgz' => array ('type'=>'image/svg+xml', 'icon'=>'image', 'groups'=>array('image'), 'string'=>'image'), 'swa' => array ('type'=>'application/x-director', 'icon'=>'flash'), 'swf' => array ('type'=>'application/x-shockwave-flash', 'icon'=>'flash'), 'swfl' => array ('type'=>'application/x-shockwave-flash', 'icon'=>'flash'), 'sxw' => array ('type'=>'application/vnd.sun.xml.writer', 'icon'=>'odt'), 'stw' => array ('type'=>'application/vnd.sun.xml.writer.template', 'icon'=>'odt'), - 'sxc' => array ('type'=>'application/vnd.sun.xml.calc', 'icon'=>'odt'), - 'stc' => array ('type'=>'application/vnd.sun.xml.calc.template', 'icon'=>'odt'), - 'sxd' => array ('type'=>'application/vnd.sun.xml.draw', 'icon'=>'odt'), - 'std' => array ('type'=>'application/vnd.sun.xml.draw.template', 'icon'=>'odt'), - 'sxi' => array ('type'=>'application/vnd.sun.xml.impress', 'icon'=>'odt'), - 'sti' => array ('type'=>'application/vnd.sun.xml.impress.template', 'icon'=>'odt'), + 'sxc' => array ('type'=>'application/vnd.sun.xml.calc', 'icon'=>'ods'), + 'stc' => array ('type'=>'application/vnd.sun.xml.calc.template', 'icon'=>'ods'), + 'sxd' => array ('type'=>'application/vnd.sun.xml.draw', 'icon'=>'odg'), + 'std' => array ('type'=>'application/vnd.sun.xml.draw.template', 'icon'=>'odg'), + 'sxi' => array ('type'=>'application/vnd.sun.xml.impress', 'icon'=>'odp'), + 'sti' => array ('type'=>'application/vnd.sun.xml.impress.template', 'icon'=>'odp'), 'sxg' => array ('type'=>'application/vnd.sun.xml.writer.global', 'icon'=>'odt'), - 'sxm' => array ('type'=>'application/vnd.sun.xml.math', 'icon'=>'odt'), + 'sxm' => array ('type'=>'application/vnd.sun.xml.math', 'icon'=>'odf'), - 'tar' => array ('type'=>'application/x-tar', 'icon'=>'zip'), - 'tif' => array ('type'=>'image/tiff', 'icon'=>'image'), - 'tiff' => array ('type'=>'image/tiff', 'icon'=>'image'), + 'tar' => array ('type'=>'application/x-tar', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive'), + 'tif' => array ('type'=>'image/tiff', 'icon'=>'tiff', 'groups'=>array('image'), 'string'=>'image'), + 'tiff' => array ('type'=>'image/tiff', 'icon'=>'tiff', 'groups'=>array('image'), 'string'=>'image'), 'tex' => array ('type'=>'application/x-tex', 'icon'=>'text'), 'texi' => array ('type'=>'application/x-texinfo', 'icon'=>'text'), 'texinfo' => array ('type'=>'application/x-texinfo', 'icon'=>'text'), 'tsv' => array ('type'=>'text/tab-separated-values', 'icon'=>'text'), - 'txt' => array ('type'=>'text/plain', 'icon'=>'text'), - 'wav' => array ('type'=>'audio/wav', 'icon'=>'audio'), - 'webm' => array ('type'=>'video/webm', 'icon'=>'video'), - 'wmv' => array ('type'=>'video/x-ms-wmv', 'icon'=>'avi'), - 'asf' => array ('type'=>'video/x-ms-asf', 'icon'=>'avi'), + 'txt' => array ('type'=>'text/plain', 'icon'=>'text', 'defaulticon'=>true), + 'wav' => array ('type'=>'audio/wav', 'icon'=>'wav', 'groups'=>array('audio'), 'string'=>'audio'), + 'webm' => array ('type'=>'video/webm', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'), + 'wmv' => array ('type'=>'video/x-ms-wmv', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'), + 'asf' => array ('type'=>'video/x-ms-asf', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'), 'xdp' => array ('type'=>'application/pdf', 'icon'=>'pdf'), 'xfd' => array ('type'=>'application/pdf', 'icon'=>'pdf'), 'xfdf' => array ('type'=>'application/pdf', 'icon'=>'pdf'), - 'xls' => array ('type'=>'application/vnd.ms-excel', 'icon'=>'excel'), + 'xls' => array ('type'=>'application/vnd.ms-excel', 'icon'=>'xlsx', 'groups'=>array('spreadsheet')), 'xlsx' => array ('type'=>'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'icon'=>'xlsx'), - 'xlsm' => array ('type'=>'application/vnd.ms-excel.sheet.macroEnabled.12', 'icon'=>'xlsm'), - 'xltx' => array ('type'=>'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'icon'=>'xltx'), - 'xltm' => array ('type'=>'application/vnd.ms-excel.template.macroEnabled.12', 'icon'=>'xltm'), - 'xlsb' => array ('type'=>'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'icon'=>'xlsb'), - 'xlam' => array ('type'=>'application/vnd.ms-excel.addin.macroEnabled.12', 'icon'=>'xlam'), + 'xlsm' => array ('type'=>'application/vnd.ms-excel.sheet.macroEnabled.12', 'icon'=>'xlsx', 'groups'=>array('spreadsheet')), + 'xltx' => array ('type'=>'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'icon'=>'xlsx'), + 'xltm' => array ('type'=>'application/vnd.ms-excel.template.macroEnabled.12', 'icon'=>'xlsx'), + 'xlsb' => array ('type'=>'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'icon'=>'xlsx'), + 'xlam' => array ('type'=>'application/vnd.ms-excel.addin.macroEnabled.12', 'icon'=>'xlsx'), 'xml' => array ('type'=>'application/xml', 'icon'=>'xml'), 'xsl' => array ('type'=>'text/xml', 'icon'=>'xml'), - 'zip' => array ('type'=>'application/zip', 'icon'=>'zip') + 'zip' => array ('type'=>'application/zip', 'icon'=>'zip', 'groups'=>array('archive'), 'string'=>'archive') ); return $mimearray; } @@ -1402,39 +1538,41 @@ function get_mimetypes_array() { * * @category files * @param string $element Desired information (usually 'icon' - * for icon filename or 'type' for MIME type) + * for icon filename or 'type' for MIME type. Can also be + * 'icon24', ...32, 48, 64, 72, 80, 96, 128, 256) * @param string $filename Filename we're looking up * @return string Requested piece of information from array */ function mimeinfo($element, $filename) { global $CFG; - $mimeinfo = get_mimetypes_array(); + $mimeinfo = & get_mimetypes_array(); + static $iconpostfixes = array(256=>'-256', 128=>'-128', 96=>'-96', 80=>'-80', 72=>'-72', 64=>'-64', 48=>'-48', 32=>'-32', 24=>'-24', 16=>''); - if (preg_match('/\.([a-z0-9]+)$/i', $filename, $match)) { - if (isset($mimeinfo[strtolower($match[1])][$element])) { - return $mimeinfo[strtolower($match[1])][$element]; - } else { - if ($element == 'icon32') { - if (isset($mimeinfo[strtolower($match[1])]['icon'])) { - $filename = $mimeinfo[strtolower($match[1])]['icon']; - } else { - $filename = 'unknown'; + $filetype = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if (empty($filetype)) { + $filetype = 'xxx'; // file without extension + } + if (preg_match('/^icon(\d*)$/', $element, $iconsizematch)) { + $iconsize = max(array(16, (int)$iconsizematch[1])); + $filenames = array($mimeinfo['xxx']['icon']); + if ($filetype != 'xxx' && isset($mimeinfo[$filetype]['icon'])) { + array_unshift($filenames, $mimeinfo[$filetype]['icon']); + } + // find the file with the closest size, first search for specific icon then for default icon + foreach ($filenames as $filename) { + foreach ($iconpostfixes as $size => $postfix) { + $fullname = $CFG->dirroot.'/pix/f/'.$filename.$postfix; + if ($iconsize >= $size && (file_exists($fullname.'.png') || file_exists($fullname.'.gif'))) { + return $filename.$postfix; } - $filename .= '-32'; - if (file_exists($CFG->dirroot.'/pix/f/'.$filename.'.png') or file_exists($CFG->dirroot.'/pix/f/'.$filename.'.gif')) { - return $filename; - } else { - return 'unknown-32'; - } - } else { - return $mimeinfo['xxx'][$element]; // By default } } - } else { - if ($element == 'icon32') { - return 'unknown-32'; - } + } else if (isset($mimeinfo[$filetype][$element])) { + return $mimeinfo[$filetype][$element]; + } else if (isset($mimeinfo['xxx'][$element])) { return $mimeinfo['xxx'][$element]; // By default + } else { + return null; } } @@ -1443,66 +1581,114 @@ function mimeinfo($element, $filename) { * the other way around. * * @category files - * @param string $element Desired information (usually 'icon') + * @param string $element Desired information ('extension', 'icon', 'icon-24', etc.) * @param string $mimetype MIME type we're looking up * @return string Requested piece of information from array */ function mimeinfo_from_type($element, $mimetype) { - $mimeinfo = get_mimetypes_array(); + /* array of cached mimetype->extension associations */ + static $cached = array(); + $mimeinfo = & get_mimetypes_array(); - foreach($mimeinfo as $values) { - if ($values['type']==$mimetype) { - if (isset($values[$element])) { - return $values[$element]; + if (!array_key_exists($mimetype, $cached)) { + $cached[$mimetype] = null; + foreach($mimeinfo as $filetype => $values) { + if ($values['type'] == $mimetype) { + if ($cached[$mimetype] === null) { + $cached[$mimetype] = $filetype; + } + if (!empty($values['defaulticon'])) { + $cached[$mimetype] = $filetype; + break; + } } - break; + } + if (empty($cached[$mimetype])) { + $cached[$mimetype] = 'xxx'; } } - return $mimeinfo['xxx'][$element]; // Default + if ($element === 'extension') { + return $cached[$mimetype]; + } else { + return mimeinfo($element, '.'.$cached[$mimetype]); + } } /** - * Get information about a filetype based on the icon file. + * Return the relative icon path for a given file * - * @category files - * @param string $element Desired information (usually 'icon') - * @param string $icon Icon file name without extension - * @param bool $all return all matching entries (defaults to false - best (by ext)/last match) - * @return string Requested piece of information from array + * Usage: + * + * // $file - instance of stored_file or file_info + * $icon = $OUTPUT->pix_url(file_file_icon($file))->out(); + * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($file))); + * + * or + * + * echo $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file)); + * + * + * @param stored_file|file_info|stdClass|array $file (in case of object attributes $file->filename + * and $file->mimetype are expected) + * @param int $size The size of the icon. Defaults to 16 can also be 24, 32, 64, 128, 256 + * @return string */ -function mimeinfo_from_icon($element, $icon, $all=false) { - $mimeinfo = get_mimetypes_array(); +function file_file_icon($file, $size = null) { + if (!is_object($file)) { + $file = (object)$file; + } + if (isset($file->filename)) { + $filename = $file->filename; + } else if (method_exists($file, 'get_filename')) { + $filename = $file->get_filename(); + } else if (method_exists($file, 'get_visible_name')) { + $filename = $file->get_visible_name(); + } else { + $filename = ''; + } + if (isset($file->mimetype)) { + $mimetype = $file->mimetype; + } else if (method_exists($file, 'get_mimetype')) { + $mimetype = $file->get_mimetype(); + } else { + $mimetype = ''; + } + if ($filename && pathinfo($filename, PATHINFO_EXTENSION) && + (!$mimetype || mimeinfo('type', $filename) === $mimetype)) { + // files with different extensions sharing the same mimetype (i.e. 'text/plain') may have different icons + return file_extension_icon($filename, $size); + } else { + // if mimetype and extension do not match we assume that mimetype is more correct + return file_mimetype_icon($mimetype, $size); + } +} - if (preg_match("/\/(.*)/", $icon, $matches)) { - $icon = $matches[1]; - } - // Try to get the extension - $extension = ''; - if (($cutat = strrpos($icon, '.')) !== false && $cutat < strlen($icon)-1) { - $extension = substr($icon, $cutat + 1); - } - $info = array($mimeinfo['xxx'][$element]); // Default - foreach($mimeinfo as $key => $values) { - if ($values['icon']==$icon) { - if (isset($values[$element])) { - $info[$key] = $values[$element]; - } - //No break, for example for 'excel' we don't want 'csv'! +/** + * Return the relative icon path for a folder image + * + * Usage: + * + * $icon = $OUTPUT->pix_url(file_folder_icon())->out(); + * echo html_writer::empty_tag('img', array('src' => $icon)); + * + * or + * + * echo $OUTPUT->pix_icon(file_folder_icon(32)); + * + * + * @param int $iconsize The size of the icon. Defaults to 16 can also be 24, 32, 48, 64, 72, 80, 96, 128, 256 + * @return string + */ +function file_folder_icon($iconsize = null) { + global $CFG; + static $iconpostfixes = array(256=>'-256', 128=>'-128', 96=>'-96', 80=>'-80', 72=>'-72', 64=>'-64', 48=>'-48', 32=>'-32', 24=>'-24', 16=>''); + $iconsize = max(array(16, (int)$iconsize)); + foreach ($iconpostfixes as $size => $postfix) { + $fullname = $CFG->dirroot.'/pix/f/folder'.$postfix; + if ($iconsize >= $size && (file_exists($fullname.'.png') || file_exists($fullname.'.gif'))) { + return 'f/folder'.$postfix; } } - if ($all) { - if (count($info) > 1) { - array_shift($info); // take off document/unknown if we have better options - } - return array_values($info); // Keep keys out when requesting all - } - - // Requested only one, try to get the best by extension coincidence, else return the last - if ($extension && isset($info[$extension])) { - return $info[$extension]; - } - - return array_pop($info); // Return last match (mimicking behaviour/comment inside foreach loop) } /** @@ -1513,27 +1699,19 @@ function mimeinfo_from_icon($element, $icon, $all=false) { * * * $mimetype = 'image/jpg'; - * $icon = $OUTPUT->pix_url(file_mimetype_icon($mimetype)); - * echo ''.$mimetype.''; + * $icon = $OUTPUT->pix_url(file_mimetype_icon($mimetype))->out(); + * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($mimetype))); * * * @category files * @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered * to conform with that. * @param string $mimetype The mimetype to fetch an icon for - * @param int $size The size of the icon. Not yet implemented + * @param int $size The size of the icon. Defaults to 16 can also be 24, 32, 64, 128, 256 * @return string The relative path to the icon */ function file_mimetype_icon($mimetype, $size = NULL) { - global $CFG; - - $icon = mimeinfo_from_type('icon', $mimetype); - if ($size) { - if (file_exists("$CFG->dirroot/pix/f/$icon-$size.png") or file_exists("$CFG->dirroot/pix/f/$icon-$size.gif")) { - $icon = "$icon-$size"; - } - } - return 'f/'.$icon; + return 'f/'.mimeinfo_from_type('icon'.$size, $mimetype); } /** @@ -1543,9 +1721,9 @@ function file_mimetype_icon($mimetype, $size = NULL) { * a return the full path to an icon. * * - * $filename = 'jpg'; - * $icon = $OUTPUT->pix_url(file_extension_icon($filename)); - * echo 'blah'; + * $filename = '.jpg'; + * $icon = $OUTPUT->pix_url(file_extension_icon($filename))->out(); + * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => '...')); * * * @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered @@ -1553,34 +1731,77 @@ function file_mimetype_icon($mimetype, $size = NULL) { * @todo MDL-31074 Implement $size * @category files * @param string $filename The filename to get the icon for - * @param int $size The size of the icon. Defaults to null can also be 32 + * @param int $size The size of the icon. Defaults to 16 can also be 24, 32, 64, 128, 256 * @return string */ function file_extension_icon($filename, $size = NULL) { - global $CFG; - - $icon = mimeinfo('icon', $filename); - if ($size) { - if (file_exists("$CFG->dirroot/pix/f/$icon-$size.png") or file_exists("$CFG->dirroot/pix/f/$icon-$size.gif")) { - $icon = "$icon-$size"; - } - } - return 'f/'.$icon; + return 'f/'.mimeinfo('icon'.$size, $filename); } /** * Obtains descriptions for file types (e.g. 'Microsoft Word document') from the * mimetypes.php language file. * - * @param string $mimetype MIME type (can be obtained using the mimeinfo function) + * @param mixed $obj - instance of stored_file or file_info or array/stdClass with field + * 'filename' and 'mimetype', or just a string with mimetype (though it is recommended to + * have filename); In case of array/stdClass the field 'mimetype' is optional. * @param bool $capitalise If true, capitalises first character of result * @return string Text description */ -function get_mimetype_description($mimetype, $capitalise=false) { - if (get_string_manager()->string_exists($mimetype, 'mimetypes')) { - $result = get_string($mimetype, 'mimetypes'); +function get_mimetype_description($obj, $capitalise=false) { + if (is_object($obj) && method_exists($obj, 'get_filename') && method_exists($obj, 'get_mimetype')) { + // this is an instance of stored_file + $mimetype = $obj->get_mimetype(); + $filename = $obj->get_filename(); + } else if (is_object($obj) && method_exists($obj, 'get_visible_name') && method_exists($obj, 'get_mimetype')) { + // this is an instance of file_info + $mimetype = $obj->get_mimetype(); + $filename = $obj->get_visible_name(); + } else if (is_array($obj) || is_object ($obj)) { + $obj = (array)$obj; + if (!empty($obj['filename'])) { + $filename = $obj['filename']; + } else { + $filename = ''; + } + if (!empty($obj['mimetype'])) { + $mimetype = $obj['mimetype']; + } else { + $mimetype = mimeinfo('type', $filename); + } } else { - $result = get_string('document/unknown','mimetypes'); + $mimetype = $obj; + $filename = ''; + } + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if (empty($extension)) { + $mimetypestr = mimeinfo_from_type('string', $mimetype); + $extension = mimeinfo_from_type('extension', $mimetype); + } else { + $mimetypestr = mimeinfo('string', $filename); + } + $chunks = explode('/', $mimetype, 2); + $chunks[] = ''; + $attr = array( + 'mimetype' => $mimetype, + 'ext' => $extension, + 'mimetype1' => $chunks[0], + 'mimetype2' => $chunks[1], + ); + $a = array(); + foreach ($attr as $key => $value) { + $a[$key] = $value; + $a[strtoupper($key)] = strtoupper($value); + $a[ucfirst($key)] = ucfirst($value); + } + if (get_string_manager()->string_exists($mimetype, 'mimetypes')) { + $result = get_string($mimetype, 'mimetypes', (object)$a); + } else if (get_string_manager()->string_exists($mimetypestr, 'mimetypes')) { + $result = get_string($mimetypestr, 'mimetypes', (object)$a); + } else if (get_string_manager()->string_exists('default', 'mimetypes')) { + $result = get_string('default', 'mimetypes', (object)$a); + } else { + $result = $mimetype; } if ($capitalise) { $result=ucfirst($result); @@ -1588,6 +1809,74 @@ function get_mimetype_description($mimetype, $capitalise=false) { return $result; } +/** + * Returns array of elements of type $element in type group(s) + * + * @param string $element name of the element we are interested in, usually 'type' or 'extension' + * @param string|array $groups one group or array of groups/extensions/mimetypes + * @return array + */ +function file_get_typegroup($element, $groups) { + static $cached = array(); + if (!is_array($groups)) { + $groups = array($groups); + } + if (!array_key_exists($element, $cached)) { + $cached[$element] = array(); + } + $result = array(); + foreach ($groups as $group) { + if (!array_key_exists($group, $cached[$element])) { + // retrieive and cache all elements of type $element for group $group + $mimeinfo = & get_mimetypes_array(); + $cached[$element][$group] = array(); + foreach ($mimeinfo as $extension => $value) { + $value['extension'] = $extension; + if (empty($value[$element])) { + continue; + } + if (($group === $extension || $group === $value['type'] || + (!empty($value['groups']) && in_array($group, $value['groups']))) && + !in_array($value[$element], $cached[$element][$group])) { + $cached[$element][$group][] = $value[$element]; + } + } + } + $result = array_merge($result, $cached[$element][$group]); + } + return array_unique($result); +} + +/** + * Checks if file with name $filename has one of the extensions in groups $groups + * + * @see get_mimetypes_array() + * @param string $filename name of the file to check + * @param string|array $groups one group or array of groups to check + * @param bool $checktype if true and extension check fails, find the mimetype and check if + * file mimetype is in mimetypes in groups $groups + * @return bool + */ +function file_extension_in_typegroup($filename, $groups, $checktype = false) { + $extension = pathinfo($filename, PATHINFO_EXTENSION); + if (!empty($extension) && in_array(strtolower($extension), file_get_typegroup('extension', $groups))) { + return true; + } + return $checktype && file_mimetype_in_typegroup(mimeinfo('type', $filename), $groups); +} + +/** + * Checks if mimetype $mimetype belongs to one of the groups $groups + * + * @see get_mimetypes_array() + * @param string $mimetype + * @param string|array $groups one group or array of groups to check + * @return bool + */ +function file_mimetype_in_typegroup($mimetype, $groups) { + return !empty($mimetype) && in_array($mimetype, file_get_typegroup('type', $groups)); +} + /** * Requested file is not found or not accessible, does not return, terminates script * @@ -2005,6 +2294,12 @@ function send_stored_file($stored_file, $lifetime=86400 , $filter=0, $forcedownl } } + // handle external resource + if ($stored_file->is_external_file()) { + $stored_file->send_file($lifetime, $filter, $forcedownload, $options); + die; + } + if (!$stored_file or $stored_file->is_directory()) { // nothing to serve if ($dontdie) { @@ -2943,7 +3238,7 @@ class curl_cache { * @global stdClass $CFG * @param string $module which module is using curl_cache */ - function __construct($module = 'repository'){ + public function __construct($module = 'repository') { global $CFG; if (!empty($module)) { $this->dir = $CFG->cachedir.'/'.$module.'/'; @@ -2974,14 +3269,13 @@ class curl_cache { * @param mixed $param * @return bool|string */ - public function get($param){ + public function get($param) { global $CFG, $USER; $this->cleanup($this->ttl); $filename = 'u'.$USER->id.'_'.md5(serialize($param)); if(file_exists($this->dir.$filename)) { $lasttime = filemtime($this->dir.$filename); - if(time()-$lasttime > $this->ttl) - { + if (time()-$lasttime > $this->ttl) { return false; } else { $fp = fopen($this->dir.$filename, 'r'); @@ -3001,7 +3295,7 @@ class curl_cache { * @param mixed $param * @param mixed $val */ - public function set($param, $val){ + public function set($param, $val) { global $CFG, $USER; $filename = 'u'.$USER->id.'_'.md5(serialize($param)); $fp = fopen($this->dir.$filename, 'w'); @@ -3012,18 +3306,19 @@ class curl_cache { /** * Remove cache files * - * @param int $expire The number os seconds before expiry + * @param int $expire The number of seconds before expiry */ - public function cleanup($expire){ - if($dir = opendir($this->dir)){ + public function cleanup($expire) { + if ($dir = opendir($this->dir)) { while (false !== ($file = readdir($dir))) { if(!is_dir($file) && $file != '.' && $file != '..') { $lasttime = @filemtime($this->dir.$file); - if(time() - $lasttime > $expire){ + if (time() - $lasttime > $expire) { @unlink($this->dir.$file); } } } + closedir($dir); } } /** @@ -3032,12 +3327,12 @@ class curl_cache { * @global object $CFG * @global object $USER */ - public function refresh(){ + public function refresh() { global $CFG, $USER; - if($dir = opendir($this->dir)){ + if ($dir = opendir($this->dir)) { while (false !== ($file = readdir($dir))) { - if(!is_dir($file) && $file != '.' && $file != '..') { - if(strpos($file, 'u'.$USER->id.'_')!==false){ + if (!is_dir($file) && $file != '.' && $file != '..') { + if (strpos($file, 'u'.$USER->id.'_') !== false) { @unlink($this->dir.$file); } } @@ -3046,109 +3341,6 @@ class curl_cache { } } -/** - * This class is used to parse lib/file/file_types.mm which help get file extensions by file types. - * - * The file_types.mm file can be edited by freemind in graphic environment. - * - * @package core_files - * @category files - * @copyright 2009 Dongsheng Cai - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class filetype_parser { - /** - * Check file_types.mm file, setup variables - * - * @global stdClass $CFG - * @param string $file - */ - public function __construct($file = '') { - global $CFG; - if (empty($file)) { - $this->file = $CFG->libdir.'/filestorage/file_types.mm'; - } else { - $this->file = $file; - } - $this->tree = array(); - $this->result = array(); - } - - /** - * A private function to browse xml nodes - * - * @param array $parent - * @param array $types - */ - private function _browse_nodes($parent, $types) { - $key = (string)$parent['TEXT']; - if(isset($parent->node)) { - $this->tree[$key] = array(); - if (in_array((string)$parent['TEXT'], $types)) { - $this->_select_nodes($parent, $this->result); - } else { - foreach($parent->node as $v){ - $this->_browse_nodes($v, $types); - } - } - } else { - $this->tree[] = $key; - } - } - - /** - * A private function to select text nodes - * - * @param array $parent - */ - private function _select_nodes($parent){ - if(isset($parent->node)) { - foreach($parent->node as $v){ - $this->_select_nodes($v, $this->result); - } - } else { - $this->result[] = (string)$parent['TEXT']; - } - } - - - /** - * Get file extensions by file types names. - * - * @param array $types - * @return mixed - */ - public function get_extensions($types) { - if (!is_array($types)) { - $types = array($types); - } - $this->result = array(); - if ((is_array($types) && in_array('*', $types)) || - $types == '*' || empty($types)) { - return array('*'); - } - foreach ($types as $key=>$value){ - if (strpos($value, '.') !== false) { - $this->result[] = $value; - unset($types[$key]); - } - } - if (file_exists($this->file)) { - $xml = simplexml_load_file($this->file); - foreach($xml->node->node as $v){ - if (in_array((string)$v['TEXT'], $types)) { - $this->_select_nodes($v); - } else { - $this->_browse_nodes($v, $types); - } - } - } else { - exit('Failed to open file lib/filestorage/file_types.mm'); - } - return $this->result; - } -} - /** * This function delegates file serving to individual plugins * @@ -3923,3 +4115,172 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } } + +/** + * Universe file cacheing class + * + * @package core_files + * @category files + * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_file { + /** @var string */ + public $cachedir = ''; + + /** + * static method to create cache_file class instance + * + * @param array $options caching ooptions + */ + public static function get_instance($options = array()) { + return new cache_file($options); + } + + /** + * Constructor + * + * @param array $options + */ + private function __construct($options = array()) { + global $CFG; + + // Path to file caches. + if (isset($options['cachedir'])) { + $this->cachedir = $options['cachedir']; + } else { + $this->cachedir = $CFG->cachedir . '/filedir'; + } + + // Create cache directory. + if (!file_exists($this->cachedir)) { + mkdir($this->cachedir, $CFG->directorypermissions, true); + } + + // When use cache_file::get, it will check ttl. + if (isset($options['ttl']) && is_numeric($options['ttl'])) { + $this->ttl = $options['ttl']; + } else { + // One day. + $this->ttl = 60 * 60 * 24; + } + } + + /** + * Get cached file, false if file expires + * + * @param mixed $param + * @param array $options caching options + * @return bool|string + */ + public static function get($param, $options = array()) { + $instance = self::get_instance($options); + $filepath = $instance->generate_filepath($param); + if (file_exists($filepath)) { + $lasttime = filemtime($filepath); + if (time() - $lasttime > $instance->ttl) { + // Remove cache file. + unlink($filepath); + return false; + } else { + return $filepath; + } + } else { + return false; + } + } + + /** + * Static method to create cache from a file + * + * @param mixed $ref + * @param string $srcfile + * @param array $options + * @return string cached file path + */ + public static function create_from_file($ref, $srcfile, $options = array()) { + $instance = self::get_instance($options); + $cachedfilepath = $instance->generate_filepath($ref); + copy($srcfile, $cachedfilepath); + return $cachedfilepath; + } + + /** + * Static method to create cache from url + * + * @param mixed $ref file reference + * @param string $url file url + * @param array $options options + * @return string cached file path + */ + public static function create_from_url($ref, $url, $options = array()) { + global $CFG; + $instance = self::get_instance($options); + $cachedfilepath = $instance->generate_filepath($ref); + $fp = fopen($cachedfilepath, 'w'); + $curl = new curl; + $curl->download(array(array('url'=>$url, 'file'=>$fp))); + // Must close file handler. + fclose($fp); + return $cachedfilepath; + } + + /** + * Static method to create cache from string + * + * @param mixed $ref file reference + * @param string $url file url + * @param array $options options + * @return string cached file path + */ + public static function create_from_string($ref, $string, $options = array()) { + global $CFG; + $instance = self::get_instance($options); + $cachedfilepath = $instance->generate_filepath($ref); + $fp = fopen($cachedfilepath, 'w'); + fwrite($fp, $string); + // Must close file handler. + fclose($fp); + return $cachedfilepath; + } + + /** + * Build path to cache file + * + * @param mixed $ref + * @return string + */ + private function generate_filepath($ref) { + global $CFG; + $hash = sha1(serialize($ref)); + $l1 = $hash[0].$hash[1]; + $l2 = $hash[2].$hash[3]; + $dir = $this->cachedir . "/$l1/$l2"; + if (!file_exists($dir)) { + mkdir($dir, $CFG->directorypermissions, true); + } + return "$dir/$hash"; + } + + /** + * Remove cache files + * + * @param array $options options + * @param int $expire The number of seconds before expiry + */ + public static function cleanup($options = array(), $expire) { + global $CFG; + $instance = self::get_instance($options); + if ($dir = opendir($instance->cachedir)) { + while (($file = readdir($dir)) !== false) { + if (!is_dir($file) && $file != '.' && $file != '..') { + $lasttime = @filemtime($instance->cachedir . $file); + if(time() - $lasttime > $expire){ + @unlink($instance->cachedir . $file); + } + } + } + closedir($dir); + } + } +} diff --git a/lib/filestorage/file_exceptions.php b/lib/filestorage/file_exceptions.php index 93eef5b0164..68c01de2e39 100644 --- a/lib/filestorage/file_exceptions.php +++ b/lib/filestorage/file_exceptions.php @@ -65,7 +65,7 @@ class stored_file_creation_exception extends file_exception { * @param string $filename file name * @param string $debuginfo extra debug info */ - function __construct($contextid, $component, $filearea, $itemid, $filepath, $filename, $debuginfo = NULL) { + function __construct($contextid, $component, $filearea, $itemid, $filepath, $filename, $debuginfo = null) { $a = new stdClass(); $a->contextid = $contextid; $a->component = $component; @@ -91,8 +91,8 @@ class file_access_exception extends file_exception { * * @param string $debuginfo extra debug info */ - function __construct($debuginfo = NULL) { - parent::__construct('nopermissions', NULL, $debuginfo); + public function __construct($debuginfo = null) { + parent::__construct('nopermissions', null, $debuginfo); } } @@ -111,7 +111,28 @@ class file_pool_content_exception extends file_exception { * @param string $contenthash content hash * @param string $debuginfo extra debug info */ - function __construct($contenthash, $debuginfo = NULL) { + public function __construct($contenthash, $debuginfo = null) { parent::__construct('hashpoolproblem', $contenthash, $debuginfo); } } + +/** + * Exception related to external file support + * + * @package core_files + * @category files + * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external_file_exception extends file_exception { + /** + * Constructor + * + * @param string $errorcode error code + * @param stdClass $a extra information + * @param string $debuginfo extra debug info + */ + public function __construct($errorcode, $a = null, $debuginfo = null) { + parent::__construct($errorcode, '', '', $a, $debuginfo); + } +} diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index 9855a6d0973..149936b8079 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -145,11 +145,12 @@ class file_storage { /** * Create instance of file class from database record. * - * @param stdClass $file_record record from the files table + * @param stdClass $filerecord record from the files table left join files_reference table * @return stored_file instance of file abstraction class */ - public function get_file_instance(stdClass $file_record) { - return new stored_file($this, $file_record, $this->filedir); + public function get_file_instance(stdClass $filerecord) { + $storedfile = new stored_file($this, $filerecord, $this->filedir); + return $storedfile; } /** @@ -245,7 +246,7 @@ class file_storage { $file->copy_content_to($tmpfilepath); if ($mode === 'tinyicon') { - $data = generate_image_thumbnail($tmpfilepath, 16, 16); + $data = generate_image_thumbnail($tmpfilepath, 24, 24); } else if ($mode === 'thumb') { $data = generate_image_thumbnail($tmpfilepath, 90, 90); @@ -271,8 +272,13 @@ class file_storage { public function get_file_by_id($fileid) { global $DB; - if ($file_record = $DB->get_record('files', array('id'=>$fileid))) { - return $this->get_file_instance($file_record); + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.id = ?"; + if ($filerecord = $DB->get_record_sql($sql, array($fileid))) { + return $this->get_file_instance($filerecord); } else { return false; } @@ -287,8 +293,13 @@ class file_storage { public function get_file_by_hash($pathnamehash) { global $DB; - if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) { - return $this->get_file_instance($file_record); + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.pathnamehash = ?"; + if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) { + return $this->get_file_instance($filerecord); } else { return false; } @@ -351,6 +362,29 @@ class file_storage { return !$DB->record_exists_sql($sql, $params); } + /** + * Returns all files belonging to given repository + * + * @param int $repositoryid + * @param string $sort + */ + public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') { + global $DB; + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE r.repositoryid = ? + ORDER BY $sort"; + + $result = array(); + $filerecords = $DB->get_records_sql($sql, array($repositoryid)); + foreach ($filerecords as $filerecord) { + $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); + } + return $result; + } + /** * Returns all area files (optionally limited by itemid) * @@ -367,16 +401,29 @@ class file_storage { $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea); if ($itemid !== false) { + $itemidsql = ' AND f.itemid = :itemid '; $conditions['itemid'] = $itemid; + } else { + $itemidsql = ''; } + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.contextid = :contextid + AND f.component = :component + AND f.filearea = :filearea + $itemidsql + ORDER BY $sort"; + $result = array(); - $file_records = $DB->get_records('files', $conditions, $sort); - foreach ($file_records as $file_record) { - if (!$includedirs and $file_record->filename === '.') { + $filerecords = $DB->get_records_sql($sql, $conditions); + foreach ($filerecords as $filerecord) { + if (!$includedirs and $filerecord->filename === '.') { continue; } - $result[$file_record->pathnamehash] = $this->get_file_instance($file_record); + $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); } return $result; } @@ -457,23 +504,25 @@ class file_storage { $dirs = $includedirs ? "" : "AND filename <> '.'"; $length = textlib::strlen($filepath); - $sql = "SELECT * - FROM {files} - WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid - AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath - AND id <> :dirid + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid + AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath + AND f.id <> :dirid $dirs ORDER BY $sort"; $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id()); $files = array(); $dirs = array(); - $file_records = $DB->get_records_sql($sql, $params); - foreach ($file_records as $file_record) { - if ($file_record->filename == '.') { - $dirs[$file_record->pathnamehash] = $this->get_file_instance($file_record); + $filerecords = $DB->get_records_sql($sql, $params); + foreach ($filerecords as $filerecord) { + if ($filerecord->filename == '.') { + $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); } else { - $files[$file_record->pathnamehash] = $this->get_file_instance($file_record); + $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); } } $result = array_merge($dirs, $files); @@ -485,32 +534,36 @@ class file_storage { $length = textlib::strlen($filepath); if ($includedirs) { - $sql = "SELECT * - FROM {files} - WHERE contextid = :contextid AND component = :component AND filearea = :filearea - AND itemid = :itemid AND filename = '.' - AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath - AND id <> :dirid + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea + AND f.itemid = :itemid AND f.filename = '.' + AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath + AND f.id <> :dirid ORDER BY $sort"; $reqlevel = substr_count($filepath, '/') + 1; - $file_records = $DB->get_records_sql($sql, $params); - foreach ($file_records as $file_record) { - if (substr_count($file_record->filepath, '/') !== $reqlevel) { + $filerecords = $DB->get_records_sql($sql, $params); + foreach ($filerecords as $filerecord) { + if (substr_count($filerecord->filepath, '/') !== $reqlevel) { continue; } - $result[$file_record->pathnamehash] = $this->get_file_instance($file_record); + $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); } } - $sql = "SELECT * - FROM {files} - WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid - AND filepath = :filepath AND filename <> '.' + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid + AND f.filepath = :filepath AND f.filename <> '.' ORDER BY $sort"; - $file_records = $DB->get_records_sql($sql, $params); - foreach ($file_records as $file_record) { - $result[$file_record->pathnamehash] = $this->get_file_instance($file_record); + $filerecords = $DB->get_records_sql($sql, $params); + foreach ($filerecords as $filerecord) { + $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); } } @@ -540,9 +593,9 @@ class file_storage { $conditions['itemid'] = $itemid; } - $file_records = $DB->get_records('files', $conditions); - foreach ($file_records as $file_record) { - $this->get_file_instance($file_record)->delete(); + $filerecords = $DB->get_records('files', $conditions); + foreach ($filerecords as $filerecord) { + $this->get_file_instance($filerecord)->delete(); } return true; // BC only @@ -572,11 +625,11 @@ class file_storage { $params['component'] = $component; $params['filearea'] = $filearea; - $file_records = $DB->get_recordset_select('files', $where, $params); - foreach ($file_records as $file_record) { - $this->get_file_instance($file_record)->delete(); + $filerecords = $DB->get_recordset_select('files', $where, $params); + foreach ($filerecords as $filerecord) { + $this->get_file_instance($filerecord)->delete(); } - $file_records->close(); + $filerecords->close(); } /** @@ -699,11 +752,11 @@ class file_storage { /** * Add new local file based on existing local file. * - * @param stdClass|array $file_record object or array describing changes + * @param stdClass|array $filerecord object or array describing changes * @param stored_file|int $fileorid id or stored_file instance of the existing local file * @return stored_file instance of newly created file */ - public function create_file_from_storedfile($file_record, $fileorid) { + public function create_file_from_storedfile($filerecord, $fileorid) { global $DB; if ($fileorid instanceof stored_file) { @@ -712,20 +765,26 @@ class file_storage { $fid = $fileorid; } - $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record! + $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record! - unset($file_record['id']); - unset($file_record['filesize']); - unset($file_record['contenthash']); - unset($file_record['pathnamehash']); + unset($filerecord['id']); + unset($filerecord['filesize']); + unset($filerecord['contenthash']); + unset($filerecord['pathnamehash']); - if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) { + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.id = ?"; + + if (!$newrecord = $DB->get_record_sql($sql, array($fid))) { throw new file_exception('storedfileproblem', 'File does not exist'); } unset($newrecord->id); - foreach ($file_record as $key=>$value) { + foreach ($filerecord as $key => $value) { // validate all parameters, we do not want any rubbish stored in database, right? if ($key == 'contextid' and (!is_number($value) or $value < 1)) { throw new file_exception('storedfileproblem', 'Invalid contextid'); @@ -776,6 +835,10 @@ class file_storage { } } + if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') { + $value = clean_param($value, PARAM_INT); + } + $newrecord->$key = $value; } @@ -790,6 +853,21 @@ class file_storage { return $this->get_file_instance($newrecord); } + if (!empty($newrecord->repositoryid)) { + try { + $referencerecord = new stdClass; + $referencerecord->repositoryid = $newrecord->repositoryid; + $referencerecord->reference = $newrecord->reference; + $referencerecord->lastsync = $newrecord->referencelastsync; + $referencerecord->lifetime = $newrecord->referencelifetime; + $referencerecord->id = $DB->insert_record('files_reference', $referencerecord); + } catch (dml_exception $e) { + throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, + $newrecord->filepath, $newrecord->filename, $e->debuginfo); + } + $newrecord->referencefileid = $referencerecord->id; + } + try { $newrecord->id = $DB->insert_record('files', $newrecord); } catch (dml_exception $e) { @@ -797,6 +875,7 @@ class file_storage { $newrecord->filepath, $newrecord->filename, $e->debuginfo); } + $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid); return $this->get_file_instance($newrecord); @@ -805,16 +884,16 @@ class file_storage { /** * Add new local file. * - * @param stdClass|array $file_record object or array describing file + * @param stdClass|array $filerecord object or array describing file * @param string $url the URL to the file * @param array $options {@link download_file_content()} options * @param bool $usetempfile use temporary file for download, may prevent out of memory problems * @return stored_file */ - public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) { + public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) { - $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects - $file_record = (object)$file_record; // we support arrays too + $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. + $filerecord = (object)$filerecord; // We support arrays too. $headers = isset($options['headers']) ? $options['headers'] : null; $postdata = isset($options['postdata']) ? $options['postdata'] : null; @@ -824,13 +903,13 @@ class file_storage { $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false; $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false; - if (!isset($file_record->filename)) { + if (!isset($filerecord->filename)) { $parts = explode('/', $url); $filename = array_pop($parts); - $file_record->filename = clean_param($filename, PARAM_FILE); + $filerecord->filename = clean_param($filename, PARAM_FILE); } - $source = !empty($file_record->source) ? $file_record->source : $url; - $file_record->source = clean_param($source, PARAM_URL); + $source = !empty($filerecord->source) ? $filerecord->source : $url; + $filerecord->source = clean_param($source, PARAM_URL); if ($usetempfile) { check_dir_exists($this->tempdir); @@ -840,7 +919,7 @@ class file_storage { throw new file_exception('storedfileproblem', 'Can not fetch file form URL'); } try { - $newfile = $this->create_file_from_pathname($file_record, $tmpfile); + $newfile = $this->create_file_from_pathname($filerecord, $tmpfile); @unlink($tmpfile); return $newfile; } catch (Exception $e) { @@ -853,104 +932,108 @@ class file_storage { if ($content === false) { throw new file_exception('storedfileproblem', 'Can not fetch file form URL'); } - return $this->create_file_from_string($file_record, $content); + return $this->create_file_from_string($filerecord, $content); } } /** * Add new local file. * - * @param stdClass|array $file_record object or array describing file + * @param stdClass|array $filerecord object or array describing file * @param string $pathname path to file or content of file * @return stored_file */ - public function create_file_from_pathname($file_record, $pathname) { + public function create_file_from_pathname($filerecord, $pathname) { global $DB; - $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects - $file_record = (object)$file_record; // we support arrays too + $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. + $filerecord = (object)$filerecord; // We support arrays too. // validate all parameters, we do not want any rubbish stored in database, right? - if (!is_number($file_record->contextid) or $file_record->contextid < 1) { + if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) { throw new file_exception('storedfileproblem', 'Invalid contextid'); } - $file_record->component = clean_param($file_record->component, PARAM_COMPONENT); - if (empty($file_record->component)) { + $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT); + if (empty($filerecord->component)) { throw new file_exception('storedfileproblem', 'Invalid component'); } - $file_record->filearea = clean_param($file_record->filearea, PARAM_AREA); - if (empty($file_record->filearea)) { + $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA); + if (empty($filerecord->filearea)) { throw new file_exception('storedfileproblem', 'Invalid filearea'); } - if (!is_number($file_record->itemid) or $file_record->itemid < 0) { + if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) { throw new file_exception('storedfileproblem', 'Invalid itemid'); } - if (!empty($file_record->sortorder)) { - if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) { - $file_record->sortorder = 0; + if (!empty($filerecord->sortorder)) { + if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) { + $filerecord->sortorder = 0; } } else { - $file_record->sortorder = 0; + $filerecord->sortorder = 0; } - $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH); - if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) { + $filerecord->referencefileid = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid; + $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync; + $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime; + + $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); + if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { // path must start and end with '/' throw new file_exception('storedfileproblem', 'Invalid file path'); } - $file_record->filename = clean_param($file_record->filename, PARAM_FILE); - if ($file_record->filename === '') { + $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE); + if ($filerecord->filename === '') { // filename must not be empty throw new file_exception('storedfileproblem', 'Invalid file name'); } $now = time(); - if (isset($file_record->timecreated)) { - if (!is_number($file_record->timecreated)) { + if (isset($filerecord->timecreated)) { + if (!is_number($filerecord->timecreated)) { throw new file_exception('storedfileproblem', 'Invalid file timecreated'); } - if ($file_record->timecreated < 0) { + if ($filerecord->timecreated < 0) { //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) - $file_record->timecreated = 0; + $filerecord->timecreated = 0; } } else { - $file_record->timecreated = $now; + $filerecord->timecreated = $now; } - if (isset($file_record->timemodified)) { - if (!is_number($file_record->timemodified)) { + if (isset($filerecord->timemodified)) { + if (!is_number($filerecord->timemodified)) { throw new file_exception('storedfileproblem', 'Invalid file timemodified'); } - if ($file_record->timemodified < 0) { + if ($filerecord->timemodified < 0) { //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) - $file_record->timemodified = 0; + $filerecord->timemodified = 0; } } else { - $file_record->timemodified = $now; + $filerecord->timemodified = $now; } $newrecord = new stdClass(); - $newrecord->contextid = $file_record->contextid; - $newrecord->component = $file_record->component; - $newrecord->filearea = $file_record->filearea; - $newrecord->itemid = $file_record->itemid; - $newrecord->filepath = $file_record->filepath; - $newrecord->filename = $file_record->filename; + $newrecord->contextid = $filerecord->contextid; + $newrecord->component = $filerecord->component; + $newrecord->filearea = $filerecord->filearea; + $newrecord->itemid = $filerecord->itemid; + $newrecord->filepath = $filerecord->filepath; + $newrecord->filename = $filerecord->filename; - $newrecord->timecreated = $file_record->timecreated; - $newrecord->timemodified = $file_record->timemodified; - $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype; - $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid; - $newrecord->source = empty($file_record->source) ? null : $file_record->source; - $newrecord->author = empty($file_record->author) ? null : $file_record->author; - $newrecord->license = empty($file_record->license) ? null : $file_record->license; - $newrecord->sortorder = $file_record->sortorder; + $newrecord->timecreated = $filerecord->timecreated; + $newrecord->timemodified = $filerecord->timemodified; + $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($pathname) : $filerecord->mimetype; + $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid; + $newrecord->source = empty($filerecord->source) ? null : $filerecord->source; + $newrecord->author = empty($filerecord->author) ? null : $filerecord->author; + $newrecord->license = empty($filerecord->license) ? null : $filerecord->license; + $newrecord->sortorder = $filerecord->sortorder; list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname); @@ -974,99 +1057,104 @@ class file_storage { /** * Add new local file. * - * @param stdClass|array $file_record object or array describing file + * @param stdClass|array $filerecord object or array describing file * @param string $content content of file * @return stored_file */ - public function create_file_from_string($file_record, $content) { + public function create_file_from_string($filerecord, $content) { global $DB; - $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects - $file_record = (object)$file_record; // we support arrays too + $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. + $filerecord = (object)$filerecord; // We support arrays too. // validate all parameters, we do not want any rubbish stored in database, right? - if (!is_number($file_record->contextid) or $file_record->contextid < 1) { + if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) { throw new file_exception('storedfileproblem', 'Invalid contextid'); } - $file_record->component = clean_param($file_record->component, PARAM_COMPONENT); - if (empty($file_record->component)) { + $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT); + if (empty($filerecord->component)) { throw new file_exception('storedfileproblem', 'Invalid component'); } - $file_record->filearea = clean_param($file_record->filearea, PARAM_AREA); - if (empty($file_record->filearea)) { + $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA); + if (empty($filerecord->filearea)) { throw new file_exception('storedfileproblem', 'Invalid filearea'); } - if (!is_number($file_record->itemid) or $file_record->itemid < 0) { + if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) { throw new file_exception('storedfileproblem', 'Invalid itemid'); } - if (!empty($file_record->sortorder)) { - if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) { - $file_record->sortorder = 0; + if (!empty($filerecord->sortorder)) { + if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) { + $filerecord->sortorder = 0; } } else { - $file_record->sortorder = 0; + $filerecord->sortorder = 0; } + $filerecord->referencefileid = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid; + $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync; + $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime; - $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH); - if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) { + $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); + if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { // path must start and end with '/' throw new file_exception('storedfileproblem', 'Invalid file path'); } - $file_record->filename = clean_param($file_record->filename, PARAM_FILE); - if ($file_record->filename === '') { + $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE); + if ($filerecord->filename === '') { // path must start and end with '/' throw new file_exception('storedfileproblem', 'Invalid file name'); } $now = time(); - if (isset($file_record->timecreated)) { - if (!is_number($file_record->timecreated)) { + if (isset($filerecord->timecreated)) { + if (!is_number($filerecord->timecreated)) { throw new file_exception('storedfileproblem', 'Invalid file timecreated'); } - if ($file_record->timecreated < 0) { + if ($filerecord->timecreated < 0) { //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) - $file_record->timecreated = 0; + $filerecord->timecreated = 0; } } else { - $file_record->timecreated = $now; + $filerecord->timecreated = $now; } - if (isset($file_record->timemodified)) { - if (!is_number($file_record->timemodified)) { + if (isset($filerecord->timemodified)) { + if (!is_number($filerecord->timemodified)) { throw new file_exception('storedfileproblem', 'Invalid file timemodified'); } - if ($file_record->timemodified < 0) { + if ($filerecord->timemodified < 0) { //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) - $file_record->timemodified = 0; + $filerecord->timemodified = 0; } } else { - $file_record->timemodified = $now; + $filerecord->timemodified = $now; } $newrecord = new stdClass(); - $newrecord->contextid = $file_record->contextid; - $newrecord->component = $file_record->component; - $newrecord->filearea = $file_record->filearea; - $newrecord->itemid = $file_record->itemid; - $newrecord->filepath = $file_record->filepath; - $newrecord->filename = $file_record->filename; + $newrecord->contextid = $filerecord->contextid; + $newrecord->component = $filerecord->component; + $newrecord->filearea = $filerecord->filearea; + $newrecord->itemid = $filerecord->itemid; + $newrecord->filepath = $filerecord->filepath; + $newrecord->filename = $filerecord->filename; - $newrecord->timecreated = $file_record->timecreated; - $newrecord->timemodified = $file_record->timemodified; - $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype; - $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid; - $newrecord->source = empty($file_record->source) ? null : $file_record->source; - $newrecord->author = empty($file_record->author) ? null : $file_record->author; - $newrecord->license = empty($file_record->license) ? null : $file_record->license; - $newrecord->sortorder = $file_record->sortorder; + $newrecord->timecreated = $filerecord->timecreated; + $newrecord->timemodified = $filerecord->timemodified; + $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid; + $newrecord->source = empty($filerecord->source) ? null : $filerecord->source; + $newrecord->author = empty($filerecord->author) ? null : $filerecord->author; + $newrecord->license = empty($filerecord->license) ? null : $filerecord->license; + $newrecord->sortorder = $filerecord->sortorder; list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content); + $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash; + // get mimetype by magic bytes + $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname) : $filerecord->mimetype; $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename); @@ -1085,10 +1173,135 @@ class file_storage { return $this->get_file_instance($newrecord); } + /** + * Create a moodle file from file reference information + * + * @param stdClass $filerecord + * @param int $repositoryid + * @param string $reference + * @param array $options options for creating external file + * @return stored_file + */ + public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) { + global $DB; + + $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. + $filerecord = (object)$filerecord; // We support arrays too. + + // validate all parameters, we do not want any rubbish stored in database, right? + if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) { + throw new file_exception('storedfileproblem', 'Invalid contextid'); + } + + $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT); + if (empty($filerecord->component)) { + throw new file_exception('storedfileproblem', 'Invalid component'); + } + + $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA); + if (empty($filerecord->filearea)) { + throw new file_exception('storedfileproblem', 'Invalid filearea'); + } + + if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) { + throw new file_exception('storedfileproblem', 'Invalid itemid'); + } + + if (!empty($filerecord->sortorder)) { + if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) { + $filerecord->sortorder = 0; + } + } else { + $filerecord->sortorder = 0; + } + + $filerecord->referencefileid = empty($filerecord->referencefileid) ? 0 : $filerecord->referencefileid; + $filerecord->referencelastsync = empty($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync; + $filerecord->referencelifetime = empty($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime; + $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype; + $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid; + $filerecord->source = empty($filerecord->source) ? null : $filerecord->source; + $filerecord->author = empty($filerecord->author) ? null : $filerecord->author; + $filerecord->license = empty($filerecord->license) ? null : $filerecord->license; + $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); + if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { + // Path must start and end with '/'. + throw new file_exception('storedfileproblem', 'Invalid file path'); + } + + $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE); + if ($filerecord->filename === '') { + // Path must start and end with '/'. + throw new file_exception('storedfileproblem', 'Invalid file name'); + } + + $now = time(); + if (isset($filerecord->timecreated)) { + if (!is_number($filerecord->timecreated)) { + throw new file_exception('storedfileproblem', 'Invalid file timecreated'); + } + if ($filerecord->timecreated < 0) { + // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) + $filerecord->timecreated = 0; + } + } else { + $filerecord->timecreated = $now; + } + + if (isset($filerecord->timemodified)) { + if (!is_number($filerecord->timemodified)) { + throw new file_exception('storedfileproblem', 'Invalid file timemodified'); + } + if ($filerecord->timemodified < 0) { + // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) + $filerecord->timemodified = 0; + } + } else { + $filerecord->timemodified = $now; + } + + // Insert file reference record. + try { + $referencerecord = new stdClass; + $referencerecord->repositoryid = $repositoryid; + $referencerecord->reference = $reference; + $referencerecord->lastsync = $filerecord->referencelastsync; + $referencerecord->lifetime = $filerecord->referencelifetime; + $referencerecord->id = $DB->insert_record('files_reference', $referencerecord); + } catch (dml_exception $e) { + throw $e; + } + + $filerecord->referencefileid = $referencerecord->id; + + // External file doesn't have content in moodle. + // So we create an empty file for it. + list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null); + + $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename); + + try { + $filerecord->id = $DB->insert_record('files', $filerecord); + } catch (dml_exception $e) { + if ($newfile) { + $this->deleted_file_cleanup($filerecord->contenthash); + } + throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, + $filerecord->filepath, $filerecord->filename, $e->debuginfo); + } + + $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid); + + // Adding repositoryid and reference to file record to create stored_file instance + $filerecord->repositoryid = $repositoryid; + $filerecord->reference = $reference; + return $this->get_file_instance($filerecord); + } + /** * Creates new image file from existing. * - * @param stdClass|array $file_record object or array describing new file + * @param stdClass|array $filerecord object or array describing new file * @param int|stored_file $fid file id or stored file object * @param int $newwidth in pixels * @param int $newheight in pixels @@ -1096,7 +1309,7 @@ class file_storage { * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png * @return stored_file */ - public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) { + public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) { if (!function_exists('imagecreatefromstring')) { //Most likely the GD php extension isn't installed //image conversion cannot succeed @@ -1107,9 +1320,9 @@ class file_storage { $fid = $fid->get_id(); } - $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record! + $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record! - if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data + if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data. throw new file_exception('storedfileproblem', 'File does not exist'); } @@ -1117,12 +1330,12 @@ class file_storage { throw new file_exception('storedfileproblem', 'File is not an image'); } - if (!isset($file_record['filename'])) { - $file_record['filename'] = $file->get_filename(); + if (!isset($filerecord['filename'])) { + $filerecord['filename'] = $file->get_filename(); } - if (!isset($file_record['mimetype'])) { - $file_record['mimetype'] = mimeinfo('type', $file_record['filename']); + if (!isset($filerecord['mimetype'])) { + $filerecord['mimetype'] = $imageinfo['mimetype']; } $width = $imageinfo['width']; @@ -1171,7 +1384,7 @@ class file_storage { } ob_start(); - switch ($file_record['mimetype']) { + switch ($filerecord['mimetype']) { case 'image/gif': imagegif($img); break; @@ -1201,7 +1414,7 @@ class file_storage { throw new file_exception('storedfileproblem', 'Can not convert image'); } - return $this->create_file_from_string($file_record, $content); + return $this->create_file_from_string($filerecord, $content); } /** @@ -1310,6 +1523,18 @@ class file_storage { return xsendfile("$hashpath/$contenthash"); } + /** + * Content exists + * + * @param string $contenthash + * @return bool + */ + public function content_exists($contenthash) { + $dir = $this->path_from_hash($contenthash); + $filepath = $dir . '/' . $contenthash; + return file_exists($filepath); + } + /** * Return path to file with given hash. * @@ -1408,6 +1633,182 @@ class file_storage { chmod($trashfile, $this->filepermissions); // fix permissions if needed } + /** + * When user referring to a moodle file, we build the reference field + * + * @param array $params + * @return string + */ + public static function pack_reference($params) { + $params = (array)$params; + $reference = array(); + $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT); + $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT); + $reference['itemid'] = is_null($params['itemid']) ? null : clean_param($params['itemid'], PARAM_INT); + $reference['filearea'] = is_null($params['filearea']) ? null : clean_param($params['filearea'], PARAM_AREA); + $reference['filepath'] = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH);; + $reference['filename'] = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE); + return base64_encode(serialize($reference)); + } + + /** + * Unpack reference field + * + * @param string $str + * @return array + */ + public static function unpack_reference($str) { + return unserialize(base64_decode($str)); + } + + /** + * Search references by providing reference content + * + * @param string $str + * @return array + */ + public function search_references($str) { + global $DB; + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE r.reference = ? AND (f.component <> ? OR f.filearea <> ?)"; + + $rs = $DB->get_recordset_sql($sql, array($str, 'user', 'draft')); + $files = array(); + foreach ($rs as $filerecord) { + $file = $this->get_file_instance($filerecord); + if ($file->is_external_file()) { + $files[$filerecord->pathnamehash] = $file; + } + } + + return $files; + } + + /** + * Search references count by providing reference content + * + * @param string $str + * @return int + */ + public function search_references_count($str) { + global $DB; + $sql = "SELECT COUNT(f.id) + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE r.reference = ? AND (f.component <> ? OR f.filearea <> ?)"; + + $count = $DB->count_records_sql($sql, array($str, 'user', 'draft')); + return $count; + } + + /** + * Return all files referring to provided stored_file instance + * This won't work for draft files + * + * @param stored_file $storedfile + * @return array + */ + public function get_references_by_storedfile($storedfile) { + global $DB; + + $params = array(); + $params['contextid'] = $storedfile->get_contextid(); + $params['component'] = $storedfile->get_component(); + $params['filearea'] = $storedfile->get_filearea(); + $params['itemid'] = $storedfile->get_itemid(); + $params['filename'] = $storedfile->get_filename(); + $params['filepath'] = $storedfile->get_filepath(); + $params['userid'] = $storedfile->get_userid(); + + $reference = self::pack_reference($params); + + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE r.reference = ? AND (f.component <> ? OR f.filearea <> ?)"; + + $rs = $DB->get_recordset_sql($sql, array($reference, 'user', 'draft')); + $files = array(); + foreach ($rs as $filerecord) { + $file = $this->get_file_instance($filerecord); + if ($file->is_external_file()) { + $files[$filerecord->pathnamehash] = $file; + } + } + + return $files; + } + + /** + * Return the count files referring to provided stored_file instance + * This won't work for draft files + * + * @param stored_file $storedfile + * @return int + */ + public function get_references_count_by_storedfile($storedfile) { + global $DB; + + $params = array(); + $params['contextid'] = $storedfile->get_contextid(); + $params['component'] = $storedfile->get_component(); + $params['filearea'] = $storedfile->get_filearea(); + $params['itemid'] = $storedfile->get_itemid(); + $params['filename'] = $storedfile->get_filename(); + $params['filepath'] = $storedfile->get_filepath(); + $params['userid'] = $storedfile->get_userid(); + + $reference = self::pack_reference($params); + + $sql = "SELECT COUNT(f.id) + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE r.reference = ? AND (f.component <> ? OR f.filearea <> ?)"; + + $count = $DB->count_records_sql($sql, array($reference, 'user', 'draft')); + return $count; + } + + /** + * Convert file alias to local file + * + * @param stored_file $storedfile a stored_file instances + * @return stored_file stored_file + */ + public function import_external_file($storedfile) { + global $CFG; + require_once($CFG->dirroot.'/repository/lib.php'); + // sync external file + repository::sync_external_file($storedfile); + // Remove file references + $storedfile->delete_reference(); + return $storedfile; + } + + /** + * Return mimetype by given file pathname + * + * This method uses fileinfo module to get mimetype using magic bytes if file exists. + * If not, it will get mimetype based on filename + * + * @param string $pathname + * @return string + */ + public static function mimetype($pathname) { + if (file_exists($pathname)) { + $finfo = new finfo(FILEINFO_MIME_TYPE); + return mimeinfo_from_type('type', $finfo->file($pathname)); + } else { + return mimeinfo('type', $pathname); + } + } + /** * Cron cleanup job. */ diff --git a/lib/filestorage/file_types.mm b/lib/filestorage/file_types.mm deleted file mode 100644 index 71ff4fbdc09..00000000000 --- a/lib/filestorage/file_types.mm +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/filestorage/stored_file.php b/lib/filestorage/stored_file.php index bc19c0ba78b..6423d6b3e45 100644 --- a/lib/filestorage/stored_file.php +++ b/lib/filestorage/stored_file.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -require_once("$CFG->libdir/filestorage/stored_file.php"); +require_once("$CFG->dirroot/repository/lib.php"); /** * Class representing local files stored in a sha1 file pool. @@ -42,10 +42,12 @@ require_once("$CFG->libdir/filestorage/stored_file.php"); class stored_file { /** @var file_storage file storage pool instance */ private $fs; - /** @var stdClass record from the files table */ + /** @var stdClass record from the files table left join files_reference table */ private $file_record; /** @var string location of content files */ private $filedir; + /** @var repository repository plugin instance */ + public $repository; /** * Constructor, this constructor should be called ONLY from the file_storage class! @@ -55,9 +57,172 @@ class stored_file { * @param string $filedir location of file directory with sh1 named content files */ public function __construct(file_storage $fs, stdClass $file_record, $filedir) { + global $DB, $CFG; $this->fs = $fs; $this->file_record = clone($file_record); // prevent modifications $this->filedir = $filedir; // keep secret, do not expose! + + if (!empty($file_record->repositoryid)) { + $this->repository = repository::get_repository_by_id($file_record->repositoryid, SYSCONTEXTID); + if ($this->repository->supported_returntypes() & FILE_REFERENCE != FILE_REFERENCE) { + // Repository cannot do file reference. + throw new moodle_exception('error'); + } + } else { + $this->repository = null; + } + } + + /** + * Whether or not this is a external resource + * + * @return bool + */ + public function is_external_file() { + return !empty($this->repository); + } + + /** + * Update some file record fields + * NOTE: Must remain protected + * + * @param stdClass $dataobject + */ + protected function update($dataobject) { + global $DB; + $keys = array_keys((array)$this->file_record); + foreach ($dataobject as $field => $value) { + if (in_array($field, $keys)) { + if ($field == 'contextid' and (!is_number($value) or $value < 1)) { + throw new file_exception('storedfileproblem', 'Invalid contextid'); + } + + if ($field == 'component') { + $value = clean_param($value, PARAM_COMPONENT); + if (empty($value)) { + throw new file_exception('storedfileproblem', 'Invalid component'); + } + } + + if ($field == 'filearea') { + $value = clean_param($value, PARAM_AREA); + if (empty($value)) { + throw new file_exception('storedfileproblem', 'Invalid filearea'); + } + } + + if ($field == 'itemid' and (!is_number($value) or $value < 0)) { + throw new file_exception('storedfileproblem', 'Invalid itemid'); + } + + + if ($field == 'filepath') { + $value = clean_param($value, PARAM_PATH); + if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) { + // path must start and end with '/' + throw new file_exception('storedfileproblem', 'Invalid file path'); + } + } + + if ($field == 'filename') { + // folder has filename == '.', so we pass this + if ($value != '.') { + $value = clean_param($value, PARAM_FILE); + } + if ($value === '') { + throw new file_exception('storedfileproblem', 'Invalid file name'); + } + } + + if ($field === 'timecreated' or $field === 'timemodified') { + if (!is_number($value)) { + throw new file_exception('storedfileproblem', 'Invalid timestamp'); + } + if ($value < 0) { + $value = 0; + } + } + + if ($field == 'referencefileid' or $field == 'referencelastsync' or $field == 'referencelifetime') { + $value = clean_param($value, PARAM_INT); + } + + // adding the field + $this->file_record->$field = $value; + } else { + throw new coding_exception("Invalid field name, $field doesn't exist in file record"); + } + } + // Validate mimetype field + // we don't use {@link stored_file::get_content_file_location()} here becaues it will try to update file_record + $pathname = $this->get_pathname_by_contenthash(); + // try to recover the content from trash + if (!is_readable($pathname)) { + if (!$this->fs->try_content_recovery($this) or !is_readable($pathname)) { + throw new file_exception('storedfilecannotread', '', $pathname); + } + } + $mimetype = $this->fs->mimetype($pathname); + $this->file_record->mimetype = $mimetype; + + $DB->update_record('files', $this->file_record); + } + + /** + * Rename filename + * + * @param string $filepath file path + * @param string $filename file name + */ + public function rename($filepath, $filename) { + if ($this->fs->file_exists($this->get_contextid(), $this->get_component(), $this->get_filearea(), $this->get_itemid(), $filepath, $filename)) { + throw new file_exception('storedfilenotcreated', '', 'file exists, cannot rename'); + } + $filerecord = new stdClass; + $filerecord->filepath = $filepath; + $filerecord->filename = $filename; + // populate the pathname hash + $filerecord->pathnamehash = $this->fs->get_pathname_hash($this->file_record->contextid, $this->file_record->component, $this->file_record->filearea, $this->file_record->itemid, $filepath, $filename); + $this->update($filerecord); + } + + /** + * Replace the content by providing another stored_file instance + * + * @param stored_file $storedfile + */ + public function replace_content_with(stored_file $storedfile) { + $contenthash = $storedfile->get_contenthash(); + $this->set_contenthash($contenthash); + } + + /** + * Delete file reference + * + */ + public function delete_reference() { + global $DB; + + // Remove repository info. + $this->repository = null; + + // Remove reference info from DB. + $DB->delete_records('files_reference', array('id'=>$this->file_record->referencefileid)); + + // Must refresh $this->file_record form DB + $filerecord = $DB->get_record('files', array('id'=>$this->get_id())); + // Update DB + $filerecord->referencelastsync = null; + $filerecord->referencelifetime = null; + $filerecord->referencefileid = null; + $this->update($filerecord); + + // unset object variable + unset($this->file_record->repositoryid); + unset($this->file_record->reference); + unset($this->file_record->referencelastsync); + unset($this->file_record->referencelifetime); + unset($this->file_record->referencefileid); } /** @@ -83,13 +248,39 @@ class stored_file { */ public function delete() { global $DB; + // If other files referring to this file, we need convert them + if ($files = $this->fs->get_references_by_storedfile($this)) { + foreach ($files as $file) { + $this->fs->import_external_file($file); + } + } + // Now delete file records in DB $DB->delete_records('files', array('id'=>$this->file_record->id)); + $DB->delete_records('files_reference', array('id'=>$this->file_record->referencefileid)); // moves pool file to trash if content not needed any more $this->fs->deleted_file_cleanup($this->file_record->contenthash); return true; // BC only } /** + * Get file pathname by contenthash + * + * NOTE, this function is not calling sync_external_file, it assume the contenthash is current + * Protected - developers must not gain direct access to this function. + * + * @return string full path to pool file with file content + */ + protected function get_pathname_by_contenthash() { + // Detect is local file or not. + $contenthash = $this->file_record->contenthash; + $l1 = $contenthash[0].$contenthash[1]; + $l2 = $contenthash[2].$contenthash[3]; + return "$this->filedir/$l1/$l2/$contenthash"; + } + + /** + * Get file pathname by given contenthash, this method will try to sync files + * * Protected - developers must not gain direct access to this function. * * NOTE: do not make this public, we must not modify or delete the pool files directly! ;-) @@ -97,10 +288,8 @@ class stored_file { * @return string full path to pool file with file content **/ protected function get_content_file_location() { - $contenthash = $this->file_record->contenthash; - $l1 = $contenthash[0].$contenthash[1]; - $l2 = $contenthash[2].$contenthash[3]; - return "$this->filedir/$l1/$l2/$contenthash"; + $this->sync_external_file(); + return $this->get_pathname_by_contenthash(); } /** @@ -128,7 +317,7 @@ class stored_file { throw new file_exception('storedfilecannotread', '', $path); } } - return fopen($path, 'rb'); //binary reading only!! + return fopen($path, 'rb'); // Binary reading only!! } /** @@ -241,7 +430,14 @@ class stored_file { * @return mixed array with width, height and mimetype; false if not an image */ public function get_imageinfo() { - if (!$imageinfo = getimagesize($this->get_content_file_location())) { + $path = $this->get_content_file_location(); + if (!is_readable($path)) { + if (!$this->fs->try_content_recovery($this) or !is_readable($path)) { + throw new file_exception('storedfilecannotread', '', $path); + } + } + $mimetype = $this->get_mimetype(); + if (!preg_match('|^image/|', $mimetype) || !filesize($path) || !($imageinfo = getimagesize($path))) { return false; } $image = array('width'=>$imageinfo[0], 'height'=>$imageinfo[1], 'mimetype'=>image_type_to_mime_type($imageinfo[2])); @@ -261,7 +457,7 @@ class stored_file { */ public function is_valid_image() { $mimetype = $this->get_mimetype(); - if ($mimetype !== 'image/gif' and $mimetype !== 'image/jpeg' and $mimetype !== 'image/png') { + if (!file_mimetype_in_typegroup($mimetype, 'web_image')) { return false; } if (!$info = $this->get_imageinfo()) { @@ -300,7 +496,33 @@ class stored_file { } /** - * Returns context id of the file- + * Sync external files + * + * @return bool true if file content changed, false if not + */ + public function sync_external_file() { + global $CFG, $DB; + if (empty($this->file_record->referencefileid)) { + return false; + } + if (empty($this->file_record->referencelastsync) or ($this->file_record->referencelastsync + $this->file_record->referencelifetime < time())) { + require_once($CFG->dirroot.'/repository/lib.php'); + if (repository::sync_external_file($this)) { + $prevcontent = $this->file_record->contenthash; + $sql = "SELECT f.*, r.repositoryid, r.reference + FROM {files} f + LEFT JOIN {files_reference} r + ON f.referencefileid = r.id + WHERE f.id = ?"; + $this->file_record = $DB->get_record_sql($sql, array($this->file_record->id), MUST_EXIST); + return ($prevcontent !== $this->file_record->contenthash); + } + } + return false; + } + + /** + * Returns context id of the file * * @return int context id */ @@ -370,9 +592,21 @@ class stored_file { * @return int bytes */ public function get_filesize() { + $this->sync_external_file(); return $this->file_record->filesize; } + /** + * Returns the size of file in bytes. + * + * @param int $filesize bytes + */ + public function set_filesize($filesize) { + $filerecord = new stdClass; + $filerecord->filesize = $filesize; + $this->update($filerecord); + } + /** * Returns mime type of file. * @@ -397,9 +631,21 @@ class stored_file { * @return int */ public function get_timemodified() { + $this->sync_external_file(); return $this->file_record->timemodified; } + /** + * set timemodified + * + * @param int $timemodified + */ + public function set_timemodified($timemodified) { + $filerecord = new stdClass; + $filerecord->timemodified = $timemodified; + $this->update($filerecord); + } + /** * Returns file status flag. * @@ -424,9 +670,26 @@ class stored_file { * @return string */ public function get_contenthash() { + $this->sync_external_file(); return $this->file_record->contenthash; } + /** + * Set contenthash + * + * @param string $contenthash + */ + protected function set_contenthash($contenthash) { + // make sure the content exists in moodle file pool + if ($this->fs->content_exists($contenthash)) { + $filerecord = new stdClass; + $filerecord->contenthash = $contenthash; + $this->update($filerecord); + } else { + throw new file_exception('storedfileproblem', 'Invalid contenthash, content must be already in filepool', $contenthash); + } + } + /** * Returns sha1 hash of all file path components sha1("contextid/component/filearea/itemid/dir/dir/filename.ext"). * @@ -445,6 +708,17 @@ class stored_file { return $this->file_record->license; } + /** + * Set license + * + * @param string $license license + */ + public function set_license($license) { + $filerecord = new stdClass; + $filerecord->license = $license; + $this->update($filerecord); + } + /** * Returns the author name of the file. * @@ -454,6 +728,17 @@ class stored_file { return $this->file_record->author; } + /** + * Set author + * + * @param string $author + */ + public function set_author($author) { + $filerecord = new stdClass; + $filerecord->author = $author; + $this->update($filerecord); + } + /** * Returns the source of the file, usually it is a url. * @@ -463,6 +748,18 @@ class stored_file { return $this->file_record->source; } + /** + * Set license + * + * @param string $license license + */ + public function set_source($source) { + $filerecord = new stdClass; + $filerecord->source = $source; + $this->update($filerecord); + } + + /** * Returns the sort order of file * @@ -471,4 +768,82 @@ class stored_file { public function get_sortorder() { return $this->file_record->sortorder; } + + /** + * Set file sort order + * + * @param int $sortorder + * @return int + */ + public function set_sortorder($sortorder) { + $filerecord = new stdClass; + $filerecord->sortorder = $sortorder; + $this->update($filerecord); + } + + /** + * Returns repository id + * + * @return int|null + */ + public function get_repository_id() { + if (!empty($this->repository)) { + return $this->repository->id; + } else { + return null; + } + } + + /** + * get reference file id + * @return int + */ + public function get_referencefileid() { + return $this->file_record->referencefileid; + } + + /** + * Get reference last sync time + * @return int + */ + public function get_referencelastsync() { + return $this->file_record->referencelastsync; + } + + /** + * Get reference last sync time + * @return int + */ + public function get_referencelifetime() { + return $this->file_record->referencelifetime; + } + /** + * Returns file reference + * + * @return string + */ + public function get_reference() { + return $this->file_record->reference; + } + + /** + * Get human readable file reference information + * + * @return string + */ + public function get_reference_details() { + return $this->repository->get_reference_details($this->get_reference()); + } + + /** + * Send file references + * + * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours) + * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only + * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin + * @param array $options additional options affecting the file serving + */ + public function send_file($lifetime, $filter, $forcedownload, $options) { + $this->repository->send_file($this, $lifetime, $filter, $forcedownload, $options); + } } diff --git a/lib/filestorage/tests/file_storage_test.php b/lib/filestorage/tests/file_storage_test.php index ba31f8043a9..5f70959c17d 100644 --- a/lib/filestorage/tests/file_storage_test.php +++ b/lib/filestorage/tests/file_storage_test.php @@ -77,4 +77,134 @@ class filestoragelib_testcase extends advanced_testcase { $this->assertInstanceOf('stored_file', $previewtinyicon); $this->assertEquals('6b9864ae1536a8eeef54e097319175a8be12f07c', $previewtinyicon->get_filename()); } + + /** + * Make sure renaming is working + * + * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} + */ + public function test_file_renaming() { + global $CFG; + + $this->resetAfterTest(true); + $fs = get_file_storage(); + $syscontext = context_system::instance(); + $component = 'core'; + $filearea = 'unittest'; + $itemid = 0; + $filepath = '/'; + $filename = 'test.txt'; + + $filerecord = array( + 'contextid' => $syscontext->id, + 'component' => $component, + 'filearea' => $filearea, + 'itemid' => $itemid, + 'filepath' => $filepath, + 'filename' => $filename, + ); + + $originalfile = $fs->create_file_from_string($filerecord, 'Test content'); + $this->assertInstanceOf('stored_file', $originalfile); + $contenthash = $originalfile->get_contenthash(); + $newpath = '/test/'; + $newname = 'newtest.txt'; + + // this should work + $originalfile->rename($newpath, $newname); + $file = $fs->get_file($syscontext->id, $component, $filearea, $itemid, $newpath, $newname); + $this->assertInstanceOf('stored_file', $file); + $this->assertEquals($contenthash, $file->get_contenthash()); + + // try break it + $this->setExpectedException('file_exception'); + // this shall throw exception + $originalfile->rename($newpath, $newname); + } + + /** + * Create file from reference tests + * + * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} + */ + public function test_create_file_from_reference() { + global $CFG, $DB; + + $this->resetAfterTest(true); + // create user + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $usercontext = context_user::instance($user->id); + $syscontext = context_system::instance(); + $USER = $DB->get_record('user', array('id'=>$user->id)); + + $fs = get_file_storage(); + + $repositorypluginname = 'user'; + // override repository permission + $capability = 'repository/' . $repositorypluginname . ':view'; + $allroles = $DB->get_records_menu('role', array(), 'id', 'archetype, id'); + assign_capability($capability, CAP_ALLOW, $allroles['guest'], $syscontext->id, true); + + + $args = array(); + $args['type'] = $repositorypluginname; + $repos = repository::get_instances($args); + $userrepository = reset($repos); + $this->assertInstanceOf('repository', $userrepository); + + $component = 'user'; + $filearea = 'private'; + $itemid = 0; + $filepath = '/'; + $filename = 'userfile.txt'; + + $filerecord = array( + 'contextid' => $usercontext->id, + 'component' => $component, + 'filearea' => $filearea, + 'itemid' => $itemid, + 'filepath' => $filepath, + 'filename' => $filename, + ); + + $content = 'Test content'; + $originalfile = $fs->create_file_from_string($filerecord, $content); + $this->assertInstanceOf('stored_file', $originalfile); + + $newfilerecord = array( + 'contextid' => $syscontext->id, + 'component' => 'core', + 'filearea' => 'phpunit', + 'itemid' => 0, + 'filepath' => $filepath, + 'filename' => $filename, + ); + $ref = $fs->pack_reference($filerecord); + $newstoredfile = $fs->create_file_from_reference($newfilerecord, $userrepository->id, $ref); + $this->assertInstanceOf('stored_file', $newstoredfile); + $this->assertEquals($userrepository->id, $newstoredfile->repository->id); + $this->assertEquals($originalfile->get_contenthash(), $newstoredfile->get_contenthash()); + $this->assertEquals($originalfile->get_filesize(), $newstoredfile->get_filesize()); + $this->assertRegExp('#' . $filename. '$#', $newstoredfile->get_reference_details()); + + // Test looking for references + $count = $fs->get_references_count_by_storedfile($originalfile); + $this->assertEquals(1, $count); + $files = $fs->get_references_by_storedfile($originalfile); + $file = reset($files); + $this->assertEquals($file, $newstoredfile); + + // Look for references by repository ID + $files = $fs->get_external_files($userrepository->id); + $file = reset($files); + $this->assertEquals($file, $newstoredfile); + + // Try convert reference to local file + $importedfile = $fs->import_external_file($newstoredfile); + $this->assertFalse($importedfile->is_external_file()); + $this->assertInstanceOf('stored_file', $importedfile); + // still readable? + $this->assertEquals($content, $importedfile->get_content()); + } } diff --git a/lib/form/dndupload.js b/lib/form/dndupload.js index 385440ab7a6..bf7316a794a 100644 --- a/lib/form/dndupload.js +++ b/lib/form/dndupload.js @@ -64,18 +64,20 @@ M.form_dndupload.init = function(Y, options) { * maxfiles: maximum number of files this form allows * maxbytes: maximum size of files allowed in this form * clientid: unqiue id of this form field used for html elements - * containerprefix: prefix of htmlid of container + * containerid: htmlid of container * repositories: array of repository objects passed from filepicker * filemanager: filemanager element we are working with - * callback: callback to filepicker element to refesh when uploaded + * formcallback: callback to filepicker element to refesh when uploaded * } */ init: function(Y, options) { this.Y = Y; if (!this.browser_supported()) { + Y.one('body').addClass('dndnotsupported'); return; // Browser does not support the required functionality } + Y.one('body').addClass('dndsupported'); // try and retrieve enabled upload repository this.repositoryid = this.get_upload_repositoryid(options.repositories); @@ -89,21 +91,12 @@ M.form_dndupload.init = function(Y, options) { this.maxfiles = options.maxfiles; this.maxbytes = options.maxbytes; this.itemid = options.itemid; - this.container = this.Y.one(options.containerprefix + this.clientid); + this.container = this.Y.one('#'+options.containerid); if (options.filemanager) { // Needed to tell the filemanager to redraw when files uploaded // and to check how many files are already uploaded this.filemanager = options.filemanager; - // Add a callback to show the 'drag and drop enabled' message - // within the filemanager box once it has finished loading, - // if there are no files yet uploaded - this.filemanager.emptycallback = function(clientid) { - var el = Y.one('#dndenabled2-'+clientid); - if (el) { - el.setStyle('display', 'inline'); - } - } } else if (options.formcallback) { // Needed to tell the filepicker to update when a new @@ -111,14 +104,13 @@ M.form_dndupload.init = function(Y, options) { this.callback = options.formcallback; } else { if (M.cfg.developerdebug) { - alert('dndupload: Need to define either options.filemanager or options.callback'); + alert('dndupload: Need to define either options.filemanager or options.formcallback'); } return; } this.init_events(); this.init_page_events(); - this.Y.one('#dndenabled-'+this.clientid).setStyle('display', 'inline'); }, /** @@ -361,11 +353,11 @@ M.form_dndupload.init = function(Y, options) { * Highlight the area where files could be dropped */ show_drop_target: function() { - this.Y.one('#filemanager-uploadmessage'+this.clientid).setStyle('display', 'block'); + this.container.addClass('dndupload-ready'); }, hide_drop_target: function() { - this.Y.one('#filemanager-uploadmessage'+this.clientid).setStyle('display', 'none'); + this.container.removeClass('dndupload-ready'); }, /** @@ -386,20 +378,14 @@ M.form_dndupload.init = function(Y, options) { * Display a progress spinner in the destination node */ show_progress_spinner: function() { - // add a loading spinner to show something is happening - var loadingspinner = this.Y.Node.create('
      '); - loadingspinner.append(''); - this.container.append(loadingspinner); + this.container.addClass('dndupload-uploading'); }, /** * Remove progress spinner in the destination node */ hide_progress_spinner: function() { - var spinner = this.Y.one('#dndprogresspinner-'+this.clientid); - if (spinner) { - spinner.remove(); - } + this.container.removeClass('dndupload-uploading'); }, /** diff --git a/lib/form/editor.php b/lib/form/editor.php index a993ffd467a..fdbbaf9faad 100644 --- a/lib/form/editor.php +++ b/lib/form/editor.php @@ -303,7 +303,7 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element { $args = new stdClass(); // need these three to filter repositories list $args->accepted_types = array('image'); - $args->return_types = (FILE_INTERNAL | FILE_EXTERNAL); + $args->return_types = (FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE); $args->context = $ctx; $args->env = 'filepicker'; // advimage plugin @@ -407,4 +407,4 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element { return ''; } -} \ No newline at end of file +} diff --git a/lib/form/filemanager.js b/lib/form/filemanager.js index 915fe90627b..53a07c31b7e 100644 --- a/lib/form/filemanager.js +++ b/lib/form/filemanager.js @@ -26,6 +26,9 @@ * this.filecount, how many files in this filemanager * this.maxfiles * this.maxbytes + * this.filemanager, contains reference to filemanager Node + * this.selectnode, contains referenct to select-file Node + * this.selectui, YUI Panel to select the file * * FileManager options: * ===== @@ -34,7 +37,11 @@ */ -M.form_filemanager = {}; +M.form_filemanager = {templates:{}}; + +M.form_filemanager.set_templates = function(Y, templates) { + M.form_filemanager.templates = templates; +} /** * This fucntion is called for each file picker on page. @@ -76,25 +83,50 @@ M.form_filemanager.init = function(Y, options) { } else { this.filecount = 0; } + // prepare filemanager for drag-and-drop upload + this.filemanager = Y.one('#filemanager-'+options.client_id); + if (this.filemanager.hasClass('filemanager-container') || !this.filemanager.one('.filemanager-container')) { + this.dndcontainer = this.filemanager; + } else { + this.dndcontainer = this.filemanager.one('.filemanager-container'); + if (!this.dndcontainer.get('id')) { + this.dndcontainer.generateID(); + } + } + // save template for one path element and location of path bar + if (this.filemanager.one('.fp-path-folder')) { + this.pathnode = this.filemanager.one('.fp-path-folder'); + this.pathbar = this.pathnode.get('parentNode'); + this.pathbar.removeChild(this.pathnode); + } + // initialize 'select file' panel + this.selectnode = Y.Node.create(M.form_filemanager.templates.fileselectlayout); + this.selectnode.generateID(); + Y.one(document.body).appendChild(this.selectnode); + this.selectui = new Y.Panel({ + srcNode : this.selectnode, + zIndex : 600000, + centered : true, + modal : true, + close : true, + render : true + }); + this.selectui.plug(Y.Plugin.Drag,{handles:['#'+this.selectnode.get('id')+' .yui3-widget-hd']}); + this.selectui.hide(); + this.setup_select_file(); + // setup buttons onclick events this.setup_buttons(); + // set event handler for lazy loading of thumbnails + this.filemanager.one('.fp-content').on(['scroll','resize'], this.content_scrolled, this); + // display files + this.viewmode = 1; // TODO take from cookies? + this.filemanager.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details').removeClass('checked') + this.filemanager.all('.fp-vb-icons').addClass('checked') this.refresh(this.currentpath); // MDL-31113 get latest list from server }, - wait: function(client_id) { - var container = Y.one('#filemanager-'+client_id); - container.set('innerHTML', ''); - var html = Y.Node.create('
        '); - container.appendChild(html); - var panel = Y.one('#draftfiles-'+client_id); - var name = ''; - var str = '
        '; - str += ''; - str += '
        '; - try { - panel.set('innerHTML', str); - } catch(e) { - alert(e.toString()); - } + wait: function() { + this.filemanager.addClass('fm-updating'); }, request: function(args, redraw) { var api = this.api + '?action='+args.action; @@ -120,7 +152,18 @@ M.form_filemanager.init = function(Y, options) { alert('IO FATAL'); return; } - var data = Y.JSON.parse(o.responseText); + var data = null; + try { + data = Y.JSON.parse(o.responseText); + } catch(e) { + // TODO display error + scope.print_msg(M.str.repository.invalidjson, 'error'); + //scope.display_error(M.str.repository.invalidjson+'
        '+stripHTML(o.responseText)+'
        ', 'invalidjson') + return; + } + if (data && data.tree && scope.set_current_tree) { + scope.set_current_tree(data.tree); + } args.callback(id,data,p); } }, @@ -138,7 +181,7 @@ M.form_filemanager.init = function(Y, options) { } Y.io(api, cfg); if (redraw) { - this.wait(this.client_id); + this.wait(); } }, filepicker_callback: function(obj) { @@ -150,13 +193,11 @@ M.form_filemanager.init = function(Y, options) { } }, check_buttons: function() { - var button_addfile = Y.one("#btnadd-"+this.client_id); - if (this.filecount > 0) { - Y.one("#btndwn-"+this.client_id).setStyle('display', 'inline'); - } - if (this.filecount >= this.maxfiles && this.maxfiles!=-1) { - button_addfile.setStyle('display', 'none'); - } + if (this.filecount>0) {this.filemanager.removeClass('fm-nofiles');} + else {this.filemanager.addClass('fm-nofiles');} + if (this.filecount >= this.maxfiles && this.maxfiles!=-1) + {this.filemanager.addClass('fm-maxfiles');} + else {this.filemanager.removeClass('fm-maxfiles');} }, refresh: function(filepath) { var scope = this; @@ -174,39 +215,70 @@ M.form_filemanager.init = function(Y, options) { scope.filecount = obj.filecount; scope.check_buttons(); scope.options = obj; + scope.lazyloading = {}; scope.render(obj); } }, true); }, + /** displays message in a popup */ + print_msg: function(msg, type) { + var header = M.str.moodle.error; + if (type != 'error') { + type = 'info'; // one of only two types excepted + header = M.str.moodle.info; + } + if (!this.msg_dlg) { + var node = Y.Node.create(M.form_filemanager.templates.message); + this.filemanager.appendChild(node); + + this.msg_dlg = new Y.Panel({ + srcNode : node, + zIndex : 800000, + centered : true, + modal : true, + visible : false, + render : true + }); + this.msg_dlg.plug(Y.Plugin.Drag,{handles:['.yui3-widget-hd']}); + node.one('.fp-msg-butok').on('click', function(e) { + e.preventDefault(); + this.msg_dlg.hide(); + }, this); + } + + this.msg_dlg.set('headerContent', header); + this.filemanager.one('.fp-msg').removeClass('fp-msg-info').removeClass('fp-msg-error').addClass('fp-msg-'+type) + this.filemanager.one('.fp-msg .fp-msg-text').setContent(msg); + this.msg_dlg.show(); + }, setup_buttons: function() { - var button_download = Y.one("#btndwn-"+this.client_id); - var button_create = Y.one("#btncrt-"+this.client_id); - var button_addfile = Y.one("#btnadd-"+this.client_id); + var button_download = this.filemanager.one('.fp-btn-download'); + var button_create = this.filemanager.one('.fp-btn-mkdir'); + var button_addfile = this.filemanager.one('.fp-btn-add'); // setup 'add file' button // if maxfiles == -1, the no limit - if (this.filecount >= this.maxfiles - && this.maxfiles!=-1) { - button_addfile.setStyle('display', 'none'); - } else { - button_addfile.on('click', function(e) { - var options = this.filepicker_options; - options.formcallback = this.filepicker_callback; - // XXX: magic here, to let filepicker use filemanager scope - options.magicscope = this; - options.savepath = this.currentpath; - M.core_filepicker.show(Y, options); - }, this); - } + button_addfile.on('click', function(e) { + e.preventDefault(); + var options = this.filepicker_options; + options.formcallback = this.filepicker_callback; + // XXX: magic here, to let filepicker use filemanager scope + options.magicscope = this; + options.savepath = this.currentpath; + M.core_filepicker.show(Y, options); + }, this); // setup 'make a folder' button if (this.options.subdirs) { button_create.on('click',function(e) { + e.preventDefault(); var scope = this; // a function used to perform an ajax request - function perform_action(e) { - var foldername = Y.one('#fm-newname').get('value'); + var perform_action = function(e) { + e.preventDefault(); + var foldername = Y.one('#fm-newname-'+scope.client_id).get('value'); if (!foldername) { + scope.mkdir_dialog.hide(); return; } scope.request({ @@ -216,39 +288,45 @@ M.form_filemanager.init = function(Y, options) { var filepath = obj.filepath; scope.mkdir_dialog.hide(); scope.refresh(filepath); - Y.one('#fm-newname').set('value', ''); + Y.one('#fm-newname-'+scope.client_id).set('value', ''); if (typeof M.core_formchangechecker != 'undefined') { M.core_formchangechecker.set_form_changed(); } } }); } - if (!Y.one('#fm-mkdir-dlg')) { - var dialog = Y.Node.create('
        '+M.str.repository.entername+'
        '); - Y.one(document.body).appendChild(dialog); - this.mkdir_dialog = new YAHOO.widget.Dialog("fm-mkdir-dlg", { - width: "300px", - visible: true, - x:e.pageX, - y:e.pageY, - constraintoviewport : true - }); - + if (!this.mkdir_dialog) { + var node = Y.Node.create(M.form_filemanager.templates.mkdir); + this.filemanager.appendChild(node); + this.mkdir_dialog = new Y.Panel({ + srcNode : node, + zIndex : 800000, + centered : true, + modal : true, + visible : false, + render : true + }); + this.mkdir_dialog.plug(Y.Plugin.Drag,{handles:['.yui3-widget-hd']}); + node.one('.fp-dlg-butcreate').on('click', perform_action, this); + node.one('input').set('id', 'fm-newname-'+this.client_id). + on('keydown', function(e){ + if (e.keyCode == 13) {Y.bind(perform_action, this)(e);} + }, this); + node.all('.fp-dlg-butcancel').on('click', function(e){e.preventDefault();this.mkdir_dialog.hide();}, this); + node.all('.fp-dlg-curpath').set('id', 'fm-curpath-'+this.client_id); } - var buttons = [ { text:M.str.moodle.ok, handler:perform_action, isDefault:true }, - { text:M.str.moodle.cancel, handler:function(){this.cancel();}}]; - - this.mkdir_dialog.cfg.queueProperty("buttons", buttons); - this.mkdir_dialog.render(); this.mkdir_dialog.show(); + Y.one('#fm-newname-'+scope.client_id).focus(); + Y.all('#fm-curpath-'+scope.client_id).setContent(this.currentpath) }, this); } else { - button_create.setStyle('display', 'none'); + this.filemanager.addClass('fm-nomkdir'); } // setup 'download this folder' button // NOTE: popup window must be enabled to perform download process - button_download.on('click',function() { + button_download.on('click',function(e) { + e.preventDefault(); var scope = this; // perform downloaddir ajax request this.request({ @@ -259,552 +337,598 @@ M.form_filemanager.init = function(Y, options) { scope.refresh(obj.filepath); var win = window.open(obj.fileurl, 'fm-download-folder'); if (!win) { - alert(M.str.repository.popupblockeddownload); + scope.print_msg(M.str.repository.popupblockeddownload, 'error'); } } else { - alert(M.str.repository.draftareanofiles); + scope.print_msg(M.str.repository.draftareanofiles, 'error'); } } }); }, this); + + this.filemanager.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details'). + on('click', function(e) { + e.preventDefault(); + var viewbar = this.filemanager.one('.fp-viewbar') + if (!viewbar || !viewbar.hasClass('disabled')) { + this.filemanager.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details').removeClass('checked') + if (e.currentTarget.hasClass('fp-vb-tree')) { + this.viewmode = 2; + } else if (e.currentTarget.hasClass('fp-vb-details')) { + this.viewmode = 3; + } else { + this.viewmode = 1; + } + e.currentTarget.addClass('checked') + this.render(); + //Y.Cookie.set('recentviewmode', this.viewmode); + } + }, this); }, - empty_filelist: function(container) { - var content = '
        '+M.str.repository.nofilesattached; - content += ''; - content += '
        '; - content += this.upload_message(); - container.set('innerHTML', content); - if (this.emptycallback) { - this.emptycallback(this.client_id); + print_path: function() { + var p = this.options.path; + this.pathbar.setContent('').addClass('empty'); + if (p && p.length!=0 && this.viewmode != 2) { + for(var i = 0; i < p.length; i++) { + var el = this.pathnode.cloneNode(true); + this.pathbar.appendChild(el); + if (i == 0) {el.addClass('first');} + if (i == p.length-1) {el.addClass('last');} + if (i%2) {el.addClass('even');} else {el.addClass('odd');} + el.one('.fp-path-folder-name').setContent(p[i].name). + on('click', function(e, path) { + e.preventDefault(); + this.refresh(path); + }, this, p[i].path); + } + this.pathbar.removeClass('empty'); } }, - upload_message: function() { - var div = ''; - return div; + get_filepath: function(obj) { + if (obj.path && obj.path.length) { + return obj.path[obj.path.length-1].path; + } + return ''; }, - render: function() { - var options = this.options; - var path = this.options.path; - var list = this.options.list; - var breadcrumb = Y.one('#fm-path-'+this.client_id); - // build breadcrumb - if (path) { - // empty breadcrumb - breadcrumb.set('innerHTML', ''); - var count = 0; - for(var p in path) { - var arrow = ''; - if (count==0) { - arrow = Y.Node.create(''+M.str.moodle.path + ': '); + treeview_dynload: function(node, cb) { + var retrieved_children = {}; + if (node.children) { + for (var i in node.children) { + retrieved_children[node.children[i].path] = node.children[i]; + } + } + if (!node.path || node.path == '/') { + // this is a root pseudo folder + node.fileinfo.filepath = '/'; + node.fileinfo.type = 'folder'; + node.fileinfo.fullname = node.fileinfo.title; + node.fileinfo.filename = '.'; + } + this.request({ + action:'list', + params: {filepath:node.path?node.path:''}, + scope:this, + callback: function(id, obj, args) { + var list = obj.list; + var scope = args.scope; + // check that user did not leave the view mode before recieving this response + if (!(scope.viewmode == 2 && node && node.getChildrenEl())) { + return; + } + if (cb != null) { // (in manual mode do not update current path) + scope.options = obj; + scope.currentpath = node.path?node.path:'/'; + } + node.highlight(false); + node.origlist = obj.list?obj.list:null; + node.origpath = obj.path?obj.path:null; + node.children = []; + for(k in list) { + if (list[k].type == 'folder' && retrieved_children[list[k].filepath]) { + // if this child is a folder and has already been retrieved + retrieved_children[list[k].filepath].fileinfo = list[k]; + node.children[node.children.length] = retrieved_children[list[k].filepath]; + } else { + // append new file to the list + scope.view_files([list[k]]); + } + } + if (cb == null) { + node.refresh(); } else { - arrow = Y.Node.create(''); + // invoke callback requested by TreeView component + cb(); } - count++; - - var pathid = 'fm-path-node-'+this.client_id; - pathid += ('-'+count); - - var crumb = Y.Node.create(''+path[p].name+''); - breadcrumb.appendChild(arrow); - breadcrumb.appendChild(crumb); - - var args = {}; - args.requestpath = path[p].path; - args.client_id = this.client_id; - Y.one('#'+pathid).on('click', function(e, args) { - var scope = this; - var params = {}; - params['filepath'] = args.requestpath; - this.currentpath = args.requestpath; - this.request({ - action: 'list', - scope: scope, - params: params, - callback: function(id, obj, args) { - scope.filecount = obj.filecount; - scope.check_buttons(); - scope.options = obj; - scope.render(obj); - } - }, true); - }, this, args); + scope.content_scrolled(); } - } - var template = Y.one('#fm-template'); - var container = Y.one('#filemanager-' + this.client_id); - var listhtml = ''; - - // folder list items - var folder_ids = []; - var folder_data = {}; - - // normal file list items - var file_ids = []; - var file_data = {}; - - // archives list items - var zip_ids = []; - var zip_data = {}; - - var html_ids = []; - var html_data = {}; - - file_data.itemid = folder_data.itemid = zip_data.itemid = options.itemid; - file_data.client_id = folder_data.client_id = zip_data.client_id = this.client_id; - - var foldername_ids = []; - if (!list || list.length == 0) { - // hide file browser and breadcrumb - //container.setStyle('display', 'none'); - this.empty_filelist(container); - if (!path || path.length <= 1) { - breadcrumb.setStyle('display', 'none'); + }, false); + }, + content_scrolled: function(e) { + setTimeout(Y.bind(function() { + if (this.processingimages) {return;} + this.processingimages = true; + var scope = this, + fpcontent = this.filemanager.one('.fp-content'), + fpcontenty = fpcontent.getY(), + fpcontentheight = fpcontent.getStylePx('height'), + is_node_visible = function(node) { + var offset = node.getY()-fpcontenty; + if (offset <= fpcontentheight && (offset >=0 || offset+node.getStylePx('height')>=0)) { + return true; + } + return false; + }; + // replace src for visible images that need to be lazy-loaded + if (scope.lazyloading) { + fpcontent.all('img').each( function(node) { + if (node.get('id') && scope.lazyloading[node.get('id')] && is_node_visible(node)) { + node.set('src', scope.lazyloading[node.get('id')]); + delete scope.lazyloading[node.get('id')]; + } + }); } + this.processingimages = false; + }, this), 200) + }, + view_files: function(appendfiles) { + this.filemanager.removeClass('fm-updating').removeClass('fm-noitems'); + if ((appendfiles == null) && (!this.options.list || this.options.list.length == 0) && this.viewmode != 2) { + this.filemanager.addClass('fm-noitems'); return; + } + var list = (appendfiles != null) ? appendfiles : this.options.list; + var element_template; + if (this.viewmode == 2 || this.viewmode == 3) { + element_template = Y.Node.create(M.form_filemanager.templates.listfilename); } else { - container.setStyle('display', 'block'); - breadcrumb.setStyle('display', 'block'); + this.viewmode = 1; + element_template = Y.Node.create(M.form_filemanager.templates.iconfilename); } - - var count = 0; - for(var i in list) { - count++; - // the li html element - var htmlid = 'fileitem-'+this.client_id+'-'+count; - // link to file - var fileid = 'filename-'+this.client_id+'-'+count; - // file menu - var action = 'action-' +this.client_id+'-'+count; - - var html = template.get('innerHTML'); - - html_ids.push('#'+htmlid); - html_data[htmlid] = action; - - list[i].htmlid = htmlid; - list[i].fileid = fileid; - list[i].action = action; - - var url = "###"; - - switch (list[i].type) { - case 'folder': - // click folder name - foldername_ids.push('#'+fileid); - // click folder menu - folder_ids.push('#'+action); - folder_data[action] = list[i]; - folder_data[fileid] = list[i]; - break; - case 'file': - file_ids.push('#'+action); - // click file name - file_ids.push('#'+fileid); - file_data[action] = list[i]; - file_data[fileid] = list[i]; - if (list[i].url) { - url = list[i].url; - } - break; - case 'zip': - zip_ids.push('#'+action); - zip_ids.push('#'+fileid); - zip_data[action] = list[i]; - zip_data[fileid] = list[i]; - if (list[i].url) { - url = list[i].url; - } - break; + var options = { + viewmode : this.viewmode, + appendonly : appendfiles != null, + filenode : element_template, + callbackcontext : this, + callback : function(e, node) { + if (e.preventDefault) { e.preventDefault(); } + if (node.type == 'folder') { + this.refresh(node.filepath); + } else { + this.select_file(node); + } + }, + rightclickcallback : function(e, node) { + if (e.preventDefault) { e.preventDefault(); } + this.select_file(node); + }, + classnamecallback : function(node) { + var classname = ''; + if (node.type == 'folder' || (!node.type && !node.filename)) { + classname = classname + ' fp-folder'; + } + if (node.filename || node.filepath || (node.path && node.path != '/')) { + classname = classname + ' fp-hascontextmenu'; + } + if (node.isref) { + classname = classname + ' fp-isreference'; + } + if (node.refcount) { + classname = classname + ' fp-hasreferences'; + } + if (node.sortorder == 1) { classname = classname + ' fp-mainfile';} + return Y.Lang.trim(classname); } - var fullname = list[i].fullname; - - if (list[i].sortorder == 1) { - html = html.replace('___fullname___', ' ' + fullname + ''); - } else { - html = html.replace('___fullname___', ' ' + fullname + ''); + }; + if (this.viewmode == 2) { + options.dynload = true; + options.filepath = this.options.path; + options.treeview_dynload = this.treeview_dynload; + options.norootrightclick = true; + options.callback = function(e, node) { + // TODO MDL-32736 e is not an event here but an object with properties 'event' and 'node' + if (!node.fullname) {return;} + if (node.type != 'folder') { + if (e.node.parent && e.node.parent.origpath) { + // set the current path + this.options.path = e.node.parent.origpath; + this.options.list = e.node.parent.origlist; + this.print_path(); + } + this.currentpath = node.filepath; + this.select_file(node); + } else { + // save current path and filelist (in case we want to jump to other viewmode) + this.options.path = e.node.origpath; + this.options.list = e.node.origlist; + this.currentpath = node.filepath; + this.print_path(); + //this.content_scrolled(); + } + }; + } + if (!this.lazyloading) {this.lazyloading={};} + this.filemanager.one('.fp-content').fp_display_filelist(options, list, this.lazyloading); + this.content_scrolled(); + }, + populate_licenses_select: function(node) { + if (!node) {return;} + node.setContent(''); + var licenses = this.options.licenses; + for (var i in licenses) { + var option = Y.Node.create('
        - -
        -FMHTML; - if (empty($filemanagertemplateloaded)) { - $filemanagertemplateloaded = true; - $html .= <<
        '; - html += ''; - html += ''; + var selectnode = this.selectnode; + var return_types = this.options.repositories[this.active_repo.id].return_types; + selectnode.removeClass('loading'); + selectnode.one('.fp-saveas input').set('value', args.title); - var le_checked = ''; - var le_style = ''; - if (this.options.repositories[this.active_repo.id].return_types == 1) { - // support external links only - le_checked = 'checked'; - le_style = ' style="display:none;"'; - } else if(this.options.repositories[this.active_repo.id].return_types == 2) { - // support internal files only - le_style = ' style="display:none;"'; - } - if ((this.options.externallink && this.options.env == 'editor' && this.options.return_types != 1)) { - html += ''; - } + var imgnode = Y.Node.create(''). + set('src', args.realthumbnail ? args.realthumbnail : args.thumbnail). + setStyle('maxHeight', ''+(args.thumbnail_height ? args.thumbnail_height : 90)+'px'). + setStyle('maxWidth', ''+(args.thumbnail_width ? args.thumbnail_width : 90)+'px'); + selectnode.one('.fp-thumbnail').setContent('').appendChild(imgnode); - if (!args.hasauthor) { - // the author of the file - html += ''; - html += ''; - html += ''; - } - - if (!args.haslicense) { - // the license of the file - var licenses = this.options.licenses; - html += ''; - html += ''; + filelink[filelinktypes[i]] = allowed; + firstfilelink = (firstfilelink==null && allowed) ? filelinktypes[i] : firstfilelink; + filelinkcount += allowed ? 1 : 0; + } + // make radio buttons enabled if this file-link-type is available and only if there are more than one file-link-type option + // check the first available file-link-type option + for (var linktype in filelink) { + var el = selectnode.one('.fp-linktype-'+linktype); + el.addClassIf('uneditable', !(filelink[linktype] && filelinkcount>1)); + el.one('input').set('disabled', (filelink[linktype] && filelinkcount>1)?'':'disabled'). + set('checked', (firstfilelink == linktype) ? 'checked' : '').simulate('change') } - html += '
        '+M.str.repository.linkexternal+'
        '; - html += '

        '; - html += ''; - html += '

        '; - html += ''; + // TODO MDL-32532: attributes 'hasauthor' and 'haslicense' need to be obsolete, + selectnode.one('.fp-setauthor input').set('value', args.author?args.author:this.options.author); + this.set_selected_license(selectnode.one('.fp-setlicense'), args.license); + selectnode.one('form #filesource-'+client_id).set('value', args.source); - var getfile_form = Y.Node.create(html); - panel.appendChild(getfile_form); - - var getfile = Y.one('#fp-confirm-'+client_id); + // display static information about a file (when known) + var attrs = ['datemodified','datecreated','size','license','author','dimensions']; + for (var i in attrs) { + if (selectnode.one('.fp-'+attrs[i])) { + var value = (args[attrs[i]+'_f']) ? args[attrs[i]+'_f'] : (args[attrs[i]] ? args[attrs[i]] : ''); + selectnode.one('.fp-'+attrs[i]).addClassIf('fp-unknown', ''+value == '') + .one('.fp-value').setContent(value); + } + } + }, + setup_select_file: function() { + var client_id = this.options.client_id; + var selectnode = this.selectnode; + var getfile = selectnode.one('.fp-select-confirm'); + // bind labels with corresponding inputs + selectnode.all('.fp-saveas,.fp-linktype-2,.fp-linktype-1,.fp-linktype-4,.fp-setauthor,.fp-setlicense').each(function (node) { + node.all('label').set('for', node.one('input,select').generateID()); + }); + selectnode.one('.fp-linktype-2 input').setAttrs({value: 2, name: 'linktype'}); + selectnode.one('.fp-linktype-1 input').setAttrs({value: 1, name: 'linktype'}); + selectnode.one('.fp-linktype-4 input').setAttrs({value: 4, name: 'linktype'}); + var changelinktype = function(e) { + if (e.currentTarget.get('checked')) { + var allowinputs = e.currentTarget.get('value') != 1/*FILE_EXTERNAL*/; + selectnode.all('.fp-setauthor,.fp-setlicense,.fp-saveas').each(function(node){ + node.addClassIf('uneditable', !allowinputs); + node.all('input,select').set('disabled', allowinputs?'':'disabled'); + }); + } + }; + selectnode.all('.fp-linktype-2,.fp-linktype-1,.fp-linktype-4').each(function (node) { + node.one('input').on('change', changelinktype, this); + }); + this.populate_licenses_select(selectnode.one('.fp-setlicense select')); + // register event on clicking submit button getfile.on('click', function(e) { + e.preventDefault(); var client_id = this.options.client_id; var scope = this; var repository_id = this.active_repo.id; - var title = Y.one('#newname-'+client_id).get('value'); - var filesource = Y.one('#filesource-'+client_id).get('value'); + var title = selectnode.one('.fp-saveas input').get('value'); + var filesource = selectnode.one('form #filesource-'+client_id).get('value'); var params = {'title':title, 'source':filesource, 'savepath': this.options.savepath}; - var license = Y.one('#select-license-'+client_id); + var license = selectnode.one('.fp-setlicense select'); if (license) { params['license'] = license.get('value'); - YAHOO.util.Cookie.set('recentlicense', license.get('value')); - } - var author = Y.one('#text-author-'+client_id); - if (author){ - params['author'] = author.get('value'); - } - - if (this.options.externallink && this.options.env == 'editor') { - // in editor, images are stored in '/' only - params.savepath = '/'; - // when image or media button is clicked - if ( this.options.return_types != 1 ) { - var linkexternal = Y.one('#linkexternal-'+client_id); - if (linkexternal && linkexternal.get('checked')) { - params['linkexternal'] = 'yes'; - } - } else { - // when link button in editor clicked - params['linkexternal'] = 'yes'; + var origlicense = selectnode.one('.fp-license .fp-value'); + if (origlicense) { origlicense = origlicense.getContent(); } + var newlicenseval = license.get('value'); + if (newlicenseval && this.options.licenses[newlicenseval] != origlicense) { + Y.Cookie.set('recentlicense', newlicenseval); } } + params['author'] = selectnode.one('.fp-setauthor input').get('value'); - if (this.options.env == 'url') { + var return_types = this.options.repositories[this.active_repo.id].return_types; + if (this.options.env == 'editor') { + // in editor, images are stored in '/' only + params.savepath = '/'; + } + if ((this.options.externallink || this.options.env != 'editor') && + (return_types & 1/*FILE_EXTERNAL*/) && + (this.options.return_types & 1/*FILE_EXTERNAL*/) && + selectnode.one('.fp-linktype-1 input').get('checked')) { params['linkexternal'] = 'yes'; + } else if ((return_types & 4/*FILE_REFERENCE*/) && + (this.options.return_types & 4/*FILE_REFERENCE*/) && + selectnode.one('.fp-linktype-4 input').get('checked')) { + params['usefilereference'] = 'yes'; } - this.wait('download', title); + selectnode.addClass('loading'); this.request({ action:'download', client_id: client_id, repository_id: repository_id, 'params': params, onerror: function(id, obj, args) { - scope.view_files(); + selectnode.removeClass('loading'); + scope.selectui.hide(); }, callback: function(id, obj, args) { + selectnode.removeClass('loading'); + if (obj.event == 'fileexists') { + scope.process_existing_file(obj); + return; + } if (scope.options.editor_target && scope.options.env=='editor') { scope.options.editor_target.value=obj.url; scope.options.editor_target.onchange(); } scope.hide(); obj.client_id = client_id; - var formcallback_scope = null; - if (args.scope.options.magicscope) { - formcallback_scope = args.scope.options.magicscope; - } else { - formcallback_scope = args.scope; - } + var formcallback_scope = args.scope.options.magicscope ? args.scope.options.magicscope : args.scope; scope.options.formcallback.apply(formcallback_scope, [obj]); } - }, true); + }, false); }, this); - var elform = Y.one('#'+form_id); + var elform = selectnode.one('form'); + elform.appendChild(Y.Node.create(''). + setAttrs({type:'hidden',id:'filesource-'+client_id})); elform.on('keydown', function(e) { if (e.keyCode == 13) { getfile.simulate('click'); e.preventDefault(); } }, this); - var cancel = Y.one('#fp-cancel-'+client_id); + var cancel = selectnode.one('.fp-select-cancel'); cancel.on('click', function(e) { - this.view_files(); + e.preventDefault(); + this.selectui.hide(); }, this); - var treeview = Y.one('#treeview-'+client_id); - if (treeview){ - treeview.setStyle('display', 'none'); - } }, - wait: function(type) { - var panel = Y.one('#panel-'+this.options.client_id); - panel.set('innerHTML', ''); - var name = ''; - var str = '
        '; - if(type=='load') { - str += ''; - str += '

        '+M.str.repository.loading+'

        '; - }else{ - str += ''; - str += '

        '+M.str.repository.copying+' '+name+'

        '; + wait: function() { + this.fpnode.one('.fp-content').setContent(M.core_filepicker.templates.loading); + }, + viewbar_set_enabled: function(mode) { + var viewbar = this.fpnode.one('.fp-viewbar') + if (viewbar) { + if (mode) { + viewbar.addClass('enabled').removeClass('disabled') + } else { + viewbar.removeClass('enabled').addClass('disabled') + } } - str += '
        '; - try { - panel.set('innerHTML', str); - } catch(e) { - alert(e.toString()); + this.fpnode.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details').removeClass('checked') + var modes = {1:'icons', 2:'tree', 3:'details'}; + this.fpnode.all('.fp-vb-'+modes[this.viewmode]).addClass('checked'); + }, + viewbar_clicked: function(e) { + e.preventDefault(); + var viewbar = this.fpnode.one('.fp-viewbar') + if (!viewbar || !viewbar.hasClass('disabled')) { + if (e.currentTarget.hasClass('fp-vb-tree')) { + this.viewmode = 2; + } else if (e.currentTarget.hasClass('fp-vb-details')) { + this.viewmode = 3; + } else { + this.viewmode = 1; + } + this.viewbar_set_enabled(true) + this.view_files(); + Y.Cookie.set('recentviewmode', this.viewmode); } }, render: function() { var client_id = this.options.client_id; - var scope = this; - var filepicker_id = 'filepicker-'+client_id; - var fpnode = Y.Node.create('
        '); - Y.one(document.body).appendChild(fpnode); - // render file picker panel - this.mainui = new YAHOO.widget.Panel(filepicker_id, { - draggable: true, - close: true, - underlay: 'none', - zindex: 9999990, - monitorresize: false, - xy: [50, YAHOO.util.Dom.getDocumentScrollTop()+20] + this.fpnode = Y.Node.create(M.core_filepicker.templates.generallayout). + set('id', 'filepicker-'+client_id); + this.selectnode = Y.Node.create(M.core_filepicker.templates.selectlayout); + Y.one(document.body).appendChild(this.fpnode); + this.mainui = new Y.Panel({ + srcNode : this.fpnode, + headerContent: M.str.repository.filepicker, + zIndex : 500000, + centered : true, + modal : true, + visible : false, + minWidth : this.fpnode.getStylePx('minWidth'), + minHeight : this.fpnode.getStylePx('minHeight'), + maxWidth : this.fpnode.getStylePx('maxWidth'), + maxHeight : this.fpnode.getStylePx('maxHeight'), + render : true }); - var layout = null; - this.mainui.beforeRenderEvent.subscribe(function() { - YAHOO.util.Event.onAvailable('layout-'+client_id, function() { - layout = new YAHOO.widget.Layout('layout-'+client_id, { - height: 480, width: 700, - units: [ - {position: 'top', height: 32, resize: false, - body:'
        ', gutter: '2'}, - {position: 'left', width: 200, resize: true, scroll:true, - body:'
          ', gutter: '0 5 0 2', minWidth: 150, maxWidth: 300 }, - {position: 'center', body: '
          ', - scroll: true, gutter: '0 2 0 0' } - ] - }); - layout.render(); - scope.show_recent_repository(); - }); + // allow to move the panel dragging it by it's header: + this.mainui.plug(Y.Plugin.Drag,{handles:['#filepicker-'+client_id+' .yui3-widget-hd']}); + this.mainui.show(); + if (this.mainui.get('y')<0) {this.mainui.set('y', 0);} + // create panel for selecting a file (initially hidden) + this.selectnode = Y.Node.create(M.core_filepicker.templates.selectlayout). + set('id', 'filepicker-select-'+client_id); + Y.one(document.body).appendChild(this.selectnode); + this.selectui = new Y.Panel({ + srcNode : this.selectnode, + zIndex : 600000, + centered : true, + modal : true, + close : true, + render : true }); + // allow to move the panel dragging it by it's header: + this.selectui.plug(Y.Plugin.Drag,{handles:['#filepicker-select-'+client_id+' .yui3-widget-hd']}); + this.selectui.hide(); + // event handler for lazy loading of thumbnails and next page + this.fpnode.one('.fp-content').on(['scroll','resize'], this.content_scrolled, this); + // save template for one path element and location of path bar + if (this.fpnode.one('.fp-path-folder')) { + this.pathnode = this.fpnode.one('.fp-path-folder'); + this.pathbar = this.pathnode.get('parentNode'); + this.pathbar.removeChild(this.pathnode); + } + // assign callbacks for view mode switch buttons + this.fpnode.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details'). + on('click', this.viewbar_clicked, this); + // assign callbacks for toolbar links + this.setup_toolbar(); + this.setup_select_file(); + this.hide_header(); - this.mainui.setHeader(M.str.repository.filepicker); - this.mainui.setBody('
          '); - this.mainui.render(); - this.rendered = true; - - var scope = this; - // adding buttons - var view_icons = {label: M.str.repository.iconview, value: 't', 'checked': true, - onclick: { - fn: function(){ - scope.view_as_icons(); - } - } - }; - var view_listing = {label: M.str.repository.listview, value: 'l', - onclick: { - fn: function(){ - scope.view_as_list(); - } - } - }; - this.viewbar = new YAHOO.widget.ButtonGroup({ - id: 'btngroup-'+client_id, - name: 'buttons', - disabled: true, - container: 'fp-viewbar-'+client_id - }); - this.viewbar.addButtons([view_icons, view_listing]); // processing repository listing - var r = this.options.repositories; - Y.on('contentready', function(el) { - var list = Y.one(el); - var count = 0; - // Resort the repositories by sortorder - var sorted_repositories = new Array(); - for (var i in r) { - sorted_repositories[r[i].sortorder - 1] = r[i]; + // Resort the repositories by sortorder + var sorted_repositories = [] + for (var i in this.options.repositories) { + sorted_repositories[i] = this.options.repositories[i] + } + sorted_repositories.sort(function(a,b){return a.sortorder-b.sortorder}) + // extract one repository template and repeat it for all repositories available, + // set name and icon and assign callbacks + var reponode = this.fpnode.one('.fp-repo'); + if (reponode) { + var list = reponode.get('parentNode'); + list.removeChild(reponode); + for (i in sorted_repositories) { + var repository = sorted_repositories[i] + var node = reponode.cloneNode(true); + list.appendChild(node); + node. + set('id', 'fp-repo-'+client_id+'-'+repository.id). + on('click', function(e, repository_id) { + e.preventDefault(); + Y.Cookie.set('recentrepository', repository_id); + this.hide_header(); + this.list({'repo_id':repository_id}); + }, this /*handler running scope*/, repository.id/*second argument of handler*/); + node.one('.fp-repo-name').setContent(repository.name) + node.one('.fp-repo-icon').set('src', repository.icon) + if (i==0) {node.addClass('first');} + if (i==sorted_repositories.length-1) {node.addClass('last');} + if (i%2) {node.addClass('even');} else {node.addClass('odd');} } - for (var i in sorted_repositories){ - repository = sorted_repositories[i]; - var id = 'repository-'+client_id+'-'+repository.id; - var link_id = id + '-link'; - list.append('
        • '+repository.name+'
        • '); - Y.one('#'+link_id).prepend(' '); - Y.one('#'+link_id).on('click', function(e, scope, repository_id) { - YAHOO.util.Cookie.set('recentrepository', repository_id); - scope.repository_id = repository_id; - this.list({'repo_id':repository_id}); - }, this /*handler running scope*/, this/*second argument*/, repository.id/*third argument of handler*/); - count++; - } - if (count==0) { - if (this.options.externallink) { - list.set('innerHTML', M.str.repository.norepositoriesexternalavailable); - } else { - list.set('innerHTML', M.str.repository.norepositoriesavailable); + } + // display error if no repositories found + if (sorted_repositories.length==0) { + this.display_error(M.str.repository.norepositoriesavailable, 'norepositoriesavailable') + } + // display repository that was used last time + this.show_recent_repository(); + }, + parse_repository_options: function(data, appendtolist) { + if (appendtolist) { + if (data.list) { + if (!this.filelist) { this.filelist = []; } + for (var i in data.list) { + this.filelist[this.filelist.length] = data.list[i]; } } - }, '#fp-list-'+client_id, this /* handler running scope */, '#fp-list-'+client_id /*first argument of handler*/); - }, - parse_repository_options: function(data) { - this.filelist = data.list?data.list:null; + } else { + this.filelist = data.list?data.list:null; + this.lazyloading = {}; + } this.filepath = data.path?data.path:null; + this.objecttag = data.object?data.object:null; this.active_repo = {}; - this.active_repo.issearchresult = Boolean(data.issearchresult); + this.active_repo.issearchresult = data.issearchresult?true:false; this.active_repo.dynload = data.dynload?data.dynload:false; this.active_repo.pages = Number(data.pages?data.pages:null); this.active_repo.page = Number(data.page?data.page:null); + this.active_repo.hasmorepages = (this.active_repo.pages && this.active_repo.page && (this.active_repo.page < this.active_repo.pages || this.active_repo.pages == -1)) this.active_repo.id = data.repo_id?data.repo_id:null; - this.active_repo.nosearch = data.nosearch?true:false; - this.active_repo.norefresh = data.norefresh?true:false; - this.active_repo.nologin = data.nologin?true:false; + this.active_repo.nosearch = (data.login || data.nosearch); // this is either login form or 'nosearch' attribute set + this.active_repo.norefresh = (data.login || data.norefresh); // this is either login form or 'norefresh' attribute set + this.active_repo.nologin = (data.login || data.nologin); // this is either login form or 'nologin' attribute is set this.active_repo.logouttext = data.logouttext?data.logouttext:null; this.active_repo.help = data.help?data.help:null; this.active_repo.manage = data.manage?data.manage:null; + this.print_header(); }, print_login: function(data) { this.parse_repository_options(data); @@ -865,341 +1272,254 @@ M.core_filepicker.init = function(Y, options) { var repository_id = data.repo_id; var l = this.logindata = data.login; var loginurl = ''; - var panel = Y.one('#panel-'+client_id); - var action = 'login'; - if (data['login_btn_action']) { - action=data['login_btn_action']; - } + var action = data['login_btn_action'] ? data['login_btn_action'] : 'login'; var form_id = 'fp-form-'+client_id; - var download_button_id = 'fp-form-download-button-'+client_id; - var search_button_id = 'fp-form-search-button-'+client_id; - var login_button_id = 'fp-form-login-button-'+client_id; - var popup_button_id = 'fp-form-popup-button-'+client_id; - var str = '
          '; - str += '
          '; - var has_pop = false; - str +=''; + var loginform_node = Y.Node.create(M.core_filepicker.templates.loginform); + loginform_node.one('form').set('id', form_id); + this.fpnode.one('.fp-content').setContent('').appendChild(loginform_node); + var templates = { + 'popup' : loginform_node.one('.fp-login-popup'), + 'textarea' : loginform_node.one('.fp-login-textarea'), + 'select' : loginform_node.one('.fp-login-select'), + 'text' : loginform_node.one('.fp-login-text'), + 'radio' : loginform_node.one('.fp-login-radiogroup'), + 'checkbox' : loginform_node.one('.fp-login-checkbox'), + 'input' : loginform_node.one('.fp-login-input') + }; + var container; + for (var i in templates) { + if (templates[i]) { + container = templates[i].get('parentNode'); + container.removeChild(templates[i]) + } + } + for(var k in l) { - str +=''; - if(l[k].type=='popup') { - // pop element + if (templates[l[k].type]) { + var node = templates[l[k].type].cloneNode(true); + } else { + node = templates['input'].cloneNode(true); + } + if (l[k].type == 'popup') { + // submit button loginurl = l[k].url; - str += ''; + var popupbutton = node.one('button'); + popupbutton.on('click', function(e){ + M.core_filepicker.active_filepicker = this; + window.open(loginurl, 'repo_auth', 'location=0,status=0,width=500,height=300,scrollbars=yes'); + e.preventDefault(); + }, this); + loginform_node.one('form').on('keydown', function(e) { + if (e.keyCode == 13) { + popupbutton.simulate('click'); + e.preventDefault(); + } + }, this); + loginform_node.all('.fp-login-submit').remove(); action = 'popup'; }else if(l[k].type=='textarea') { // textarea element - str += ''; + if (node.one('label')) { node.one('label').set('for', l[k].id).setContent(l[k].label) } + node.one('textarea').setAttrs({id:l[k].id, name:l[k].name}); }else if(l[k].type=='select') { // select element - str += ''; - str += ''; - }else{ + }else if(l[k].type=='radio') { + // radio input element + node.all('label').setContent(l[k].label); + var list = l[k].value.split('|'); + var labels = l[k].value_label.split('|'); + var radionode = null; + for(var item in list) { + if (radionode == null) { + radionode = node.one('.fp-login-radio'); + radionode.one('input').set('checked', 'checked'); + } else { + var x = radionode.cloneNode(true); + radionode.insert(x, 'after'); + radionode = x; + radionode.one('input').set('checked', ''); + } + radionode.one('input').setAttrs({id:''+l[k].id+item, name:l[k].name, + type:l[k].type, value:list[item]}); + radionode.all('label').setContent(labels[item]).set('for', ''+l[k].id+item) + } + if (radionode == null) { + node.one('.fp-login-radio').remove(); + } + }else { // input element - var label_id = ''; - var field_id = ''; - var field_value = ''; - if(l[k].id) { - label_id = ' for="'+l[k].id+'"'; - field_id = ' id="'+l[k].id+'"'; - } - if (l[k].label) { - str += ''; - } else { - str += ''; - } - if(l[k].value) { - field_value = ' value="'+l[k].value+'"'; - } - if(l[k].type=='radio'){ - var list = l[k].value.split('|'); - var labels = l[k].value_label.split('|'); - str += ''; - }else{ - str += ''; - } + if (node.one('label')) { node.one('label').set('for', l[k].id).setContent(l[k].label) } + node.one('input'). + set('type', l[k].type). + set('id', l[k].id). + set('name', l[k].name). + set('value', l[k].value?l[k].value:'') } - str +=''; + container.appendChild(node); } - str +='

          '+M.str.repository.popup+'

          '; - str += '

          '; - str += '

          '; - str += ''+l[k].label+' '; - for(var item in list) { - str +=''+labels[item]+'
          '; - } - str += '
          '; - str += ''; - str += '
          '; - str += '
          '; - - // custom lable text - var btn_label = data['login_btn_label']?data['login_btn_label']:M.str.repository.submit; - if (action != 'popup') { - str += '

          '; + // custom label text for submit button + if (data['login_btn_label']) { + loginform_node.all('.fp-login-submit').setContent(data['login_btn_label']) } - - str += '
          '; - - // insert login form - try { - panel.set('innerHTML', str); - } catch(e) { - alert(M.str.repository.xhtmlerror); - } - // register buttons - // process login action - var login_button = Y.one('#'+login_button_id); - var scope = this; - if (login_button) { - login_button.on('click', function(){ - // collect form data - var data = this.logindata; - var scope = this; - var params = {}; - for (var k in data) { - if(data[k].type!='popup') { - var el = Y.one('[name='+data[k].name+']'); - var type = el.get('type'); - params[data[k].name] = ''; - if(type == 'checkbox') { - params[data[k].name] = el.get('checked'); - } else { - params[data[k].name] = el.get('value'); - } - } - } - // start ajax request + // register button action for login and search + if (action == 'login' || action == 'search') { + loginform_node.one('.fp-login-submit').on('click', function(e){ + e.preventDefault(); + this.hide_header(); this.request({ - 'params': params, - 'scope': scope, - 'action':'signin', + 'scope': this, + 'action':(action == 'search') ? 'search' : 'signin', 'path': '', 'client_id': client_id, 'repository_id': repository_id, - 'callback': function(id, o, args) { - scope.parse_repository_options(o); - scope.view_files(); - } + 'form': {id:form_id, upload:false, useDisabled:true}, + 'callback': this.display_response }, true); }, this); } - var search_button = Y.one('#'+search_button_id); - if (search_button) { - search_button.on('click', function(){ - var data = this.logindata; - var params = {}; - - for (var k in data) { - if(data[k].type!='popup') { - var el = document.getElementsByName(data[k].name)[0]; - params[data[k].name] = ''; - if(el.type == 'checkbox') { - params[data[k].name] = el.checked; - } else if(el.type == 'radio') { - var tmp = document.getElementsByName(data[k].name); - for(var i in tmp) { - if (tmp[i].checked) { - params[data[k].name] = tmp[i].value; - } - } - } else { - params[data[k].name] = el.value; - } - } + // if 'Enter' is pressed in the form, simulate the button click + if (loginform_node.one('.fp-login-submit')) { + loginform_node.one('form').on('keydown', function(e) { + if (e.keyCode == 13) { + loginform_node.one('.fp-login-submit').simulate('click') + e.preventDefault(); } - this.request({ - scope: scope, - action:'search', - client_id: client_id, - repository_id: repository_id, - form: {id: 'fp-form-'+scope.options.client_id,upload:false,useDisabled:true}, - callback: function(id, o, args) { - o.issearchresult = true; - scope.parse_repository_options(o); - scope.view_files(); - } - }, true); }, this); } - var download_button = Y.one('#'+download_button_id); - if (download_button) { - download_button.on('click', function(){ - alert('download'); - }); - } - var popup_button = Y.one('#'+popup_button_id); - if (popup_button) { - popup_button.on('click', function(e){ - M.core_filepicker.active_filepicker = this; - window.open(loginurl, 'repo_auth', 'location=0,status=0,width=500,height=300,scrollbars=yes'); - e.preventDefault(); - }, this); - } - var elform = Y.one('#'+form_id); - elform.on('keydown', function(e) { - if (e.keyCode == 13) { - switch (action) { - case 'search': - search_button.simulate('click'); - break; - default: - login_button.simulate('click'); - break; - } - e.preventDefault(); - } - }, this); - }, - search: function(args) { - var data = this.logindata; - var params = {}; - - for (var k in data) { - if(data[k].type!='popup') { - var el = document.getElementsByName(data[k].name)[0]; - params[data[k].name] = ''; - if(el.type == 'checkbox') { - params[data[k].name] = el.checked; - } else if(el.type == 'radio') { - var tmp = document.getElementsByName(data[k].name); - for(var i in tmp) { - if (tmp[i].checked) { - params[data[k].name] = tmp[i].value; - } - } - } else { - params[data[k].name] = el.value; - } - } + display_response: function(id, obj, args) { + var scope = args.scope + // highlight the current repository in repositories list + scope.fpnode.all('.fp-repo.active').removeClass('active'); + scope.fpnode.all('#fp-repo-'+scope.options.client_id+'-'+obj.repo_id).addClass('active') + // add class repository_REPTYPE to the filepicker (for repository-specific styles) + for (var i in scope.options.repositories) { + scope.fpnode.removeClass('repository_'+scope.options.repositories[i].type) + } + if (obj.repo_id && scope.options.repositories[obj.repo_id]) { + scope.fpnode.addClass('repository_'+scope.options.repositories[obj.repo_id].type) + } + // display response + if (obj.login) { + scope.viewbar_set_enabled(false); + scope.print_login(obj); + } else if (obj.upload) { + scope.viewbar_set_enabled(false); + scope.parse_repository_options(obj); + scope.create_upload_form(obj); + } else if (obj.object) { + M.core_filepicker.active_filepicker = scope; + scope.viewbar_set_enabled(false); + scope.parse_repository_options(obj); + scope.create_object_container(obj.object); + } else if (obj.list) { + scope.viewbar_set_enabled(true); + scope.parse_repository_options(obj); + scope.view_files(); } - this.request({ - scope: scope, - action:'search', - client_id: client_id, - repository_id: repository_id, - form: {id: 'fp-form-'+scope.options.client_id,upload:false,useDisabled:true}, - callback: function(id, o, args) { - o.issearchresult = true; - scope.parse_repository_options(o); - scope.view_files(); - } - }, true); }, list: function(args) { - var scope = this; if (!args) { args = {}; } if (!args.repo_id) { - args.repo_id = scope.active_repo.id; + args.repo_id = this.active_repo.id; } - scope.request({ - action:'list', - client_id: scope.options.client_id, + this.request({ + action: 'list', + client_id: this.options.client_id, repository_id: args.repo_id, - path:args.path?args.path:'', - page:args.page?args.page:'', - callback: function(id, obj, args) { - Y.all('#fp-list-'+scope.options.client_id+' li a').setStyle('backgroundColor', 'transparent'); - var el = Y.one('#repository-'+scope.options.client_id+'-'+obj.repo_id+'-link'); - if (el) { - el.setStyle('backgroundColor', '#AACCEE'); - } - if (obj.login) { - scope.viewbar.set('disabled', true); - scope.print_login(obj); - } else if (obj.upload) { - scope.viewbar.set('disabled', true); - scope.parse_repository_options(obj); - scope.create_upload_form(obj); - - } else if (obj.iframe) { - - } else if (obj.list) { - obj.issearchresult = false; - scope.viewbar.set('disabled', false); - scope.parse_repository_options(obj); - scope.view_files(); - } - } + path: args.path, + page: args.page, + scope: this, + callback: this.display_response }, true); }, - create_upload_form: function(data) { - var client_id = this.options.client_id; - Y.one('#panel-'+client_id).set('innerHTML', ''); - var types = this.options.accepted_types; - - this.print_header(); - var id = data.upload.id+'_'+client_id; - var str = '
          '; - str += '
          '; - str += ''; - str += ''; - str += ''; - str += ''; - str += ''; - for (var i in types) { - str += ''; + populate_licenses_select: function(node) { + if (!node) { + return; } - str += ''; - str += ''; - str += ''; - str += ''; - str += ''; - str += ''; - str += ''; - str += '
          '; - str += ''; - str += '
          '+M.str.repository.chooselicense+': '; + node.setContent(''); var licenses = this.options.licenses; - str += ''; - str += '
          '; - str += '
          '; - str += '
          '; - str += '
          '; - var upload_form = Y.Node.create(str); - Y.one('#panel-'+client_id).appendChild(upload_form); + }, + set_selected_license: function(node, value) { + var licenseset = false; + node.all('option').each(function(el) { + if (el.get('value')==value || el.getContent()==value) { + el.set('selected', true); + licenseset = true; + } + }); + if (!licenseset) { + // we did not find the value in the list + var recentlicense = Y.Cookie.get('recentlicense'); + node.all('option[selected]').set('selected', false); + node.all('option[value='+recentlicense+']').set('selected', true); + } + }, + create_object_container: function(data) { + var content = this.fpnode.one('.fp-content'); + content.setContent(''); + //var str = ''; + var container = Y.Node.create(''). + setAttrs({data:data.src, type:data.type, id:'container_object'}). + addClass('fp-object-container'); + content.setContent('').appendChild(container); + }, + create_upload_form: function(data) { + var client_id = this.options.client_id; + var id = data.upload.id+'_'+client_id; + var content = this.fpnode.one('.fp-content'); + content.setContent(M.core_filepicker.templates.uploadform); + + content.all('.fp-file,.fp-saveas,.fp-setauthor,.fp-setlicense').each(function (node) { + node.all('label').set('for', node.one('input,select').generateID()); + }); + content.one('form').set('id', id); + content.one('.fp-file input').set('name', 'repo_upload_file'); + content.one('.fp-saveas input').set('name', 'title'); + content.one('.fp-setauthor input').setAttrs({name:'author', value:this.options.author}); + content.one('.fp-setlicense select').set('name', 'license'); + this.populate_licenses_select(content.one('.fp-setlicense select')) + // append hidden inputs to the upload form + content.one('form').appendChild(Y.Node.create(''). + setAttrs({type:'hidden',name:'itemid',value:this.options.itemid})); + var types = this.options.accepted_types; + for (var i in types) { + content.one('form').appendChild(Y.Node.create(''). + setAttrs({type:'hidden',name:'accepted_types[]',value:types[i]})); + } + var scope = this; - Y.one('#'+id+'_action').on('click', function(e) { + content.one('.fp-upload-btn').on('click', function(e) { e.preventDefault(); - var license = Y.one('#select-license-'+client_id).get('value'); - YAHOO.util.Cookie.set('recentlicense', license); - if (!Y.one('#'+id+'_file').get('value')) { + var license = content.one('.fp-setlicense select'); + Y.Cookie.set('recentlicense', license.get('value')); + if (!content.one('.fp-file input').get('value')) { scope.print_msg(M.str.repository.nofilesattached, 'error'); return false; } + this.hide_header(); scope.request({ scope: scope, action:'upload', @@ -1211,302 +1531,186 @@ M.core_filepicker.init = function(Y, options) { scope.create_upload_form(data); }, callback: function(id, o, args) { + if (o.event == 'fileexists') { + scope.create_upload_form(data); + scope.process_existing_file(o); + return; + } if (scope.options.editor_target&&scope.options.env=='editor') { scope.options.editor_target.value=o.url; scope.options.editor_target.onchange(); } scope.hide(); o.client_id = client_id; - var formcallback_scope = null; - if (args.scope.options.magicscope) { - formcallback_scope = args.scope.options.magicscope; - } else { - formcallback_scope = args.scope; - } + var formcallback_scope = args.scope.options.magicscope ? args.scope.options.magicscope : args.scope; scope.options.formcallback.apply(formcallback_scope, [o]); } }, true); }, this); }, + /** setting handlers and labels for elements in toolbar. Called once during the initial render of filepicker */ + setup_toolbar: function() { + var client_id = this.options.client_id; + var toolbar = this.fpnode.one('.fp-toolbar'); + toolbar.one('.fp-tb-logout').one('a,button').on('click', function(e) { + e.preventDefault(); + if (!this.active_repo.nologin) { + this.hide_header(); + this.request({ + action:'logout', + client_id: this.options.client_id, + repository_id: this.active_repo.id, + path:'', + callback: this.display_response + }, true); + } + }, this); + toolbar.one('.fp-tb-refresh').one('a,button').on('click', function(e) { + e.preventDefault(); + if (!this.active_repo.norefresh) { + this.list(); + } + }, this); + toolbar.one('.fp-tb-search form'). + set('method', 'POST'). + set('id', 'fp-tb-search-'+client_id). + on('submit', function(e) { + e.preventDefault(); + if (!this.active_repo.nosearch) { + this.request({ + scope: this, + action:'search', + client_id: this.options.client_id, + repository_id: this.active_repo.id, + form: {id: 'fp-tb-search-'+client_id, upload:false, useDisabled:true}, + callback: this.display_response + }, true); + } + }, this); + + // it does not matter what kind of element is .fp-tb-manage, we create a dummy + // element and use it to open url on click event + var managelnk = Y.Node.create(''). + setAttrs({id:'fp-tb-manage-'+client_id+'-link', target:'_blank'}). + setStyle('display', 'none'); + toolbar.append(managelnk); + toolbar.one('.fp-tb-manage').one('a,button'). + on('click', function(e) { + e.preventDefault(); + managelnk.simulate('click') + }); + + // same with .fp-tb-help + var helplnk = Y.Node.create(''). + setAttrs({id:'fp-tb-help-'+client_id+'-link', target:'_blank'}). + setStyle('display', 'none'); + toolbar.append(helplnk); + toolbar.one('.fp-tb-manage').one('a,button'). + on('click', function(e) { + e.preventDefault(); + helplnk.simulate('click') + }); + }, + hide_header: function() { + if (this.fpnode.one('.fp-toolbar')) { + this.fpnode.one('.fp-toolbar').addClass('empty'); + } + if (this.pathbar) { + this.pathbar.setContent('').addClass('empty'); + } + }, print_header: function() { var r = this.active_repo; var scope = this; var client_id = this.options.client_id; - var repository_id = this.active_repo.id; - var panel = Y.one('#panel-'+client_id); - var str = '
          '; - str += '
          '; - str += '
          '; - var head = Y.Node.create(str); - panel.appendChild(head); - //if(this.active_repo.pages < 8){ - this.print_paging('header'); - //} + this.hide_header(); + this.print_path(); + var toolbar = this.fpnode.one('.fp-toolbar'); + if (!toolbar) { return; } - var toolbar = Y.one('#repo-tb-'+client_id); + var enable_tb_control = function(node, enabled) { + if (!node) { return; } + node.addClassIf('disabled', !enabled).addClassIf('enabled', enabled) + if (enabled) { + toolbar.removeClass('empty'); + } + } + // TODO 'back' permanently disabled for now. Note, flickr_public uses 'Logout' for it! + enable_tb_control(toolbar.one('.fp-tb-back'), false); + + // search form + enable_tb_control(toolbar.one('.fp-tb-search'), !r.nosearch); if(!r.nosearch) { - var html = '
          '+M.str.repository.search+''; - var search = Y.Node.create(html); - search.on('click', function() { - scope.request({ - scope: scope, - action:'searchform', - repository_id: repository_id, - callback: function(id, obj, args) { - var scope = args.scope; - var client_id = scope.options.client_id; - var repository_id = scope.active_repo.id; - var container = document.getElementById('fp-search-dlg'); - if(container) { - container.innerHTML = ''; - container.parentNode.removeChild(container); - } - var container = document.createElement('DIV'); - container.id = 'fp-search-dlg'; - - var dlg_title = document.createElement('DIV'); - dlg_title.className = 'hd'; - dlg_title.innerHTML = M.str.repository.search; - - var dlg_body = document.createElement('DIV'); - dlg_body.className = 'bd'; - - var sform = document.createElement('FORM'); - sform.method = 'POST'; - sform.id = "fp-search-form"; - sform.innerHTML = obj.form; - - dlg_body.appendChild(sform); - container.appendChild(dlg_title); - container.appendChild(dlg_body); - Y.one(document.body).appendChild(container); - var search_dialog= null; - function dialog_handler() { - scope.viewbar.set('disabled', false); - scope.request({ - scope: scope, - action:'search', - client_id: client_id, - repository_id: repository_id, - form: {id: 'fp-search-form',upload:false,useDisabled:true}, - callback: function(id, o, args) { - scope.parse_repository_options(o); - scope.view_files(); - } - }, true); - search_dialog.cancel(); - } - Y.one('#fp-search-form').on('keydown', function(e){ - if (e.keyCode == 13) { - dialog_handler(); - e.preventDefault(); - } - }, this); - - search_dialog = new YAHOO.widget.Dialog("fp-search-dlg", { - postmethod: 'async', - draggable: true, - width : "30em", - modal: true, - fixedcenter : true, - zindex: 9999991, - visible : false, - constraintoviewport : true, - buttons: [ - { - text:M.str.repository.submit, - handler:dialog_handler, - isDefault:true - }, { - text:M.str.moodle.cancel, - handler:function(){ - this.destroy() - } - }] - }); - search_dialog.render(); - search_dialog.show(); + var searchform = toolbar.one('.fp-tb-search form'); + searchform.setContent(''); + this.request({ + scope: this, + action:'searchform', + repository_id: this.active_repo.id, + callback: function(id, obj, args) { + if (obj.repo_id == scope.active_repo.id && obj.form) { + // if we did not jump to another repository meanwhile + searchform.setContent(obj.form); } - }); - },this); - toolbar.appendChild(search); + } + }, false); } + + // refresh button // weather we use cache for this instance, this button will reload listing anyway - if(!r.norefresh) { - var html = ' '+M.str.repository.refresh+''; - var refresh = Y.Node.create(html); - refresh.on('click', function() { - this.list(); - }, this); - toolbar.appendChild(refresh); - } + enable_tb_control(toolbar.one('.fp-tb-refresh'), !r.norefresh); + + // login button + enable_tb_control(toolbar.one('.fp-tb-logout'), !r.nologin); if(!r.nologin) { var label = r.logouttext?r.logouttext:M.str.repository.logout; - var html = ' '+label+''; - var logout = Y.Node.create(html); - logout.on('click', function() { - this.request({ - action:'logout', - client_id: client_id, - repository_id: repository_id, - path:'', - callback: function(id, obj, args) { - scope.viewbar.set('disabled', true); - scope.print_login(obj); - } - }, true); - }, this); - toolbar.appendChild(logout); + toolbar.one('.fp-tb-logout').one('a,button').setContent(label) } - if(r.manage) { - var mgr = document.createElement('A'); - mgr.href = r.manage; - mgr.target = "_blank"; - mgr.innerHTML = ' '+M.str.repository.manageurl; - toolbar.appendChild(mgr); - } - if(r.help) { - var help = document.createElement('A'); - help.href = r.help; - help.target = "_blank"; - help.innerHTML = ' '+M.str.repository.help; - toolbar.appendChild(help); - } + // manage url + enable_tb_control(toolbar.one('.fp-tb-manage'), r.manage); + Y.one('#fp-tb-manage-'+client_id+'-link').set('href', r.manage); - this.print_path(); - }, - get_page_button: function(page) { - var r = this.active_repo; - var css = ''; - if (page == r.page) { - css = 'class="cur_page" '; - } - var str = ''; - return str; - }, - print_paging: function(html_id) { - var client_id = this.options.client_id; - var scope = this; - var r = this.active_repo; - var str = ''; - var action = ''; - var lastpage = r.pages; - var lastpagetext = r.pages; - if (r.pages == -1) { - lastpage = r.page + 1; - lastpagetext = M.str.moodle.next; - } - if (lastpage > 1) { - str += '
          '; - str += this.get_page_button(1)+'1 '; - - var span = 5; - var ex = (span-1)/2; - - if (r.page+ex>=lastpage) { - var max = lastpage; - } else { - if (r.page= span) { - str += ' ... '; - for(var i=r.page-ex; i'; - } else { - str += this.get_page_button(max)+max+''; - str += ' ... '+this.get_page_button(lastpage)+lastpagetext+''; - } - str += '
          '; - } - if (str) { - var a = Y.Node.create(str); - Y.one('#fp-header-'+client_id).appendChild(a); - - Y.all('#fp-header-'+client_id+' .fp-paging a').each( - function(node, id) { - node.on('click', function(e) { - var id = node.get('id'); - var re = new RegExp("repo-page-(\\d+)", "i"); - var result = id.match(re); - var args = {}; - args.page = result[1]; - if (scope.active_repo.issearchresult) { - scope.request({ - scope: scope, - action:'search', - client_id: client_id, - repository_id: r.id, - params: {'page':result[1]}, - callback: function(id, o, args) { - o.issearchresult = true; - scope.parse_repository_options(o); - scope.view_files(result[1]); - } - }, true); - - } else { - if (scope.viewmode == 2) { - scope.view_as_list(result[1]); - } else { - scope.list(args); - } - } - }); - }); - } + // help url + enable_tb_control(toolbar.one('.fp-tb-help'), r.help); + Y.one('#fp-tb-help-'+client_id+'-link').set('href', r.help); }, print_path: function() { - var client_id = this.options.client_id; - var panel = Y.one('#panel-'+client_id); + if (!this.pathbar) { return; } + this.pathbar.setContent('').addClass('empty'); var p = this.filepath; - if (p && p.length!=0) { - var path = Y.Node.create('
          '); - panel.appendChild(path); + if (p && p.length!=0 && this.viewmode != 2) { for(var i = 0; i < p.length; i++) { - var link_path = p[i].path; - var link = document.createElement('A'); - link.href = "###"; - link.innerHTML = p[i].name; - link.id = 'path-node-'+client_id+'-'+i; - var sep = Y.Node.create('/'); - path.appendChild(link); - path.appendChild(sep); - Y.one('#'+link.id).on('click', function(Y, path){ - this.list({'path':path}); - }, this, link_path) + var el = this.pathnode.cloneNode(true); + this.pathbar.appendChild(el); + if (i == 0) {el.addClass('first');} + if (i == p.length-1) {el.addClass('last');} + if (i%2) {el.addClass('even');} else {el.addClass('odd');} + el.all('.fp-path-folder-name').setContent(p[i].name); + el.on('click', + function(e, path) { + e.preventDefault(); + this.list({'path':path}); + }, + this, p[i].path); } + this.pathbar.removeClass('empty'); } }, hide: function() { + this.selectui.hide(); + if (this.process_dlg) { + this.process_dlg.hide(); + } + if (this.msg_dlg) { + this.msg_dlg.hide(); + } this.mainui.hide(); }, show: function() { - if (this.rendered) { - var panel = Y.one('#panel-'+this.options.client_id); - panel.set('innerHTML', ''); + if (this.fpnode) { + this.hide(); this.mainui.show(); this.show_recent_repository(); } else { @@ -1517,7 +1721,13 @@ M.core_filepicker.init = function(Y, options) { this.render(); }, show_recent_repository: function() { - var repository_id = YAHOO.util.Cookie.get('recentrepository'); + this.hide_header(); + this.viewbar_set_enabled(false); + var repository_id = Y.Cookie.get('recentrepository'); + this.viewmode = Y.Cookie.get('recentviewmode', Number); + if (this.viewmode != 2 && this.viewmode != 3) { + this.viewmode = 1; + } if (this.options.repositories[repository_id]) { this.list({'repo_id':repository_id}); } diff --git a/repository/filesystem/lang/en/repository_filesystem.php b/repository/filesystem/lang/en/repository_filesystem.php index 16031379bb2..1a1de4a5262 100644 --- a/repository/filesystem/lang/en/repository_filesystem.php +++ b/repository/filesystem/lang/en/repository_filesystem.php @@ -30,6 +30,7 @@ $string['filesystem:view'] = 'View file system repository'; $string['information'] = 'These folders are within the {$a} directory.'; $string['invalidpath'] = 'Invalid root path'; $string['path'] = 'Select a subdirectory'; +$string['root'] = 'Root'; $string['nosubdir'] = 'You need to create at least one folder inside the {$a} directory so you can select it here.'; $string['pluginname_help'] = 'Create repository from local directory'; $string['pluginname'] = 'File system'; diff --git a/repository/filesystem/lib.php b/repository/filesystem/lib.php index 6133434411b..99b9dad767f 100644 --- a/repository/filesystem/lib.php +++ b/repository/filesystem/lib.php @@ -1,5 +1,4 @@ . +/** + * This plugin is used to access files on server file system + * + * @since 2.0 + * @package repository_filesystem + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); +require_once($CFG->libdir . '/filelib.php'); + /** * repository_filesystem class + * * Create a repository from your local filesystem * *NOTE* for security issue, we use a fixed repository path * which is %moodledata%/repository * - * @since 2.0 * @package repository - * @subpackage filesystem - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_filesystem extends repository { + + /** + * Constructor + * + * @param int $repositoryid repository ID + * @param int $context context ID + * @param array $options + */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { global $CFG; parent::__construct($repositoryid, $context, $options); @@ -54,7 +70,7 @@ class repository_filesystem extends repository { $list['list'] = array(); // process breacrumb trail $list['path'] = array( - array('name'=>'Root', 'path'=>'') + array('name'=>get_string('root', 'repository_filesystem'), 'path'=>'') ); $trail = ''; if (!empty($path)) { @@ -83,8 +99,10 @@ class repository_filesystem extends repository { 'title' => $file, 'source' => $path.'/'.$file, 'size' => filesize($this->root_path.$file), - 'date' => time(), - 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($this->root_path.$file, 32))->out(false) + 'datecreated' => filectime($this->root_path.$file), + 'datemodified' => filemtime($this->root_path.$file), + 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file, 90))->out(false), + 'icon' => $OUTPUT->pix_url(file_extension_icon($file, 24))->out(false) ); } else { if (!empty($path)) { @@ -95,7 +113,9 @@ class repository_filesystem extends repository { $list['list'][] = array( 'title' => $file, 'children' => array(), - 'thumbnail' => $OUTPUT->pix_url('f/folder-32')->out(false), + 'datecreated' => filectime($this->root_path.$file), + 'datemodified' => filemtime($this->root_path.$file), + 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(90))->out(false), 'path' => $current_path ); } @@ -114,6 +134,7 @@ class repository_filesystem extends repository { public function global_search() { return false; } + /** * Return file path * @return array @@ -176,10 +197,6 @@ class repository_filesystem extends repository { } } - public function supported_returntypes() { - return FILE_INTERNAL; - } - public static function create($type, $userid, $context, $params, $readonly=0) { global $PAGE; if (has_capability('moodle/site:config', get_system_context())) { @@ -195,4 +212,52 @@ class repository_filesystem extends repository { } return $errors; } + + /** + * User cannot use the external link to dropbox + * + * @return int + */ + public function supported_returntypes() { + return FILE_INTERNAL | FILE_REFERENCE; + } + + /** + * Get file from external repository by reference + * {@link repository::get_file_reference()} + * {@link repository::get_file()} + * + * @param stdClass $reference file reference db record + * @return stdClass|null|false + */ + public function get_file_by_reference($reference) { + $ref = $reference->reference; + if ($ref{0} == '/') { + $filepath = $this->root_path.substr($ref, 1, strlen($ref)-1); + } else { + $filepath = $this->root_path.$ref; + } + $fileinfo = new stdClass; + $fileinfo->filepath = $filepath; + return $fileinfo; + } + + /** + * Repository method to serve file + * + * @param stored_file $storedfile + * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours) + * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only + * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin + * @param array $options additional options affecting the file serving + */ + public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) { + $reference = $storedfile->get_reference(); + if ($reference{0} == '/') { + $file = $this->root_path.substr($reference, 1, strlen($reference)-1); + } else { + $file = $this->root_path.$reference; + } + send_file($file, $storedfile->get_filename(), 'default' , $filter, false, $forcedownload); + } } diff --git a/repository/flickr/lib.php b/repository/flickr/lib.php index 7d5b6fdcd5c..b54ad931f27 100644 --- a/repository/flickr/lib.php +++ b/repository/flickr/lib.php @@ -1,5 +1,4 @@ . /** - * repository_flickr class - * This plugin is used to access user's private flickr repository + * This plugin is used to access flickr pictures * * @since 2.0 - * @package repository - * @subpackage flickr - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_flickr + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - +require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir.'/flickrlib.php'); /** + * This plugin is used to access user's private flickr repository * + * @since 2.0 + * @package repository_flickr + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_flickr extends repository { private $flickr; diff --git a/repository/flickr_public/lib.php b/repository/flickr_public/lib.php index 2c91090247c..3b34b980b94 100644 --- a/repository/flickr_public/lib.php +++ b/repository/flickr_public/lib.php @@ -14,6 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This plugin is used to access flickr pictures + * + * @since 2.0 + * @package repository_flickr_public + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); +require_once($CFG->libdir.'/flickrlib.php'); +require_once(dirname(__FILE__) . '/image.php'); + /** * repository_flickr_public class * This one is used to create public repository @@ -21,16 +33,10 @@ * flickr photos from this plugin * * @since 2.0 - * @package repository - * @subpackage flickr_public - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_flickr_public + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -require_once($CFG->libdir.'/flickrlib.php'); -require_once(dirname(__FILE__) . '/image.php'); - class repository_flickr_public extends repository { private $flickr; public $photos; diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index acb991dd003..36648bdb81e 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -1,5 +1,4 @@ . /** - * Google Docs Plugin + * This plugin is used to access google docs * * @since 2.0 - * @package repository - * @subpackage googledocs + * @package repository_googledocs * @copyright 2009 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - +require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir.'/googleapi.php'); +/** + * Google Docs Plugin + * + * @since 2.0 + * @package repository_googledocs + * @copyright 2009 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class repository_googledocs extends repository { private $subauthtoken = ''; diff --git a/repository/lib.php b/repository/lib.php index 7ddb8989f19..bb6c4eb4d9f 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1,5 +1,4 @@ . - /** * This file contains classes used to manage the repository plugins in Moodle * and was introduced as part of the changes occuring in Moodle 2.0 * * @since 2.0 - * @package core - * @subpackage repository - * @copyright 2009 Dongsheng Cai - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package repository + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(dirname(dirname(__FILE__)) . '/config.php'); require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/formslib.php'); -define('FILE_EXTERNAL', 1); -define('FILE_INTERNAL', 2); +define('FILE_EXTERNAL', 1); +define('FILE_INTERNAL', 2); +define('FILE_REFERENCE', 4); define('RENAME_SUFFIX', '_2'); /** @@ -51,8 +49,7 @@ define('RENAME_SUFFIX', '_2'); * - When you create a type for a plugin that can't have multiple instances, a * instance is automatically created. * - * @package moodlecore - * @subpackage repository + * @package repository * @copyright 2009 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -92,12 +89,13 @@ class repository_type { */ private $_sortorder; - /** + /** * Return if the instance is visible in a context - * TODO: check if the context visibility has been overwritten by the plugin creator + * + * @todo check if the context visibility has been overwritten by the plugin creator * (need to create special functions to be overvwritten in repository class) - * @param objet $context - context - * @return boolean + * @param stdClass $context context + * @return bool */ public function get_contextvisibility($context) { global $USER; @@ -118,11 +116,11 @@ class repository_type { /** * repository_type constructor - * @global object $CFG - * @param integer $typename + * + * @param int $typename * @param array $typeoptions - * @param boolean $visible - * @param integer $sortorder (don't really need set, it will be during create() call) + * @param bool $visible + * @param int $sortorder (don't really need set, it will be during create() call) */ public function __construct($typename = '', $typeoptions = array(), $visible = true, $sortorder = 0) { global $CFG; @@ -163,7 +161,8 @@ class repository_type { /** * Get the type name (no whitespace) * For a human readable name, use get_readablename() - * @return String the type name + * + * @return string the type name */ public function get_typename() { return $this->_typename; @@ -171,6 +170,7 @@ class repository_type { /** * Return a human readable and user-friendly type name + * * @return string user-friendly type name */ public function get_readablename() { @@ -179,6 +179,7 @@ class repository_type { /** * Return general options + * * @return array the general options */ public function get_options() { @@ -187,7 +188,8 @@ class repository_type { /** * Return visibility - * @return boolean + * + * @return bool */ public function get_visible() { return $this->_visible; @@ -195,7 +197,8 @@ class repository_type { /** * Return order / position of display in the file picker - * @return integer + * + * @return int */ public function get_sortorder() { return $this->_sortorder; @@ -203,10 +206,8 @@ class repository_type { /** * Create a repository type (the type name must not already exist) - * @param boolean throw exception? + * @param bool $silent throw exception? * @return mixed return int if create successfully, return false if - * any errors - * @global object $DB */ public function create($silent = false) { global $DB; @@ -279,8 +280,9 @@ class repository_type { /** * Update plugin options into the config_plugin table + * * @param array $options - * @return boolean + * @return bool */ public function update_options($options = null) { global $DB; @@ -314,9 +316,9 @@ class repository_type { * or with the visible value of this object * This function is private. * For public access, have a look to switch_and_update_visibility() - * @global object $DB - * @param boolean $visible - * @return boolean + * + * @param bool $visible + * @return bool */ private function update_visible($visible = null) { global $DB; @@ -336,9 +338,9 @@ class repository_type { * or with the sortorder value of this object * This function is private. * For public access, have a look to move_order() - * @global object $DB - * @param integer $sortorder - * @return boolean + * + * @param int $sortorder + * @return bool */ private function update_sortorder($sortorder = null) { global $DB; @@ -362,7 +364,7 @@ class repository_type { * 1. retrieve all types in an array. This array is sorted by sortorder, * and the array keys start from 0 to X (incremented by 1) * 2. switch sortorder values of this type and its adjacent type - * @global object $DB + * * @param string $move "up" or "down" */ public function move_order($move) { @@ -370,7 +372,7 @@ class repository_type { $types = repository::get_types(); // retrieve all types - /// retrieve this type into the returned array + // retrieve this type into the returned array $i = 0; while (!isset($indice) && $iget_typename() == $this->_typename) { @@ -379,7 +381,7 @@ class repository_type { $i++; } - /// retrieve adjacent indice + // retrieve adjacent indice switch ($move) { case "up": $adjacentindice = $indice - 1; @@ -403,9 +405,10 @@ class repository_type { /** * 1. Change visibility to the value chosen - * * 2. Update the type - * @return boolean + * + * @param bool $visible + * @return bool */ public function update_visibility($visible = null) { if (is_bool($visible)) { @@ -420,10 +423,11 @@ class repository_type { /** * Delete a repository_type (general options are removed from config_plugin * table, and all instances are deleted) - * @global object $DB - * @return boolean + * + * @param bool $downloadcontents download external contents if exist + * @return bool */ - public function delete() { + public function delete($downloadcontents = false) { global $DB; //delete all instances of this type @@ -433,7 +437,7 @@ class repository_type { $params['type'] = $this->_typename; $instances = repository::get_instances($params); foreach ($instances as $instance) { - $instance->delete(); + $instance->delete($downloadcontents); } //delete all general options @@ -441,53 +445,50 @@ class repository_type { set_config($name, null, $this->_typename); } - return $DB->delete_records('repository', array('type' => $this->_typename)); + try { + $DB->delete_records('repository', array('type' => $this->_typename)); + } catch (dml_exception $ex) { + return false; + } + return true; } } /** - * This is the base class of the repository class + * This is the base class of the repository class. * - * To use repository plugin, see: - * http://docs.moodle.org/dev/Repository_How_to_Create_Plugin - * class repository is an abstract class, some functions must be implemented in subclass. - * See an example: repository/boxnet/lib.php + * To create repository plugin, see: {@link http://docs.moodle.org/dev/Repository_plugins} + * See an example: {@link repository_boxnet} * - * A few notes: - * // for ajax file picker, this will print a json string to tell file picker - * // how to build a login form - * $repo->print_login(); - * // for ajax file picker, this will return a files list. - * $repo->get_listing(); - * // this function will be used for non-javascript version. - * $repo->print_listing(); - * // print a search box - * $repo->print_search(); - * - * @package moodlecore - * @subpackage repository - * @copyright 2009 Dongsheng Cai + * @package repository + * @category repository + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class repository { // $disabled can be set to true to disable a plugin by force // example: self::$disabled = true + /** @var bool force disable repository instance */ public $disabled = false; + /** @var int repository instance id */ public $id; - /** @var object current context */ + /** @var stdClass current context */ public $context; + /** @var array repository options */ public $options; + /** @var bool Whether or not the repository instance is editable */ public $readonly; + /** @var int return types */ public $returntypes; - /** @var object repository instance database record */ + /** @var stdClass repository instance database record */ public $instance; /** - * 1. Initialize context and options - * 2. Accept necessary parameters + * Constructor * - * @param integer $repositoryid repository instance id - * @param integer|object a context id or context object + * @param int $repositoryid repository instance id + * @param int|stdClass $context a context id or context object * @param array $options repository options + * @param int $readonly indicate this repo is readonly or not */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { global $DB; @@ -502,6 +503,7 @@ abstract class repository { $this->options = array(); if (is_array($options)) { + // The get_option() method will get stored options in database. $options = array_merge($this->get_option(), $options); } else { $options = $this->get_option(); @@ -514,9 +516,41 @@ abstract class repository { $this->super_called = true; } + /** + * Get repository instance using repository id + * + * @param int $repositoryid repository ID + * @param stdClass|int $context context instance or context ID + * @return repository + */ + public static function get_repository_by_id($repositoryid, $context) { + global $CFG, $DB; + + $sql = 'SELECT i.name, i.typeid, r.type FROM {repository} r, {repository_instances} i WHERE i.id=? AND i.typeid=r.id'; + + if (!$record = $DB->get_record_sql($sql, array($repositoryid))) { + throw new repository_exception('invalidrepositoryid', 'repository'); + } else { + $type = $record->type; + if (file_exists($CFG->dirroot . "/repository/$type/lib.php")) { + require_once($CFG->dirroot . "/repository/$type/lib.php"); + $classname = 'repository_' . $type; + $contextid = $context; + if (is_object($context)) { + $contextid = $context->id; + } + $repository = new $classname($repositoryid, $contextid, array('type'=>$type)); + return $repository; + } else { + throw new moodle_exception('error'); + } + } + } + /** * Get a repository type object by a given type name. - * @global object $DB + * + * @static * @param string $typename the repository type name * @return repository_type|bool */ @@ -532,7 +566,8 @@ abstract class repository { /** * Get the repository type by a given repository type id. - * @global object $DB + * + * @static * @param int $id the type id * @return object */ @@ -549,9 +584,9 @@ abstract class repository { /** * Return all repository types ordered by sortorder field * first repository type in returnedarray[0], second repository type in returnedarray[1], ... - * @global object $DB - * @global object $CFG - * @param boolean $visible can return types by visiblity, return all types if null + * + * @static + * @param bool $visible can return types by visiblity, return all types if null * @return array Repository types */ public static function get_types($visible=null) { @@ -575,9 +610,11 @@ abstract class repository { /** * To check if the context id is valid - * @global object $USER + * + * @static * @param int $contextid - * @return boolean + * @param stdClass $instance + * @return bool */ public static function check_capability($contextid, $instance) { $context = get_context_instance_by_id($contextid); @@ -590,10 +627,11 @@ abstract class repository { /** * Check if file already exists in draft area * + * @static * @param int $itemid * @param string $filepath * @param string $filename - * @return boolean + * @return bool */ public static function draftfile_exists($itemid, $filepath, $filename) { global $USER; @@ -606,33 +644,25 @@ abstract class repository { } } - /** - * Does this repository used to browse moodle files? - * - * @return boolean - */ - public function has_moodle_files() { - return false; - } /** * This function is used to copy a moodle file to draft area * - * @global object $USER - * @global object $DB * @param string $encoded The metainfo of file, it is base64 encoded php serialized data - * @param string $draftitemid itemid - * @param string $new_filename The intended name of file + * @param int $draftitemid itemid * @param string $new_filepath the new path in draft area + * @param string $new_filename The intended name of file * @return array The information of file */ public function copy_to_area($encoded, $draftitemid, $new_filepath, $new_filename) { global $USER, $DB; + $fs = get_file_storage(); + $browser = get_file_browser(); if ($this->has_moodle_files() == false) { - throw new coding_exception('Only repository used to browse moodle files can use copy_to_area'); + throw new coding_exception('Only repository used to browse moodle files can use repository::copy_to_area()'); } - $browser = get_file_browser(); + $params = unserialize(base64_decode($encoded)); $user_context = get_context_instance(CONTEXT_USER, $USER->id); @@ -677,6 +707,7 @@ abstract class repository { /** * Get unused filename by appending suffix * + * @static * @param int $itemid * @param string $filepath * @param string $filename @@ -694,6 +725,7 @@ abstract class repository { /** * Append a suffix to filename * + * @static * @param string $filename * @return string */ @@ -709,7 +741,9 @@ abstract class repository { /** * Return all types that you a user can create/edit and which are also visible * Note: Mostly used in order to know if at least one editable type can be set - * @param object $context the context for which we want the editable types + * + * @static + * @param stdClass $context the context for which we want the editable types * @return array types */ public static function get_editable_types($context = null) { @@ -733,10 +767,8 @@ abstract class repository { /** * Return repository instances - * @global object $DB - * @global object $CFG - * @global object $USER * + * @static * @param array $args Array containing the following keys: * currentcontext * context @@ -812,7 +844,6 @@ abstract class repository { } $repositories = array(); - $ft = new filetype_parser(); if (isset($args['accepted_types'])) { $accepted_types = $args['accepted_types']; } else { @@ -843,16 +874,10 @@ abstract class repository { } else { // check mimetypes if ($accepted_types !== '*' and $repository->supported_filetypes() !== '*') { - $accepted_types = $ft->get_extensions($accepted_types); - $supported_filetypes = $ft->get_extensions($repository->supported_filetypes()); - - $is_supported = false; - foreach ($supported_filetypes as $type) { - if (in_array($type, $accepted_types)) { - $is_supported = true; - } - } - + $accepted_ext = file_get_typegroup('extension', $accepted_types); + $supported_ext = file_get_typegroup('extension', $repository->supported_filetypes()); + $valid_ext = array_intersect($accepted_ext, $supported_ext); + $is_supported = !empty($valid_ext); } // check return values if ($returntypes !== 3 and $repository->supported_returntypes() !== 3) { @@ -890,8 +915,8 @@ abstract class repository { /** * Get single repository instance - * @global object $DB - * @global object $CFG + * + * @static * @param integer $id repository id * @return object repository instance */ @@ -919,9 +944,10 @@ abstract class repository { /** * Call a static function. Any additional arguments than plugin and function will be passed through. - * @global object $CFG - * @param string $plugin - * @param string $function + * + * @static + * @param string $plugin repository plugin name + * @param string $function funciton name * @return mixed */ public static function static_function($plugin, $function) { @@ -964,7 +990,6 @@ abstract class repository { * @param string $thefile * @param string $filename name of the file * @param bool $deleteinfected - * @return void */ public static function antivir_scan_file($thefile, $filename, $deleteinfected) { global $CFG; @@ -1024,14 +1049,83 @@ abstract class repository { } } + /** + * Repository method to serve file + * + * @param stored_file $storedfile + * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours) + * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only + * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin + * @param array $options additional options affecting the file serving + */ + public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) { + throw new coding_exception("Repository plugin must implement send_file() method."); + } + + /** + * Return reference file life time + * + * @param string $ref + * @return int + */ + public function get_reference_file_lifetime($ref) { + // One day + return 60 * 60 * 24; + } + + /** + * Decide whether or not the file should be synced + * + * @param stored_file $storedfile + * @return bool + */ + public function sync_individual_file(stored_file $storedfile) { + return true; + } + + /** + * Return human readable reference information + * {@link stored_file::get_reference()} + * + * @param string $reference + * @return string|null + */ + public function get_reference_details($reference) { + return null; + } + + /** + * Cache file from external repository by reference + * {@link repository::get_file_reference()} + * {@link repository::get_file()} + * Invoked at MOODLE/repository/repository_ajax.php + * + * @param string $reference this reference is generated by + * repository::get_file_reference() + * @param stored_file $storedfile created file reference + */ + public function cache_file_by_reference($reference, $storedfile) { + } + + /** + * Get file from external repository by reference + * {@link repository::get_file_reference()} + * {@link repository::get_file()} + * + * @param stdClass $reference file reference db record + * @return stdClass|null|false + */ + public function get_file_by_reference($reference) { + return null; + } + /** * Move file from download folder to file pool using FILE API - * @global object $DB - * @global object $CFG - * @global object $USER - * @global object $OUTPUT + * + * @todo MDL-28637 + * @static * @param string $thefile file path in download folder - * @param object $record + * @param stdClass $record * @return array containing the following keys: * icon * file @@ -1044,29 +1138,13 @@ abstract class repository { // scan for viruses if possible, throws exception if problem found self::antivir_scan_file($thefile, $record->filename, empty($CFG->repository_no_delete)); //TODO: MDL-28637 this repository_no_delete is a bloody hack! - if ($record->filepath !== '/') { - $record->filepath = trim($record->filepath, '/'); - $record->filepath = '/'.$record->filepath.'/'; - } - $context = get_context_instance(CONTEXT_USER, $USER->id); - $now = time(); - - $record->contextid = $context->id; - $record->component = 'user'; - $record->filearea = 'draft'; - $record->timecreated = $now; - $record->timemodified = $now; - $record->userid = $USER->id; - $record->mimetype = mimeinfo('type', $thefile); - if(!is_numeric($record->itemid)) { - $record->itemid = 0; - } $fs = get_file_storage(); - if ($existingfile = $fs->get_file($context->id, $record->component, $record->filearea, $record->itemid, $record->filepath, $record->filename)) { + // If file name being used. + if (repository::draftfile_exists($record->itemid, $record->filepath, $record->filename)) { $draftitemid = $record->itemid; $new_filename = repository::get_unused_filename($draftitemid, $record->filepath, $record->filename); $old_filename = $record->filename; - // create a tmp file + // Create a tmp file. $record->filename = $new_filename; $newfile = $fs->create_file_from_pathname($record, $thefile); $event = array(); @@ -1099,16 +1177,16 @@ abstract class repository { } /** - * Builds a tree of files This function is - * then called recursively. + * Builds a tree of files This function is then called recursively. * - * @param $fileinfo an object returned by file_browser::get_file_info() - * @param $search searched string - * @param $dynamicmode bool no recursive call is done when in dynamic mode - * @param $list - the array containing the files under the passed $fileinfo + * @static + * @todo take $search into account, and respect a threshold for dynamic loading + * @param file_info $fileinfo an object returned by file_browser::get_file_info() + * @param string $search searched string + * @param bool $dynamicmode no recursive call is done when in dynamic mode + * @param array $list the array containing the files under the passed $fileinfo * @returns int the number of files found * - * todo: take $search into account, and respect a threshold for dynamic loading */ public static function build_tree($fileinfo, $search, $dynamicmode, &$list) { global $CFG, $OUTPUT; @@ -1138,7 +1216,7 @@ abstract class repository { 'size' => 0, 'date' => $filedate, 'path' => array_reverse($path), - 'thumbnail' => $OUTPUT->pix_url('f/folder-32') + 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(90))->out(false) ); //if ($dynamicmode && $child->is_writable()) { @@ -1175,7 +1253,8 @@ abstract class repository { 'date' => $filedate, //'source' => $child->get_url(), 'source' => base64_encode($source), - 'thumbnail'=>$OUTPUT->pix_url(file_extension_icon($filename, 32)), + 'icon'=>$OUTPUT->pix_url(file_file_icon($child, 24))->out(false), + 'thumbnail'=>$OUTPUT->pix_url(file_file_icon($child, 90))->out(false), ); $filecount++; } @@ -1184,13 +1263,11 @@ abstract class repository { return $filecount; } - /** * Display a repository instance list (with edit/delete/create links) - * @global object $CFG - * @global object $USER - * @global object $OUTPUT - * @param object $context the context for which we display the instance + * + * @static + * @param stdClass $context the context for which we display the instance * @param string $typename if set, we display only one type of instance */ public static function display_instances_list($context, $typename = null) { @@ -1205,7 +1282,6 @@ abstract class repository { } else { $baseurl = new moodle_url('/repository/manage_instances.php', array('contextid'=>$context->id, 'sesskey'=>sesskey())); } - $url = $baseurl; $namestr = get_string('name'); $pluginstr = get_string('plugin', 'repository'); @@ -1240,15 +1316,15 @@ abstract class repository { if ($type->get_contextvisibility($context)) { if (!$i->readonly) { - $url->param('type', $i->options['type']); - $url->param('edit', $i->id); - $settings .= html_writer::link($url, $settingsstr); + $settingurl = new moodle_url($baseurl); + $settingurl->param('type', $i->options['type']); + $settingurl->param('edit', $i->id); + $settings .= html_writer::link($settingurl, $settingsstr); - $url->remove_params('edit'); - $url->param('delete', $i->id); - $delete .= html_writer::link($url, $deletestr); - - $url->remove_params('type'); + $deleteurl = new moodle_url($baseurl); + $deleteurl->param('delete', $i->id); + $deleteurl->param('type', $i->options['type']); + $delete .= html_writer::link($deleteurl, $deletestr); } } @@ -1294,9 +1370,7 @@ abstract class repository { if (!empty($instanceoptionnames)) { //create a unique type of instance $addable = 1; $baseurl->param('new', $typename); - $instancehtml .= "
          -

          -
          "; + $output .= $OUTPUT->single_button($baseurl, get_string('createinstance', 'repository'), 'get'); $baseurl->remove_params('new'); } } @@ -1312,9 +1386,20 @@ abstract class repository { print($output); } + /** + * Prepare file reference information + * + * @param string $source + * @return string file referece + */ + public function get_file_reference($source) { + return $source; + } /** * Decide where to save the file, can be overwriten by subclass - * @param string filename + * + * @param string $filename file name + * @return file path */ public function prepare_file($filename) { global $CFG; @@ -1333,6 +1418,15 @@ abstract class repository { return $dir.$filename; } + /** + * Does this repository used to browse moodle files? + * + * @return bool + */ + public function has_moodle_files() { + return false; + } + /** * Return file URL, for most plugins, the parameter is the original * url, but some plugins use a file id, so we need this function to @@ -1346,14 +1440,11 @@ abstract class repository { } /** - * Download a file, this function can be overridden by - * subclass. + * Download a file, this function can be overridden by subclass. {@link curl} * - * @global object $CFG * @param string $url the url of file * @param string $filename save location * @return string the location of the file - * @see curl package */ public function get_file($url, $filename = '') { global $CFG; @@ -1361,6 +1452,8 @@ abstract class repository { $fp = fopen($path, 'w'); $c = new curl; $c->download(array(array('url'=>$url, 'file'=>$fp))); + // Close file handler. + fclose($fp); return array('path'=>$path, 'url'=>$url); } @@ -1368,7 +1461,7 @@ abstract class repository { * Return size of a file in bytes. * * @param string $source encoded and serialized data of file - * @return integer file size in bytes + * @return int file size in bytes */ public function get_file_size($source) { $browser = get_file_browser(); @@ -1392,7 +1485,8 @@ abstract class repository { /** * Return is the instance is visible * (is the type visible ? is the context enable ?) - * @return boolean + * + * @return bool */ public function is_visible() { $type = repository::get_type_by_id($this->options['typeid']); @@ -1410,7 +1504,7 @@ abstract class repository { /** * Return the name of this instance, can be overridden. - * @global object $DB + * * @return string */ public function get_name() { @@ -1423,7 +1517,8 @@ abstract class repository { } /** - * what kind of files will be in this repository? + * What kind of files will be in this repository? + * * @return array return '*' means this repository support any files, otherwise * return mimetypes of files, it can be an array */ @@ -1433,27 +1528,27 @@ abstract class repository { } /** - * does it return a file url or a item_id + * Does it return a file url or a item_id + * * @return string */ public function supported_returntypes() { - return (FILE_INTERNAL | FILE_EXTERNAL); + return (FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE); } /** * Provide repository instance information for Ajax - * @global object $CFG - * @return object + * + * @return stdClass */ final public function get_meta() { global $CFG, $OUTPUT; - $ft = new filetype_parser; $meta = new stdClass(); $meta->id = $this->id; $meta->name = $this->get_name(); $meta->type = $this->options['type']; $meta->icon = $OUTPUT->pix_url('icon', 'repository_'.$meta->type)->out(false); - $meta->supported_types = $ft->get_extensions($this->supported_filetypes()); + $meta->supported_types = file_get_typegroup('extension', $this->supported_filetypes()); $meta->return_types = $this->supported_returntypes(); $meta->sortorder = $this->options['sortorder']; return $meta; @@ -1461,13 +1556,13 @@ abstract class repository { /** * Create an instance for this plug-in - * @global object $CFG - * @global object $DB + * + * @static * @param string $type the type of the repository - * @param integer $userid the user id - * @param object $context the context + * @param int $userid the user id + * @param stdClass $context the context * @param array $params the options for this instance - * @param integer $readonly whether to create it readonly or not (defaults to not) + * @param int $readonly whether to create it readonly or not (defaults to not) * @return mixed */ public static function create($type, $userid, $context, $params, $readonly=0) { @@ -1512,21 +1607,29 @@ abstract class repository { /** * delete a repository instance - * @global object $DB - * @return mixed + * + * @param bool $downloadcontents + * @return bool */ - final public function delete() { + final public function delete($downloadcontents = false) { global $DB; - $DB->delete_records('repository_instances', array('id'=>$this->id)); - $DB->delete_records('repository_instance_config', array('instanceid'=>$this->id)); + if ($downloadcontents) { + $this->convert_references_to_local(); + } + try { + $DB->delete_records('repository_instances', array('id'=>$this->id)); + $DB->delete_records('repository_instance_config', array('instanceid'=>$this->id)); + } catch (dml_exception $ex) { + return false; + } return true; } /** * Hide/Show a repository - * @global object $DB + * * @param string $hide - * @return boolean + * @return bool */ final public function hide($hide = 'toggle') { global $DB; @@ -1552,9 +1655,9 @@ abstract class repository { /** * Save settings for repository instance * $repo->set_option(array('api_key'=>'f2188bde132', 'name'=>'dongsheng')); - * @global object $DB + * * @param array $options settings - * @return int Id of the record + * @return bool */ public function set_option($options = array()) { global $DB; @@ -1582,7 +1685,7 @@ abstract class repository { /** * Get settings for repository instance - * @global object $DB + * * @param string $config * @return array Settings */ @@ -1607,46 +1710,42 @@ abstract class repository { } } + /** + * Filter file listing to display specific types + * + * @param array $value + * @return bool + */ public function filter(&$value) { - $pass = false; $accepted_types = optional_param_array('accepted_types', '', PARAM_RAW); - $ft = new filetype_parser; - //$ext = $ft->get_extensions($this->supported_filetypes()); if (isset($value['children'])) { - $pass = true; if (!empty($value['children'])) { $value['children'] = array_filter($value['children'], array($this, 'filter')); } + return true; // always return directories } else { if ($accepted_types == '*' or empty($accepted_types) or (is_array($accepted_types) and in_array('*', $accepted_types))) { - $pass = true; - } elseif (is_array($accepted_types)) { - foreach ($accepted_types as $type) { - $extensions = $ft->get_extensions($type); - if (!is_array($extensions)) { - $pass = true; - } else { - foreach ($extensions as $ext) { - if (preg_match('#'.$ext.'$#i', $value['title'])) { - $pass = true; - } - } + return true; + } else { + $extensions = file_get_typegroup('extension', $accepted_types); + foreach ($extensions as $ext) { + if (preg_match('#\.'.$ext.'$#i', $value['title'])) { + return true; } } } } - return $pass; + return false; } /** * Given a path, and perhaps a search, get a list of files. * - * See details on http://docs.moodle.org/dev/Repository_plugins + * See details on {@link http://docs.moodle.org/dev/Repository_plugins} * - * @param string $path, this parameter can - * a folder name, or a identification of folder - * @param string $page, the page number of file list + * @param string $path this parameter can a folder name, or a identification of folder + * @param string $page the page number of file list * @return array the list of files, including meta infomation, containing the following keys * manage, url to manage url * client_id @@ -1665,12 +1764,93 @@ abstract class repository { public function get_listing($path = '', $page = '') { } + /** + * Prepares list of files before passing it to AJAX, makes sure data is in the correct + * format and stores formatted values. + * + * @param array|stdClass $listing result of get_listing() or search() or file_get_drafarea_files() + * @return array + */ + public static function prepare_listing($listing) { + global $OUTPUT; + if (is_array($listing) && isset($listing['list']) && is_array(($listing['list']))) { + $listing['list'] = array_values($listing['list']); // convert to array + $files = &$listing['list']; + } else if (is_object($listing) && isset($listing->list) && is_array(($listing->list))) { + $listing->list = array_values($listing->list); // convert to array + $files = &$listing->list; + } else { + return $listing; + } + $len = count($files); + for ($i=0; $i<$len; $i++) { + if (is_object($files[$i])) { + $file = (array)$files[$i]; + $converttoobject = true; + } else { + $file = & $files[$i]; + $converttoobject = false; + } + if (isset($file['size'])) { + $file['size'] = (int)$file['size']; + $file['size_f'] = display_size($file['size']); + } + if (isset($file['license']) && + get_string_manager()->string_exists($file['license'], 'license')) { + $file['license_f'] = get_string($file['license'], 'license'); + } + if (isset($file['image_width']) && isset($file['image_height'])) { + $a = array('width' => $file['image_width'], 'height' => $file['image_height']); + $file['dimensions'] = get_string('imagesize', 'repository', (object)$a); + } + foreach (array('date', 'datemodified', 'datecreated') as $key) { + if (!isset($file[$key]) && isset($file['date'])) { + $file[$key] = $file['date']; + } + if (isset($file[$key])) { + // must be UNIX timestamp + $file[$key] = (int)$file[$key]; + if (!$file[$key]) { + unset($file[$key]); + } else { + $file[$key.'_f'] = userdate($file[$key], get_string('strftimedatetime', 'langconfig')); + $file[$key.'_f_s'] = userdate($file[$key], get_string('strftimedatetimeshort', 'langconfig')); + } + } + } + $isfolder = (array_key_exists('children', $file) || (isset($file['type']) && $file['type'] == 'folder')); + $filename = null; + if (isset($file['title'])) { + $filename = $file['title']; + } + else if (isset($file['fullname'])) { + $filename = $file['fullname']; + } + if (!isset($file['mimetype']) && !$isfolder && $filename) { + $file['mimetype'] = get_mimetype_description(array('filename' => $filename)); + } + if (!isset($file['icon'])) { + if ($isfolder) { + $file['icon'] = $OUTPUT->pix_url(file_folder_icon(24))->out(false); + } else if ($filename) { + $file['icon'] = $OUTPUT->pix_url(file_extension_icon($filename, 24))->out(false); + } + } + if ($converttoobject) { + $files[$i] = (object)$file; + } + } + return $listing; + } + /** * Search files in repository * When doing global search, $search_text will be used as * keyword. * - * @return mixed, see get_listing() + * @param string $search_text search key word + * @param int $page page + * @return mixed {@see repository::get_listing} */ public function search($search_text, $page = 0) { $list = array(); @@ -1691,7 +1871,7 @@ abstract class repository { /** * To check whether the user is logged in. * - * @return boolean + * @return bool */ public function check_login(){ return true; @@ -1700,6 +1880,8 @@ abstract class repository { /** * Show the login screen, if required + * + * @return string */ public function print_login(){ return $this->get_listing(); @@ -1707,15 +1889,13 @@ abstract class repository { /** * Show the search screen, if required - * @return null + * + * @return string */ public function print_search() { - $str = ''; - $str .= ''; - $str .= ''; - $str .= ''; - $str .= '

          '; - return $str; + global $PAGE; + $renderer = $PAGE->get_renderer('core', 'files'); + return $renderer->repository_default_searchform(); } /** @@ -1727,7 +1907,8 @@ abstract class repository { /** * is it possible to do glboal search? - * @return boolean + * + * @return bool */ public function global_search() { return false; @@ -1735,7 +1916,8 @@ abstract class repository { /** * Defines operations that happen occasionally on cron - * @return boolean + * + * @return bool */ public function cron() { return true; @@ -1743,7 +1925,8 @@ abstract class repository { /** * function which is run when the type is created (moodle administrator add the plugin) - * @return boolean success or fail? + * + * @return bool success or fail? */ public static function plugin_init() { return true; @@ -1751,7 +1934,8 @@ abstract class repository { /** * Edit/Create Admin Settings Moodle form - * @param object $mform Moodle form (passed by reference) + * + * @param moodleform $mform Moodle form (passed by reference) * @param string $classname repository class name */ public static function type_config_form($mform, $classname = 'repository') { @@ -1767,9 +1951,11 @@ abstract class repository { /** * Validate Admin Settings Moodle form - * @param object $mform Moodle form (passed by reference) - * @param array array of ("fieldname"=>value) of submitted data - * @param array array of ("fieldname"=>errormessage) of errors + * + * @static + * @param moodleform $mform Moodle form (passed by reference) + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $errors array of ("fieldname"=>errormessage) of errors * @return array array of errors */ public static function type_form_validation($mform, $data, $errors) { @@ -1779,14 +1965,16 @@ abstract class repository { /** * Edit/Create Instance Settings Moodle form - * @param object $mform Moodle form (passed by reference) + * + * @param moodleform $mform Moodle form (passed by reference) */ public function instance_config_form($mform) { } /** - * Return names of the general options + * Return names of the general options. * By default: no general option name + * * @return array */ public static function get_type_option_names() { @@ -1794,18 +1982,34 @@ abstract class repository { } /** - * Return names of the instance options + * Return names of the instance options. * By default: no instance option name + * * @return array */ public static function get_instance_option_names() { return array(); } + /** + * Validate repository plugin instance form + * + * @param moodleform $mform moodle form + * @param array $data form data + * @param array $errors errors + * @return array errors + */ public static function instance_form_validation($mform, $data, $errors) { return $errors; } + /** + * Create a shorten filename + * + * @param string $str filename + * @param int $maxlength max file name length + * @return string short filename + */ public function get_short_filename($str, $maxlength) { if (textlib::strlen($str) >= $maxlength) { return trim(textlib::substr($str, 0, $maxlength)).'...'; @@ -1822,7 +2026,7 @@ abstract class repository { * @param string $filename * @param string $newfilepath * @param string $newfilename - * @return boolean + * @return bool */ public static function overwrite_existing_draftfile($itemid, $filepath, $filename, $newfilepath, $newfilename) { global $USER; @@ -1848,7 +2052,7 @@ abstract class repository { * @param int $draftitemid * @param string $filepath * @param string $filename - * @return boolean + * @return bool */ public static function delete_tempfile_from_draft($draftitemid, $filepath, $filename) { global $USER; @@ -1861,15 +2065,106 @@ abstract class repository { return false; } } + + /** + * Find all external files in this repo and import them + */ + public function convert_references_to_local() { + $fs = get_file_storage(); + $files = $fs->get_external_files($this->id); + foreach ($files as $storedfile) { + $fs->import_external_file($storedfile); + } + } + + + + /** + * Call to request proxy file sync with repository source. + * + * @param stored_file $file + * @return bool success + */ + public static function sync_external_file(stored_file $file) { + global $DB; + + $fs = get_file_storage(); + + if (!$reference = $DB->get_record('files_reference', array('id'=>$file->get_referencefileid()))) { + return false; + } + + if (!empty($reference->lastsync) and ($reference->lastsync + $reference->lifetime > time())) { + return false; + } + + if (!$repository = self::get_repository_by_id($reference->repositoryid, SYSCONTEXTID)) { + return false; + } + + if (!$repository->sync_individual_file($file)) { + return false; + } + + $fileinfo = $repository->get_file_by_reference($reference); + if ($fileinfo === null) { + // does not exist any more - set status to missing + $sql = "UPDATE {files} SET status = :missing WHERE referencefileid = :referencefileid"; + $params = array('referencefileid'=>$reference->id, 'missing'=>666); + $DB->execute($sql, $params); + //TODO: purge content from pool if we set some other content hash and it is no used any more + return true; + } else if ($fileinfo === false) { + // error + return false; + } + + $contenthash = null; + $filesize = null; + if (!empty($fileinfo->contenthash)) { + // contenthash returned, file already in moodle + $contenthash = $fileinfo->contenthash; + $filesize = $fileinfo->filesize; + } else if (!empty($fileinfo->filepath)) { + // File path returned + list($contenthash, $filesize, $newfile) = $fs->add_file_to_pool($fileinfo->filepath); + } else if (!empty($fileinfo->handle) && is_resource($fileinfo->handle)) { + // File handle returned + $contents = ''; + while (!feof($fileinfo->handle)) { + $contents .= fread($handle, 8192); + } + fclose($fileinfo->handle); + list($contenthash, $filesize, $newfile) = $fs->add_string_to_pool($content); + } else if (isset($fileinfo->content)) { + // File content returned + list($contenthash, $filesize, $newfile) = $fs->add_string_to_pool($fileinfo->content); + } + + if (!isset($contenthash) or !isset($filesize)) { + return false; + } + + $now = time(); + // update files table + $sql = "UPDATE {files} SET contenthash = :contenthash, filesize = :filesize, referencelastsync = :now, referencelifetime = :lifetime, timemodified = :now2 WHERE referencefileid = :referencefileid AND contenthash <> :contenthash2"; + $params = array('contenthash'=>$contenthash, 'filesize'=>$filesize, 'now'=>$now, 'lifetime'=>$reference->lifetime, + 'now2'=>$now, 'referencefileid'=>$reference->id, 'contenthash2'=>$contenthash); + $DB->execute($sql, $params); + + $DB->set_field('files_reference', 'lastsync', $now, array('id'=>$reference->id)); + + return true; + } } /** * Exception class for repository api * * @since 2.0 - * @package moodlecore - * @subpackage repository - * @copyright 2009 Dongsheng Cai + * @package repository + * @category repository + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_exception extends moodle_exception { @@ -1879,14 +2174,20 @@ class repository_exception extends moodle_exception { * This is a class used to define a repository instance form * * @since 2.0 - * @package moodlecore - * @subpackage repository - * @copyright 2009 Dongsheng Cai + * @package repository + * @category repository + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class repository_instance_form extends moodleform { + /** @var stdClass repository instance */ protected $instance; + /** @var string repository plugin type */ protected $plugin; + + /** + * Added defaults to moodle form + */ protected function add_defaults() { $mform =& $this->_form; $strrequired = get_string('required'); @@ -1906,6 +2207,9 @@ final class repository_instance_form extends moodleform { $mform->addRule('name', $strrequired, 'required', null, 'client'); } + /** + * Define moodle form elements + */ public function definition() { global $CFG; // type of plugin, string @@ -1952,6 +2256,13 @@ final class repository_instance_form extends moodleform { } } + /** + * Validate moodle form data + * + * @param array $data form data + * @param array $files files in form + * @return array errors + */ public function validation($data, $files) { global $DB; $errors = array(); @@ -1980,19 +2291,21 @@ final class repository_instance_form extends moodleform { * This is a class used to define a repository type setting form * * @since 2.0 - * @package moodlecore - * @subpackage repository - * @copyright 2009 Dongsheng Cai + * @package repository + * @category repository + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class repository_type_form extends moodleform { + /** @var stdClass repository instance */ protected $instance; + /** @var string repository plugin name */ protected $plugin; + /** @var string action */ protected $action; /** * Definition of the moodleform - * @global object $CFG */ public function definition() { global $CFG; @@ -2062,6 +2375,13 @@ final class repository_type_form extends moodleform { $this->add_action_buttons(true, get_string('save','repository')); } + /** + * Validate moodle form data + * + * @param array $data moodle form data + * @param array $files + * @return array errors + */ public function validation($data, $files) { $errors = array(); $plugin = $this->_customdata['plugin']; @@ -2081,7 +2401,7 @@ final class repository_type_form extends moodleform { /** * Generate all options needed by filepicker * - * @param array $args, including following keys + * @param array $args including following keys * context * accepted_types * return_types @@ -2093,6 +2413,7 @@ final class repository_type_form extends moodleform { */ function initialise_filepicker($args) { global $CFG, $USER, $PAGE, $OUTPUT; + static $templatesinitialized; require_once($CFG->libdir . '/licenselib.php'); $return = new stdClass(); @@ -2114,7 +2435,6 @@ function initialise_filepicker($args) { $return->author = fullname($USER); - $ft = new filetype_parser(); if (empty($args->context)) { $context = $PAGE->context; } else { @@ -2151,7 +2471,7 @@ function initialise_filepicker($args) { } // provided by form element - $return->accepted_types = $ft->get_extensions($args->accepted_types); + $return->accepted_types = file_get_typegroup('extension', $args->accepted_types); $return->return_types = $args->return_types; foreach ($repositories as $repository) { $meta = $repository->get_meta(); @@ -2159,10 +2479,18 @@ function initialise_filepicker($args) { // JavaScript a lot, the key NEEDS to be the repository id. $return->repositories[$repository->id] = $meta; } + if (!$templatesinitialized) { + // we need to send filepicker templates to the browser just once + $fprenderer = $PAGE->get_renderer('core', 'files'); + $templates = $fprenderer->filepicker_js_templates(); + $PAGE->requires->js_init_call('M.core_filepicker.set_templates', array($templates), true); + $templatesinitialized = true; + } return $return; } /** * Small function to walk an array to attach repository ID + * * @param array $value * @param string $key * @param int $id diff --git a/repository/local/lib.php b/repository/local/lib.php index 3f154382f53..72ba3d3c571 100644 --- a/repository/local/lib.php +++ b/repository/local/lib.php @@ -1,5 +1,4 @@ . /** - * repository_local class is used to browse moodle files + * This plugin is used to access local files * - * @since 2.0 + * @since 2.0 * @package repository_local - * @copyright 2009 Dongsheng Cai + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +require_once($CFG->dirroot . '/repository/lib.php'); +/** + * repository_local class is used to browse moodle files + * + * @since 2.0 + * @package repository_local + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class repository_local extends repository { /** * local plugin doesn't require login, so list all files @@ -200,15 +208,28 @@ class repository_local_file { $encodedpath = base64_encode(serialize($this->fileinfo->get_params())); $node = array( 'title' => $this->fileinfo->get_visible_name(), - 'size' => 0, - 'date' => ''); + 'datemodified' => $this->fileinfo->get_timemodified(), + 'datecreated' => $this->fileinfo->get_timecreated() + ); if ($this->isdir) { $node['path'] = $encodedpath; - $node['thumbnail'] = $OUTPUT->pix_url('f/folder-32')->out(false); + $node['thumbnail'] = $OUTPUT->pix_url(file_folder_icon(90))->out(false); $node['children'] = array(); } else { + $node['size'] = $this->fileinfo->get_filesize(); + $node['author'] = $this->fileinfo->get_author(); + $node['license'] = $this->fileinfo->get_license(); $node['source'] = $encodedpath; - $node['thumbnail'] = $OUTPUT->pix_url(file_extension_icon($node['title'], 32))->out(false); + $node['thumbnail'] = $OUTPUT->pix_url(file_file_icon($this->fileinfo, 90))->out(false); + $node['icon'] = $OUTPUT->pix_url(file_file_icon($this->fileinfo), 24)->out(false); + if ($imageinfo = $this->fileinfo->get_imageinfo()) { + // what a beautiful picture, isn't it + $fileurl = new moodle_url($this->fileinfo->get_url()); + $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $this->fileinfo->get_timemodified())); + $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $this->fileinfo->get_timemodified())); + $node['image_width'] = $imageinfo['width']; + $node['image_height'] = $imageinfo['height']; + } } return $node; } diff --git a/repository/merlot/lib.php b/repository/merlot/lib.php index 91f2da7e57c..0ad1418419b 100644 --- a/repository/merlot/lib.php +++ b/repository/merlot/lib.php @@ -1,5 +1,4 @@ . +/** + * This plugin is used to access merlot files + * + * @since 2.0 + * @package repository_merlot + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); + /** * repository_merlot is used to search merlot.org in moodle * * @since 2.0 - * @package repository - * @subpackage merlot - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_merlot + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_merlot extends repository { @@ -85,7 +92,7 @@ class repository_merlot extends repository { foreach ($xml->results->material as $entry) { $list[] = array( 'title'=>(string)$entry->title, - 'thumbnail'=>$OUTPUT->pix_url('f/unknown-32')->out(false), + 'thumbnail'=>$OUTPUT->pix_url(file_extension_icon($entry->title, 90))->out(false), 'date'=>userdate((int)$entry->creationDate), 'size'=>'', 'source'=>(string)$entry->URL diff --git a/repository/picasa/lib.php b/repository/picasa/lib.php index bd43d848242..6a83ca08238 100644 --- a/repository/picasa/lib.php +++ b/repository/picasa/lib.php @@ -1,5 +1,4 @@ . +/** + * This plugin is used to access picasa pictures + * + * @since 2.0 + * @package repository_picasa + * @copyright 2009 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); +require_once($CFG->libdir.'/googleapi.php'); + /** * Picasa Repository Plugin * @@ -25,9 +35,6 @@ * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -require_once($CFG->libdir.'/googleapi.php'); - class repository_picasa extends repository { private $subauthtoken = ''; diff --git a/repository/recent/lib.php b/repository/recent/lib.php index 1f4299a531b..a491015235e 100644 --- a/repository/recent/lib.php +++ b/repository/recent/lib.php @@ -15,16 +15,24 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This plugin is used to access recent used files + * + * @since 2.0 + * @package repository_recent + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); + /** * repository_recent class is used to browse recent used files * * @since 2.0 - * @package repository - * @subpackage recent - * @copyright 2010 Dongsheng Cai + * @package repository_recent + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - define('DEFAULT_RECENT_FILES_NUM', 50); class repository_recent extends repository { @@ -56,8 +64,12 @@ class repository_recent extends repository { private function get_recent_files($limitfrom = 0, $limit = DEFAULT_RECENT_FILES_NUM) { // XXX: get current itemid global $USER, $DB, $itemid; + // This SQL will ignore draft files if not owned by current user. + // Ignore all file references. $sql = 'SELECT files1.* FROM {files} files1 + LEFT JOIN {files_reference} r + ON files1.referencefileid = r.id JOIN ( SELECT contenthash, filename, MAX(id) AS id FROM {files} @@ -66,7 +78,8 @@ class repository_recent extends repository { AND ((filearea = :filearea1 AND itemid = :itemid) OR filearea != :filearea2) GROUP BY contenthash, filename ) files2 ON files1.id = files2.id - ORDER BY files1.timemodified DESC'; + WHERE r.repositoryid is NULL + ORDER BY files1.timemodified DESC'; $params = array( 'userid' => $USER->id, 'filename' => '.', @@ -107,17 +120,31 @@ class repository_recent extends repository { try { foreach ($files as $file) { - $params = base64_encode(serialize($file)); - // Check that file exists and accessible - $filesize = $this->get_file_size($params); - if (!empty($filesize)) { + // Check that file exists and accessible, retrieve size/date info + $browser = get_file_browser(); + $context = get_context_instance_by_id($file['contextid']); + $fileinfo = $browser->get_file_info($context, $file['component'], + $file['filearea'], $file['itemid'], $file['filepath'], $file['filename']); + if ($fileinfo) { + $params = base64_encode(serialize($file)); $node = array( - 'title' => $file['filename'], - 'size' => $filesize, - 'date' => '', + 'title' => $fileinfo->get_visible_name(), + 'size' => $fileinfo->get_filesize(), + 'datemodified' => $fileinfo->get_timemodified(), + 'datecreated' => $fileinfo->get_timecreated(), + 'author' => $fileinfo->get_author(), + 'license' => $fileinfo->get_license(), 'source'=> $params, - 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file['filename'], 32))->out(false), + 'icon' => $OUTPUT->pix_url(file_file_icon($fileinfo, 24))->out(false), + 'thumbnail' => $OUTPUT->pix_url(file_file_icon($fileinfo, 90))->out(false), ); + if ($imageinfo = $fileinfo->get_imageinfo()) { + $fileurl = new moodle_url($fileinfo->get_url()); + $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $fileinfo->get_timemodified())); + $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $fileinfo->get_timemodified())); + $node['image_width'] = $imageinfo['width']; + $node['image_height'] = $imageinfo['height']; + } $list[] = $node; } } diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index b7793c8b9b0..c0043880938 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -1,5 +1,4 @@ + * @package repository + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -34,7 +32,7 @@ require_once(dirname(__FILE__).'/lib.php'); $err = new stdClass(); -/// Parameters +// Parameters $action = optional_param('action', '', PARAM_ALPHA); $repo_id = optional_param('repo_id', 0, PARAM_INT); // Repository ID $contextid = optional_param('ctx_id', SYSCONTEXTID, PARAM_INT); // Context ID @@ -51,6 +49,7 @@ $saveas_filename = optional_param('title', '', PARAM_FILE); // save as file $saveas_path = optional_param('savepath', '/', PARAM_PATH); // save as file path $search_text = optional_param('s', '', PARAM_CLEANHTML); $linkexternal = optional_param('linkexternal', '', PARAM_ALPHA); +$usefilereference = optional_param('usefilereference', '', PARAM_ALPHA); list($context, $course, $cm) = get_context_info_array($contextid); require_login($course, false, $cm); @@ -59,7 +58,7 @@ $PAGE->set_context($context); echo $OUTPUT->header(); // send headers @header('Content-type: text/html; charset=utf-8'); -// if uploaded file is larger than post_max_size (php.ini) setting, $_POST content will lost +// If uploaded file is larger than post_max_size (php.ini) setting, $_POST content will be empty. if (empty($_POST) && !empty($action)) { $err->error = get_string('errorpostmaxsize', 'repository'); die(json_encode($err)); @@ -70,7 +69,7 @@ if (!confirm_sesskey()) { die(json_encode($err)); } -/// Get repository instance information +// Get repository instance information $sql = 'SELECT i.name, i.typeid, r.type FROM {repository} r, {repository_instances} i WHERE i.id=? AND i.typeid=r.id'; if (!$repository = $DB->get_record_sql($sql, array($repo_id))) { @@ -80,7 +79,7 @@ if (!$repository = $DB->get_record_sql($sql, array($repo_id))) { $type = $repository->type; } -/// Check permissions +// Check permissions repository::check_capability($contextid, $repository); $moodle_maxbytes = get_max_upload_file_size(); @@ -89,7 +88,7 @@ if ($maxbytes == 0 || $maxbytes>=$moodle_maxbytes) { $maxbytes = $moodle_maxbytes; } -/// Wait as long as it takes for this script to finish +// Wait as long as it takes for this script to finish set_time_limit(0); // Early actions which need to be done before repository instances initialised @@ -125,19 +124,25 @@ switch ($action) { if (file_exists($CFG->dirroot.'/repository/'.$type.'/lib.php')) { require_once($CFG->dirroot.'/repository/'.$type.'/lib.php'); $classname = 'repository_' . $type; - $repo = new $classname($repo_id, $contextid, array('ajax'=>true, 'name'=>$repository->name, 'type'=>$type)); + $repooptions = array( + 'ajax' => true, + 'name' => $repository->name, + 'type' => $type, + 'mimetypes' => $accepted_types + ); + $repo = new $classname($repo_id, $contextid, $repooptions); } else { $err->error = get_string('invalidplugin', 'repository', $type); die(json_encode($err)); } -/// These actions all occur on the currently active repository instance +// These actions all occur on the currently active repository instance switch ($action) { case 'sign': case 'signin': case 'list': if ($repo->check_login()) { - $listing = $repo->get_listing($req_path, $page); + $listing = repository::prepare_listing($repo->get_listing($req_path, $page)); $listing['repo_id'] = $repo_id; echo json_encode($listing); break; @@ -155,11 +160,13 @@ switch ($action) { echo json_encode($logout); break; case 'searchform': + $search_form['repo_id'] = $repo_id; $search_form['form'] = $repo->print_search(); + $search_form['allowcaching'] = true; echo json_encode($search_form); break; case 'search': - $search_result = $repo->search($search_text, (int)$page); + $search_result = repository::prepare_listing($repo->search($search_text, (int)$page)); $search_result['repo_id'] = $repo_id; $search_result['issearchresult'] = true; echo json_encode($search_result); @@ -174,7 +181,7 @@ switch ($action) { $mimetypes[] = mimeinfo('type', $type); } if (!in_array(mimeinfo('type', $saveas_filename), $mimetypes)) { - throw new moodle_exception('invalidfiletype', 'repository', '', get_string(mimeinfo('type', $saveas_filename), 'mimetypes')); + throw new moodle_exception('invalidfiletype', 'repository', '', get_mimetype_description(array('filename' => $saveas_filename))); } } @@ -201,45 +208,43 @@ switch ($action) { echo json_encode($info); die; } else { - // some repository plugins deal with moodle internal files, so we cannot use get_file + $fs = get_file_storage(); + // Some repository plugins are hosting moodle internal files, we cannot use get_file // method, so we use copy_to_area method // (local, user, coursefiles, recent) - if ($repo->has_moodle_files()) { + if ($repo->has_moodle_files() && ($usefilereference != 'yes')) { // check filesize against max allowed size $filesize = $repo->get_file_size($source); if (empty($filesize)) { - $err->error = get_string('filesizenull', 'repository'); - die(json_encode($err)); + $filesize = 0; } if (($maxbytes !== -1) && ($filesize > $maxbytes)) { throw new file_exception('maxbytes'); } + // If the moodle file is an alias to a file in external repository + // we copy this alias instead of create alias to alias + // {@link repository::copy_to_area()}. $fileinfo = $repo->copy_to_area($source, $itemid, $saveas_path, $saveas_filename); + if (!isset($fileinfo['event'])) { $fileinfo['file'] = $fileinfo['title']; } + echo json_encode($fileinfo); die; } - // Download file to moodle - $file = $repo->get_file($source, $saveas_filename); - if ($file['path'] === false) { - $err->error = get_string('cannotdownload', 'repository'); - die(json_encode($err)); - } - - // check if exceed maxbytes - if (($maxbytes!==-1) && (filesize($file['path']) > $maxbytes)) { - throw new file_exception('maxbytes'); - } + // Prepare file record. $record = new stdClass(); $record->filepath = $saveas_path; $record->filename = $saveas_filename; $record->component = 'user'; $record->filearea = 'draft'; - $record->itemid = $itemid; - + if (!is_numeric($itemid)) { + $record->itemid = 0; + } else { + $record->itemid = $itemid; + } if (!empty($file['license'])) { $record->license = $file['license']; } else { @@ -250,11 +255,84 @@ switch ($action) { } else { $record->author = $author; } - $record->source = !empty($file['url']) ? $file['url'] : ''; - $info = repository::move_to_filepool($file['path'], $record); - if (empty($info)) { - $info['e'] = get_string('error', 'moodle'); + if ($record->filepath !== '/') { + $record->filepath = trim($record->filepath, '/'); + $record->filepath = '/'.$record->filepath.'/'; + } + $usercontext = get_context_instance(CONTEXT_USER, $USER->id); + $now = time(); + $record->contextid = $usercontext->id; + $record->timecreated = $now; + $record->timemodified = $now; + $record->userid = $USER->id; + + + if ($usefilereference == 'yes') { + $reference = $repo->get_file_reference($source); + // get reference life time from repo + $record->referencelifetime = $repo->get_reference_file_lifetime($reference); + // Check if file exists. + if (repository::draftfile_exists($itemid, $saveas_path, $saveas_filename)) { + // File name being used, rename it. + $unused_filename = repository::get_unused_filename($itemid, $saveas_path, $saveas_filename); + $record->filename = $unused_filename; + // Create a file copy using unused filename. + $storedfile = $fs->create_file_from_reference($record, $repo_id, $reference); + + $event = array(); + $event['event'] = 'fileexists'; + $event['newfile'] = new stdClass; + $event['newfile']->filepath = $saveas_path; + $event['newfile']->filename = $unused_filename; + $event['newfile']->url = moodle_url::make_draftfile_url($itemid, $saveas_path, $unused_filename)->out(); + + $event['existingfile'] = new stdClass; + $event['existingfile']->filepath = $saveas_path; + $event['existingfile']->filename = $saveas_filename; + $event['existingfile']->url = moodle_url::make_draftfile_url($itemid, $saveas_path, $saveas_filename)->out();; + echo json_encode($event); + die; + } + $storedfile = $fs->create_file_from_reference($record, $repo_id, $reference); + // Repository plugin callback + // You can cache reository file in this callback + // or complete other tasks. + $repo->cache_file_by_reference($reference, $storedfile); + $info = array( + 'url'=>moodle_url::make_draftfile_url($storedfile->get_itemid(), $storedfile->get_filepath(), $storedfile->get_filename())->out(), + 'id'=>$storedfile->get_itemid(), + 'file'=>$storedfile->get_filename(), + 'icon' => $OUTPUT->pix_url(file_file_icon($storedfile, 32))->out(), + ); + echo json_encode($info); + die; + } else { + // Download file to moodle. + $downloadedfile = $repo->get_file($source, $saveas_filename); + if ($downloadedfile['path'] === false) { + $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'); + } + + // {@link file_restore_source_field_from_draft_file()} + $sourcefield = ''; + if (!empty($downloadedfile['url'])) { + $source = new stdClass; + $source->source = $downloadedfile['url']; + $sourcefield = serialize($source); + } + $record->source = $sourcefield; + + $info = repository::move_to_filepool($downloadedfile['path'], $record); + if (empty($info)) { + $info['e'] = get_string('error', 'moodle'); + } } echo json_encode($info); die; diff --git a/repository/s3/lib.php b/repository/s3/lib.php index e0a8f328f7f..2479050597c 100644 --- a/repository/s3/lib.php +++ b/repository/s3/lib.php @@ -15,20 +15,25 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This plugin is used to access s3 files + * + * @since 2.0 + * @package repository_s3 + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); +require_once('S3.php'); /** * This is a repository class used to browse Amazon S3 content. * * @since 2.0 - * @package repository - * @subpackage s3 - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_s3 + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -require_once('S3.php'); - class repository_s3 extends repository { /** @@ -73,7 +78,7 @@ class repository_s3 extends repository { $folder = array( 'title' => $bucket, 'children' => array(), - 'thumbnail'=>$OUTPUT->pix_url('f/folder-32')->out(false), + 'thumbnail'=>$OUTPUT->pix_url(file_folder_icon(90))->out(false), 'path'=>$bucket ); $tree[] = $folder; @@ -87,7 +92,7 @@ class repository_s3 extends repository { 'size'=>$file['size'], 'date'=>userdate($file['time']), 'source'=>$path.'/'.$file['name'], - 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file['name'], 32))->out(false) + 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file['name'], 90))->out(false) ); } } diff --git a/repository/tests/repository_test.php b/repository/tests/repository_test.php new file mode 100644 index 00000000000..46eaf449264 --- /dev/null +++ b/repository/tests/repository_test.php @@ -0,0 +1,61 @@ +. + +/** + * Repository API unit tests + * + * @package repository + * @category phpunit + * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("$CFG->dirroot/repository/lib.php"); + +class repositorylib_testcase extends advanced_testcase { + + /** + * Installing repository tests + * + * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} + */ + public function test_install_repository() { + global $CFG, $DB; + + $this->resetAfterTest(true); + + $syscontext = context_system::instance(); + $repositorypluginname = 'boxnet'; + // override repository permission + $capability = 'repository/' . $repositorypluginname . ':view'; + $allroles = $DB->get_records_menu('role', array(), 'id', 'archetype, id'); + assign_capability($capability, CAP_ALLOW, $allroles['guest'], $syscontext->id, true); + + $plugintype = new repository_type($repositorypluginname); + $pluginid = $plugintype->create(false); + $this->assertInternalType('int', $pluginid); + $args = array(); + $args['type'] = $repositorypluginname; + $repos = repository::get_instances($args); + $repository = reset($repos); + $this->assertInstanceOf('repository', $repository); + $info = $repository->get_meta(); + $this->assertEquals($repositorypluginname, $info->type); + } +} diff --git a/repository/upload/lib.php b/repository/upload/lib.php index 1b2bb9d85f7..db029d17e26 100644 --- a/repository/upload/lib.php +++ b/repository/upload/lib.php @@ -15,14 +15,22 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This plugin is used to upload files + * + * @since 2.0 + * @package repository_upload + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); + /** * A repository plugin to allow user uploading files * * @since 2.0 - * @package repository - * @subpackage upload - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_upload + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -193,18 +201,18 @@ class repository_upload extends repository { $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(); + $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();; + $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(), + 'url'=>moodle_url::make_draftfile_url($record->itemid, $record->filepath, $record->filename)->out(false), 'id'=>$record->itemid, 'file'=>$record->filename); } diff --git a/repository/url/lib.php b/repository/url/lib.php index bc31e9531f7..16f2eb07ae5 100644 --- a/repository/url/lib.php +++ b/repository/url/lib.php @@ -15,20 +15,26 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This plugin is used to access files by providing an url + * + * @since 2.0 + * @package repository_url + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); +require_once(dirname(__FILE__).'/locallib.php'); + /** * repository_url class * A subclass of repository, which is used to download a file from a specific url * * @since 2.0 - * @package repository - * @subpackage url - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_url + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -require_once(dirname(__FILE__).'/locallib.php'); - class repository_url extends repository { var $processedfiles = array(); diff --git a/repository/user/lib.php b/repository/user/lib.php index c86eb3cb02b..7a46557e1b0 100644 --- a/repository/user/lib.php +++ b/repository/user/lib.php @@ -16,19 +16,28 @@ // along with Moodle. If not, see . /** - * repository_user class is used to browse user private files + * This plugin is used to access user's private files * * @since 2.0 - * @package repository - * @subpackage user - * @copyright 2010 Dongsheng Cai + * @package repository_user + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +require_once($CFG->dirroot . '/repository/lib.php'); +/** + * repository_user class is used to browse user private files + * + * @since 2.0 + * @package repository_user + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class repository_user extends repository { /** * user plugin doesn't require login + * * @return mixed */ public function print_login() { @@ -87,22 +96,33 @@ class repository_user extends repository { $encodedpath = base64_encode(serialize($child->get_params())); $node = array( 'title' => $child->get_visible_name(), - 'size' => 0, - 'date' => '', + 'datemodified' => $child->get_timemodified(), + 'datecreated' => $child->get_timecreated(), 'path' => $encodedpath, 'children'=>array(), - 'thumbnail' => $OUTPUT->pix_url('f/folder-32')->out(false) + 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(90))->out(false) ); $list[] = $node; } else { $encodedpath = base64_encode(serialize($child->get_params())); $node = array( 'title' => $child->get_visible_name(), - 'size' => 0, - 'date' => '', + 'size' => $child->get_filesize(), + 'datemodified' => $child->get_timemodified(), + 'datecreated' => $child->get_timecreated(), + 'author' => $child->get_author(), + 'license' => $child->get_license(), 'source'=> $encodedpath, - 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($child->get_visible_name(), 32))->out(false) + 'icon' => $OUTPUT->pix_url(file_file_icon($child, 24))->out(false), + 'thumbnail' => $OUTPUT->pix_url(file_file_icon($child, 90))->out(false) ); + if ($imageinfo = $child->get_imageinfo()) { + $fileurl = new moodle_url($child->get_url()); + $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $child->get_timemodified())); + $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $child->get_timemodified())); + $node['image_width'] = $imageinfo['width']; + $node['image_height'] = $imageinfo['height']; + } $list[] = $node; } } @@ -115,15 +135,6 @@ class repository_user extends repository { return $ret; } - /** - * User file don't support to link to external links - * - * @return int - */ - public function supported_returntypes() { - return FILE_INTERNAL; - } - /** * Does this repository used to browse moodle files? * @@ -132,4 +143,122 @@ class repository_user extends repository { public function has_moodle_files() { return true; } + + /** + * User cannot use the external link to dropbox + * + * @return int + */ + public function supported_returntypes() { + return FILE_INTERNAL | FILE_REFERENCE; + } + + + /** + * Prepare file reference information + * + * @param string $source + * @return string file referece + */ + public function get_file_reference($source) { + global $USER; + $params = unserialize(base64_decode($source)); + if (is_array($params)) { + $filepath = clean_param($params['filepath'], PARAM_PATH);; + $filename = clean_param($params['filename'], PARAM_FILE); + $contextid = clean_param($params['contextid'], PARAM_INT); + } + // We store all file parameters, so file api could + // find the refernces later. + $reference = array(); + $reference['contextid'] = $contextid; + $reference['component'] = 'user'; + $reference['filearea'] = 'private'; + $reference['itemid'] = 0; + $reference['filepath'] = $filepath; + $reference['filename'] = $filename; + + return file_storage::pack_reference($reference); + } + + /** + * Get file from external repository by reference + * {@link repository::get_file_reference()} + * {@link repository::get_file()} + * + * @param stdClass $reference file reference db record + * @return stdClass|null|false + */ + public function get_file_by_reference($reference) { + $fs = get_file_storage(); + $ref = $reference->reference; + $params = unserialize(base64_decode($ref)); + if (!is_array($params)) { + throw new repository_exception('invalidparams', 'repository'); + } + $filename = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE); + $filepath = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH);; + $contextid = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT); + + // hard coded component, filearea and item for security + $component = 'user'; + $filearea = 'private'; + $itemid = 0; + + $storedfile = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename); + + $fileinfo = new stdClass; + $fileinfo->contenthash = $storedfile->get_contenthash(); + $fileinfo->filesize = $storedfile->get_filesize(); + return $fileinfo; + } + + /** + * Return human readable reference information + * {@link stored_file::get_reference()} + * + * @param string $reference + * @return string|null + */ + public function get_reference_details($reference) { + $params = file_storage::unpack_reference($reference); + // Indicate this is from user private area + return $this->get_name() . ': ' . $params['filepath'] . $params['filename']; + } + + /** + * Return reference file life time + * + * @param string $ref + * @return int + */ + public function get_reference_file_lifetime($ref) { + // this should be realtime + return 0; + } + + /** + * Repository method to serve file + * + * @param stored_file $storedfile + * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours) + * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only + * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin + * @param array $options additional options affecting the file serving + */ + public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) { + $reference = $storedfile->get_reference(); + $params = file_storage::unpack_reference($reference); + $filepath = clean_param($params['filepath'], PARAM_PATH);; + $filename = clean_param($params['filename'], PARAM_FILE); + $contextid = clean_param($params['contextid'], PARAM_INT); + $filearea = 'private'; + $component = 'user'; + $itemid = 0; + + $fs = get_file_storage(); + $storedfile = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename); + + send_stored_file($storedfile, $lifetime, $filter, $forcedownload, $options); + } } diff --git a/repository/webdav/lib.php b/repository/webdav/lib.php index ea4444d2fc5..a8d4bb9be79 100644 --- a/repository/webdav/lib.php +++ b/repository/webdav/lib.php @@ -15,19 +15,25 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This plugin is used to access webdav files + * + * @since 2.0 + * @package repository_webdav + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); +require_once($CFG->libdir.'/webdavlib.php'); + /** * repository_webdav class * * @since 2.0 - * @package repository - * @subpackage webdav - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_webdav + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -require_once($CFG->libdir.'/webdavlib.php'); - class repository_webdav extends repository { public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { parent::__construct($repositoryid, $context, $options); @@ -121,7 +127,7 @@ class repository_webdav extends repository { if ($path != $v['href']) { $folders[] = array( 'title'=>$title, - 'thumbnail'=>$OUTPUT->pix_url('f/folder-32')->out(false), + 'thumbnail'=>$OUTPUT->pix_url(file_folder_icon(90))->out(false), 'children'=>array(), 'datemodified'=>$v['lastmodified'], 'path'=>$v['href'] @@ -132,7 +138,7 @@ class repository_webdav extends repository { $size = !empty($v['getcontentlength'])? $v['getcontentlength']:''; $files[] = array( 'title'=>$title, - 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($title, 32))->out(false), + 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($title, 90))->out(false), 'size'=>$size, 'datemodified'=>$v['lastmodified'], 'source'=>$v['href'] diff --git a/repository/wikimedia/lib.php b/repository/wikimedia/lib.php index 8583bbab9c6..50d85562873 100644 --- a/repository/wikimedia/lib.php +++ b/repository/wikimedia/lib.php @@ -15,17 +15,24 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -require_once('wikimedia.php'); +/** + * This plugin is used to access wikimedia files + * + * @since 2.0 + * @package repository_wikimedia + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); +require_once(dirname(__FILE__) . '/wikimedia.php'); /** * repository_wikimedia class * This is a class used to browse images from wikimedia * * @since 2.0 - * @package repository - * @subpackage wikimedia - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_wikimedia + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/repository/wikimedia/wikimedia.php b/repository/wikimedia/wikimedia.php index ce186b8f21e..d02b181be59 100644 --- a/repository/wikimedia/wikimedia.php +++ b/repository/wikimedia/wikimedia.php @@ -118,9 +118,9 @@ class wikimedia { $commons_main_dir = 'http://upload.wikimedia.org/wikipedia/commons/'; if ($image_url) { $short_path = str_replace($commons_main_dir, '', $image_url); - $extension = pathinfo($short_path, PATHINFO_EXTENSION); + $extension = strtolower(pathinfo($short_path, PATHINFO_EXTENSION)); if (strcmp($extension, 'gif') == 0) { //no thumb for gifs - return $OUTPUT->pix_url(file_extension_icon('xx.jpg', 32)); + return $OUTPUT->pix_url(file_extension_icon('.gif', $thumb_width))->out(false); } $dir_parts = explode('/', $short_path); $file_name = end($dir_parts); diff --git a/repository/youtube/lib.php b/repository/youtube/lib.php index 41151f9a3b4..7a7c695ed5b 100644 --- a/repository/youtube/lib.php +++ b/repository/youtube/lib.php @@ -1,5 +1,4 @@ . +/** + * This plugin is used to access youtube videos + * + * @since 2.0 + * @package repository_youtube + * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once($CFG->dirroot . '/repository/lib.php'); + /** * repository_youtube class * * @since 2.0 - * @package repository - * @subpackage youtube - * @copyright 2009 Dongsheng Cai - * @author Dongsheng Cai + * @package repository_youtube + * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/theme/base/config.php b/theme/base/config.php index 561fb1ddbaf..978396c955b 100644 --- a/theme/base/config.php +++ b/theme/base/config.php @@ -48,7 +48,8 @@ $THEME->sheets = array( 'grade', 'message', 'question', - 'user' + 'user', + 'filemanager' ); $THEME->editor_sheets = array('editor'); diff --git a/theme/base/pix/fp/alias.png b/theme/base/pix/fp/alias.png new file mode 100644 index 00000000000..e1695923796 Binary files /dev/null and b/theme/base/pix/fp/alias.png differ diff --git a/theme/base/pix/fp/check.png b/theme/base/pix/fp/check.png new file mode 100644 index 00000000000..1fd55ffbcc2 Binary files /dev/null and b/theme/base/pix/fp/check.png differ diff --git a/theme/base/pix/fp/cross.png b/theme/base/pix/fp/cross.png new file mode 100644 index 00000000000..7899d43f9eb Binary files /dev/null and b/theme/base/pix/fp/cross.png differ diff --git a/theme/base/pix/fp/dnd_arrow.png b/theme/base/pix/fp/dnd_arrow.png new file mode 100644 index 00000000000..771f95a6a95 Binary files /dev/null and b/theme/base/pix/fp/dnd_arrow.png differ diff --git a/theme/base/pix/fp/link.png b/theme/base/pix/fp/link.png new file mode 100644 index 00000000000..c456eced04a Binary files /dev/null and b/theme/base/pix/fp/link.png differ diff --git a/theme/base/pix/fp/path_folder.png b/theme/base/pix/fp/path_folder.png new file mode 100644 index 00000000000..432b50688da Binary files /dev/null and b/theme/base/pix/fp/path_folder.png differ diff --git a/theme/base/pix/fp/view_icon_active.png b/theme/base/pix/fp/view_icon_active.png new file mode 100644 index 00000000000..1aade6b4a47 Binary files /dev/null and b/theme/base/pix/fp/view_icon_active.png differ diff --git a/theme/base/pix/fp/view_icon_inactive.png b/theme/base/pix/fp/view_icon_inactive.png new file mode 100644 index 00000000000..d4eca1d5c90 Binary files /dev/null and b/theme/base/pix/fp/view_icon_inactive.png differ diff --git a/theme/base/pix/fp/view_icon_selected.png b/theme/base/pix/fp/view_icon_selected.png new file mode 100644 index 00000000000..2d6c45ec654 Binary files /dev/null and b/theme/base/pix/fp/view_icon_selected.png differ diff --git a/theme/base/pix/fp/view_list_active.png b/theme/base/pix/fp/view_list_active.png new file mode 100644 index 00000000000..e0977a0f80b Binary files /dev/null and b/theme/base/pix/fp/view_list_active.png differ diff --git a/theme/base/pix/fp/view_list_inactive.png b/theme/base/pix/fp/view_list_inactive.png new file mode 100644 index 00000000000..b20c0686515 Binary files /dev/null and b/theme/base/pix/fp/view_list_inactive.png differ diff --git a/theme/base/pix/fp/view_list_selected.png b/theme/base/pix/fp/view_list_selected.png new file mode 100644 index 00000000000..5d2e2cbc147 Binary files /dev/null and b/theme/base/pix/fp/view_list_selected.png differ diff --git a/theme/base/pix/fp/view_tree_active.png b/theme/base/pix/fp/view_tree_active.png new file mode 100644 index 00000000000..8819de3f07a Binary files /dev/null and b/theme/base/pix/fp/view_tree_active.png differ diff --git a/theme/base/pix/fp/view_tree_inactive.png b/theme/base/pix/fp/view_tree_inactive.png new file mode 100644 index 00000000000..68402b86eb4 Binary files /dev/null and b/theme/base/pix/fp/view_tree_inactive.png differ diff --git a/theme/base/pix/fp/view_tree_selected.png b/theme/base/pix/fp/view_tree_selected.png new file mode 100644 index 00000000000..37ee7d6b345 Binary files /dev/null and b/theme/base/pix/fp/view_tree_selected.png differ diff --git a/theme/base/style/core.css b/theme/base/style/core.css index ceb387d10c2..8e7c3a7a7e6 100644 --- a/theme/base/style/core.css +++ b/theme/base/style/core.css @@ -444,108 +444,6 @@ body.tag .managelink {padding: 5px;} .tag_cloud .s1 {font-size: 0.9em;} .tag_cloud .s0 {font-size: 0.8em;} -/** - * File picker - * Copyright (c) 2006 Yahoo! Inc. All rights reserved. - * copy from yui/examples/treeview/assets/css/folders/tree.css - * first or middle sibling, no children - */ -/* first or middle sibling, no children */ -.file-picker .ygtvtn {background: url([[pix:moodle|y/tn]]) 0 0 no-repeat;width:17px;height:22px;} -/* first or middle sibling, collapsable */ -.file-picker .ygtvtm {background: url([[pix:moodle|y/tm]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* first or middle sibling, collapsable, hover */ -.file-picker .ygtvtmh {background: url([[pix:moodle|y/tmh]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* first or middle sibling, expandable */ -.file-picker .ygtvtp {background: url([[pix:moodle|y/tp]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* first or middle sibling, expandable, hover */ -.file-picker .ygtvtph {background: url([[pix:moodle|y/tph]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* last sibling, no children */ -.file-picker .ygtvln {background: url([[pix:moodle|y/ln]]) 0 0 no-repeat;width:17px;height:22px;} -/* Last sibling, collapsable */ -.file-picker .ygtvlm {background: url([[pix:moodle|y/lm]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* Last sibling, collapsable, hover */ -.file-picker .ygtvlmh {background: url([[pix:moodle|y/lmh]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* Last sibling, expandable */ -.file-picker .ygtvlp {background: url([[pix:moodle|y/lp]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* Last sibling, expandable, hover */ -.file-picker .ygtvlph {background: url([[pix:moodle|y/lph]]) 0 0 no-repeat;width:34px;height:22px;cursor:pointer;} -/* Loading icon */ -.file-picker .ygtvloading {background: url([[pix:moodle|y/loading]]) 0 0 no-repeat;width:16px;height:22px;} -/* the style for the empty cells that are used for rendering the depth* of the node */ -.file-picker .ygtvdepthcell {background: url([[pix:moodle|y/vline]]) 0 0 no-repeat;width:17px;height:22px;} -.file-picker .ygtvblankdepthcell {width:17px;height:22px;} -/* the style of the div around each node */ -.file-picker .ygtvitem table{margin-bottom:0;} -.file-picker .ygtvitem td {border:none;padding:0;} -/* the style of the div around each node's collection of children */ -* html .file-picker .ygtvchildren {height:1%;} -/* the style of the text label in ygTextNode */ -.file-picker .ygtvlabel, -.file-picker .ygtvlabel:link, -.file-picker .ygtvlabel:visited, -.file-picker .ygtvlabel:hover {margin-left:2px;text-decoration: none;} -.file-picker {font-size:12px;} -.file-picker strong {background:#FFFFCC;} -.file-picker .fp-panel {padding:0;margin:0;text-align:left;} -.file-picker .fp-login-form {text-align:center;} -.file-picker .fp-searchbar {float:right;} -.file-picker .fp-viewbar {width:300px;float:left;} -.file-picker .fp-toolbar {padding: .8em;background: #FFFFCC;text-align:center;margin: 3px;} -.file-picker .fp-toolbar a {padding: 0 .5em;} -.file-picker .fp-list {list-style-type:none;padding:0;float:left;width:100%;margin:0;} -.dir-rtl .file-picker .fp-list {text-align:right;} -.file-picker .fp-list li {border-bottom:1px dotted gray;margin-bottom: 1em;} -.file-picker .fp-repo-name {display:block;padding: .5em;margin-bottom: .5em;} -.file-picker .fp-pathbar {margin: .4em;border-bottom: 1px dotted gray;} -.file-picker .fp-pathbar a {padding: .4em;} -.file-picker .fp-rename-form {text-align:center;} -.file-picker .fp-rename-form p {margin: 1em;} -.file-picker .fp-upload-form {margin: 2em 0;text-align:center;} -.file-picker .fp-upload-btn {clear:both;margin-top: 2em;} -.file-picker .fp-paging {margin:1em .5em;clear:both;text-align:center;line-height: 2.5em;} -.file-picker .fp-paging a {padding: .5em;border: 1px solid #CCC;} -.file-picker .fp-paging a.cur_page {border: 1px solid blue;} -.file-picker .fp-popup {text-align:center;} -.file-picker .fp-grid-panel{} -.file-picker .fp-grid {float:left;text-align:center;} -.file-picker .fp-grid div {overflow: hidden;} -.file-picker .fp-grid p {margin:0;padding:0;background: #FFFFCC;} -.file-picker .fp-grid .label {height:48px;text-align:center;} -.file-picker .fp-grid span {color:gray;} -.file-picker .fp-error {padding: 2em 0;margin: 3em 5px;text-align:center;background: #FFBBBB;} -.file-picker .fp-emptylist, .file-picker .fp-msg {text-align:center;} -.filepicker-filelist {padding: 5px;margin: 6px 0;background: #E9F4FF;border: #AACCEE 1px solid} -/* file picker search dialog */ -.file-picker div.bd {text-align:left;} - -/** - * File Manager - */ -.filemanager-toolbar {margin: 5px 0;} -.filemanager-toolbar a {border: 1px solid #AACCEE;background: #F4FAFF;color: black;padding: 3px;} -.filemanager-toolbar a:hover {background: #FFFFFF;} -.filemanager-toolbar .helplink a {border: 0px; background: transparent;} -.fm-breadcrumb {margin:0;} -.filemanager-container {padding: 5px;margin: 6px 0;background: #E9F4FF;border: #AACCEE 1px solid} -.filemanager-container ul{margin:0;padding:0;} -.filemanager-container ul li{white-space:nowrap;list-style-type:none;} -.filemanager-container ul li a{padding:0} -.filemanager-loading{display:none} -.jsenabled .filemanager-loading{display:block} -.fm-menuicon{cursor: pointer;} -#fm-move-div {margin: 6px;} -#fm-move-div strong{color:red;} -.fm-file-entry{border: 1px solid red;} -.fm-operation {font-weight: bold;} - -.filemanager-container, -.filepicker-filelist {overflow:hidden;} -.filemanager-container .dndupload-target, -.filepicker-filelist .dndupload-target {background:#f7f998;position:absolute;height:100%;width:100%;top:0;left:0;text-align:center;padding:5px;z-index:1000} -.filemanager-container.dndupload-over .dndupload-target, -.filepicker-filelist.dndupload-over .dndupload-target {background:#8EF947;font-weight:bold} - /* * Backup and Restore CSS */ diff --git a/theme/base/style/filemanager.css b/theme/base/style/filemanager.css new file mode 100644 index 00000000000..09f898379fa --- /dev/null +++ b/theme/base/style/filemanager.css @@ -0,0 +1,326 @@ +/** + * File Picker and File Manager + */ + +.filemanager, .file-picker {font-size:11px;color: #555;letter-spacing: .2px;} +.filemanager a, .file-picker a {color:#555;} +.filemanager a:hover, .file-picker a:hover {color:#555;text-decoration: none;} +.filemanager select, .filemanager input, .filemanagerbutton, .filemanager textarea, +.file-picker select, .file-picker input, .file-picker button, .file-picker textarea {color:#555; letter-spacing: .2px;} +.filemanager input[type="text"], .file-picker input[type="text"] {border: 1px solid #BBB;width: 265px;height: 18px;padding: 1px 6px;} +.filemanager select, .file-picker select {height: 22px;padding: 2px 1px;} +.fp-content-center {height: 100%;width: 100%;display:table-cell;vertical-align: middle;} + +/* + * Dialogue (File Picker and File Manager) + */ +.yui3-panel-focused {outline: none;} +.yui3-panel-content {padding-bottom: 20px!important;background: #F2F2F2!important;border-radius: 8px;border: 1px solid #FFF!important;display: inline-block;-webkit-box-shadow: 5px 5px 20px 0px #666!important;-moz-box-shadow: 5px 5px 20px 0px #666!important;box-shadow: 5px 5px 20px 0px #666!important;} +.yui3-widget-hd {border-radius: 10px 10px 0px 0px;border-bottom: 1px solid #BBB;padding:5px 5px 5px 5px!important;text-align: center;font-size:12px;letter-spacing: 1px;color:#333!important; text-shadow: 1px 1px 1px #FFF;filter: dropshadow(color=#FFF, offx=1, offy=1); +background: #CCC!important;filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFF', endColorstr='#CCC')!important;background: -webkit-gradient(linear, left top, left bottom, from(#FFF), to(#CCC))!important;background: -moz-linear-gradient(top, #FFF, #CCC)!important;} +.fp-panel-button {background: #FFF;padding: 3px 20px 2px 20px; text-align: center;margin:10px; border-radius: 10px;display: inline-block;-webkit-box-shadow: 2px 2px 3px .1px #999;-moz-box-shadow: 2px 2px 3px .1px #999;box-shadow: 2px 2px 3px .1px #999;} + +/* + * File Picker layout + */ +.file-picker.fp-generallayout {width: 724px;background: #FFF!important;border-radius:10px;border: 1px solid #CCC!important;position: relative;} +.file-picker .fp-repo-area {width:180px;overflow:auto;display:inline-block;border-right:1px solid #BBB;position:absolute;top:26px;bottom:1px;} +.file-picker .fp-repo-items {vertical-align:top;display: inline-block;margin-left: 181px;} +.file-picker .fp-navbar {background: #F2F2F2;min-height:22px;border-bottom: 1px solid #BBB;padding: 5px 8px;} +.file-picker .fp-content {background: #FFF;clear: both;overflow:auto;width: 543px;height: 349px;margin-bottom:-14px;} +.file-picker .fp-content-loading {height: 100%;width: 100%;display: table;text-align:center;} +.file-picker .fp-content .fp-object-container {width:98%;height:98%;} + +/* + * Repositories on fp-repo-area (File Picker only) + */ +.file-picker .fp-list {list-style-type:none;padding:0;float:left;width:100%;margin:0;} +.dir-rtl .file-picker .fp-list {text-align:right;} +.file-picker .fp-list .fp-repo a{display:block;padding:.5em .7em;} +.file-picker .fp-list .fp-repo.active {background:#F2F2F2;} +.file-picker .fp-repo-icon {padding: 0px 7px 0px 5px;} + +/* + * Tools, Path & View on fp-navbar (File Picker and File Manager) + */ +.fp-toolbar {display: table-row;line-height: 22px;float:left;} +.fp-toolbar.empty {display:none;} +.fp-toolbar .disabled {display:none;} +.fp-toolbar div {display: inline-block;padding: 0px 2px;} +.fp-toolbar img {vertical-align: -15%; margin-right: 5px;} +.file-picker .search-entry {background:#FFF url('[[pix:a/search]]') no-repeat 3px 3px;height:18px;width:230px;border: 1px solid #BBB;padding-left:20px;} + +.file-picker .fp-pathbar {display: table-row;} +.fp-pathbar .fp-path-folder {background:url('[[pix:theme|fp/path_folder]]') no-repeat 0 0;width:27px;height:12px;margin-left: 4px;} +.fp-pathbar .fp-path-folder-name {margin-left: 32px;line-height: 20px;} + +.fp-viewbar {float:right;width:69px;height:22px;margin-right:8px;} +.fp-vb-icons {background:url('[[pix:theme|fp/view_icon_active]]') no-repeat 0 0;width:22px;height:22px;display: inline-block;} +.fp-vb-icons.checked {background:url('[[pix:theme|fp/view_icon_selected]]');} +.fp-viewbar.disabled .fp-vb-icons {background:url('[[pix:theme|fp/view_icon_inactive]]');} +.fp-vb-details {background:url('[[pix:theme|fp/view_list_active]]') no-repeat 0px 0px;width:23px;height:22px;display: inline-block;margin-left: -4px;} +.fp-vb-details.checked {background:url('[[pix:theme|fp/view_list_selected]]');} +.fp-viewbar.disabled .fp-vb-details {background:url('[[pix:theme|fp/view_list_inactive]]');} +.fp-vb-tree {background:url('[[pix:theme|fp/view_tree_active]]') no-repeat 0px 0px;width:23px;height:22px;display: inline-block;margin-left: -4px;} +.fp-vb-tree.checked {background:url('[[pix:theme|fp/view_tree_selected]]');} +.fp-viewbar.disabled .fp-vb-tree {background:url('[[pix:theme|fp/view_tree_inactive]]');} + +/* + * Icon view (File Picker and File Manager) + */ +.fp-iconview .fp-file {float: left;text-align: center;position: relative;margin: 10px 10px 35px;} +.fp-iconview .fp-thumbnail {min-width:110px;min-height:110px;line-height: 110px;text-align: center;border: 1px solid #FFF;} +.fp-iconview .fp-thumbnail img {border: 1px solid #DDD;padding:3px;vertical-align: middle;-webkit-box-shadow: 1px 1px 2px 0px #CCC;-moz-box-shadow: 1px 1px 2px 0px #CCC;box-shadow: 1px 1px 2px 0px #CCC;} +.fp-iconview .fp-thumbnail:hover {background: #FFF;border: 1px solid #DDD;-webkit-box-shadow: inset 0px 0px 10px 0px #CCC;-moz-box-shadow: inset 0px 0px 10px 0px #CCC;box-shadow: inset 0px 0px 10px 0px #CCC;} +.fp-iconview .fp-filename-field {height:33px;word-wrap:break-word;overflow: hidden;position: absolute;} +.fp-iconview .fp-filename-field:hover {overflow: visible;z-index: 1000;} +.fp-iconview .fp-filename-field .fp-filename {background: #FFF;padding-top: 5px;padding-bottom: 12px;min-width:112px;} + +/* + * Table view (File Picker only) + */ +.file-picker .yui3-datatable table {border: 0px solid #BBB;width:100%;} +.file-picker .yui3-datatable-header {background: #FFF;border-bottom: 1px solid #CCC;border-left: 0 solid #FFF;color: #555;} +.file-picker .yui3-datatable-columns .yui3-datatable-sorted, .file-picker .yui3-datatable-sortable-column:hover {background-color: #FFF;} +.file-picker .yui3-datatable-odd .yui3-datatable-cell {background-color: #F6F6F6;border-left: 0px solid #F6F6F6;} +.file-picker .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted {background-color: #F6F6F6;} +.file-picker .yui3-datatable-even .yui3-datatable-cell {background-color: #FFF;border-left: 0px solid #FFF;} +.file-picker .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted {background-color: #FFF;} +.file-picker .fp-icon img {max-height:16px;max-width:16px;} + +/* + * Tree view (File Picker and File Manager) + */ +/*.file-picker .fp-treeview .fp-folder .fp-icon, .filemanager .fp-treeview .fp-folder .fp-icon {}*/ +/* first or middle sibling, no children */ +/*.file-picker .ygtvtn, .filemanager*/ .ygtvtn {background: url('[[pix:moodle|y/tn]]') 0 0 no-repeat;width:17px;height:22px;} +/* first or middle sibling, collapsable */ +/*.file-picker .ygtvtm, .filemanager*/ .ygtvtm {background: url('[[pix:moodle|y/tm]]') 0 0 no-repeat;width:13px;height:12px;cursor:pointer;} +/* first or middle sibling, collapsable, hover */ +/*.file-picker .ygtvtmh, .filemanager*/ .ygtvtmh {background: url('[[pix:moodle|y/tm]]') 0 0 no-repeat;width:13px;height:12px;cursor:pointer;} +/* first or middle sibling, expandable */ +/*.file-picker .ygtvtp, .filemanager*/ .ygtvtp {background: url('[[pix:moodle|y/tp]]') 0 0 no-repeat;width:13px;height:12px;cursor:pointer;} +/* first or middle sibling, expandable, hover */ +/*.file-picker .ygtvtph, .filemanager*/ .ygtvtph {background: url('[[pix:moodle|y/tp]]') 0 0 no-repeat;width:13px;height:22px;cursor:pointer;} +/* last sibling, no children */ +/*.file-picker .ygtvln, .filemanager*/ .ygtvln {background: url('[[pix:moodle|y/ln]]') 0 0 no-repeat;width:17px;height:22px;} +/* Last sibling, collapsable */ +/*.file-picker .ygtvlm, .filemanager*/ .ygtvlm {background: url('[[pix:moodle|y/lm]]') 0 0 no-repeat;width:13px;height:12px;cursor:pointer;} +/* Last sibling, collapsable, hover */ +/*.file-picker .ygtvlmh, .filemanager*/ .ygtvlmh {background: url('[[pix:moodle|y/lm]]') 0 0 no-repeat;width:13px;height:12px;cursor:pointer;} +/* Last sibling, expandable */ +/*.file-picker .ygtvlp, .filemanager*/ .ygtvlp {background: url('[[pix:moodle|y/lp]]') 0 0 no-repeat;width:13px;height:12px;cursor:pointer;} +/* Last sibling, expandable, hover */ +/*.file-picker .ygtvlph, .filemanager*/ .ygtvlph {background: url('[[pix:moodle|y/lp]]') 0 0 no-repeat;width:13px;height:12px;cursor:pointer;} +/* Loading icon */ +/*.file-picker .ygtvloading, .filemanager*/ .ygtvloading {background: url('[[pix:moodle|y/loading]]') 0 0 no-repeat;width:16px;height:22px;} +/* the style for the empty cells that are used for rendering the depth* of the node */ +/*.file-picker .ygtvdepthcell, .filemanager*/ .ygtvdepthcell {background: url('[[pix:moodle|y/vline]]') 0 0 no-repeat;width:17px;height:22px;} +/*.file-picker .ygtvblankdepthcell, .filemanager*/ .ygtvblankdepthcell {width:17px;height:22px;} +/* the style of the div around each node */ +/*.file-picker .ygtvitem table, .filemanager .ygtvitem table{margin-bottom:0;}*/ +/*.file-picker .ygtvitem td, .filemanager .ygtvitem td {border:none;padding:0;}*/ +/* the style of the div around each node's collection of children */ +/** html .file-picker .ygtvchildren, * html .filemanager .ygtvchildren {height:1%;}*/ +/* the style of the text label in ygTextNode */ +/*.file-picker .ygtvlabel,.file-picker .ygtvlabel:link,.file-picker .ygtvlabel:visited,.file-picker .ygtvlabel:hover, +.filemanager .ygtvlabel,.filemanager .ygtvlabel:link,.filemanager .ygtvlabel:visited,.filemanager .ygtvlabel:hover {margin-left:2px;text-decoration: none;}*/ +a.ygtvspacer:hover {color: transparent;text-decoration: none;} +.ygtvlabel, .ygtvlabel:link, .ygtvlabel:visited, .ygtvlabel:hover {background-color: transparent;cursor: pointer;margin-left: 2px;text-decoration: none;} +/*.file-picker*/ .ygtvfocus {background-color: #DDD;} + +/* + * Repositories Login on fp-content (File Picker only) + */ +.file-picker .fp-login-form {height: 100%;width: 100%;display: table;} +.file-picker .fp-login-form table {margin: 0 auto;} +.file-picker .fp-login-form p {text-align: center;margin-top: 3em;} +.file-picker .fp-login-form .fp-login-input .label {text-align: right;vertical-align: middle;} +.file-picker .fp-login-form .fp-login-input .input {text-align: left;} +.file-picker .fp-login-form input[type="checkbox"]{width: 15px;height:15px;} + +/* + * Upload on fp-content (File Picker only) + */ +.file-picker .fp-upload-form {height: 100%;width: 100%;display: table;} +.file-picker .fp-upload-form table {margin: 0 auto;} +.file-picker .fp-upload-btn {margin: 2em;} + +/* + * File exists dialogue on Upload (File Picker only) + */ +.file-picker.fp-dlg {text-align: center;} +.file-picker.fp-dlg .fp-dlg-text {padding: 40px 20px 10px 20px;font-size: 12px;} + +/* + * Error dialogue on Upload (File Picker only) + */ +.file-picker.fp-msg {text-align: center;} +.file-picker.fp-msg .fp-msg-text {padding: 40px 20px 10px 20px;min-width:200px;max-width:500px;max-height:300px;overflow:auto;font-size: 12px;} +.file-picker.fp-msg.fp-msg-error .fp-msg-text {padding: 40px 20px 10px 20px;font-size: 12px;} + +/* + * Error on fp-content (File Picker only) + */ +.file-picker .fp-content-error {height: 100%;width: 100%;display: table;text-align: center;} +.file-picker .fp-content-error .fp-error {height: 100%;width: 100%;display:table-cell;vertical-align: middle;padding: 40px 20px 10px 20px;font-size: 12px;} + +/* + * Lazy loading on fp-content (File Picker only) + */ +.file-picker .fp-nextpage {clear:both;align:center;} +.file-picker .fp-nextpage .fp-nextpage-link {align:center;} +.file-picker .fp-nextpage .fp-nextpage-loading {display:none;} +.file-picker .fp-nextpage.loading .fp-nextpage-link {display:none;} +.file-picker .fp-nextpage.loading .fp-nextpage-loading {display:block;text-align: center;height: 100px;padding-top: 50px;} + +/* + * Select Dialogue (File Picker and File Manager) + */ +.file-picker.fp-select {width:420px;} +.fp-select .fp-select-loading {text-align: center;margin-top: 20px;} +.fp-select .fp-hr {clear: both;height: 1px; background-color: #FFF;border-bottom: 1px solid #BBB;width: auto; margin: 5px 20px;} +.fp-select table {margin: 10px 20px;} +.fp-select-update {float:right;margin-right: 20px;} +.fp-select .fp-file-update {background:url('[[pix:theme|fp/check]]') no-repeat left center;width:17px;height:17px;} +.fp-select .fp-file-cancel {background:url('[[pix:theme|fp/cross]]') no-repeat left center;width:17px;height:17px;margin-left: 25px;} +.fp-select .fp-select-update span {margin-left: 17px;} +.fp-select .fp-thumbnail {float:left;min-width:110px;min-height:110px;line-height: 110px;text-align: center;margin: 10px 20px 0px 20px;background: #FFF;border: 1px solid #DDD;-webkit-box-shadow: inset 0px 0px 10px 0px #CCC;-moz-box-shadow: inset 0px 0px 10px 0px #CCC;box-shadow: inset 0px 0px 10px 0px #CCC;} +.fp-select .fp-thumbnail img {border: 1px solid #DDD;padding:3px;vertical-align: middle;margin: 10px;} +.fp-select .fp-fileinfo {display: inline-block;margin-top: 4px;margin-right: 20px;} +.file-picker.fp-select .fp-fileinfo {max-width:240px;} +.fp-select .fp-fileinfo div {padding: 4px 0px;} + +.file-picker.fp-select .uneditable {display:none;} +.file-picker.fp-select .fp-select-loading {display:none;} +.file-picker .fp-select.loading .fp-select-loading {display:block;} +.file-picker .fp-select.loading form {display:none;} + + /* + * File Manager + */ +.filemanager-loading{display:none} +.jsenabled .filemanager-loading{display:block;margin-top: 100px;} +.filemanager.fm-loading .filemanager-toolbar, +.filemanager.fm-loading .fp-pathbar, +.filemanager.fm-loading .filemanager-container {display:none;} +.filemanager.fm-loaded .filemanager-loading {display:none;} +.filemanager.fm-maxfiles .fp-btn-add {display:none;} +.filemanager.fm-maxfiles .dndupload-message {display:none;} +.filemanager.fm-nofiles .fp-btn-download {display:none;} +.filemanager .fm-empty-container {display:none;} +.filemanager.fm-noitems .filemanager-container .fp-content {display:none;} +.filemanager .filemanager-updating {display:none;text-align:center;} +.filemanager.fm-updating .filemanager-updating {display:block;} +.filemanager.fm-updating .fm-content-wrapper {display:none;} +.filemanager.fm-nomkdir .fp-btn-mkdir {display:none;} + + /* + * File Manager layout + */ +.fp-restrictions{text-align: right;} +.filemanager .fp-navbar {background: #F2F2F2;border-top: 1px solid #BBB;border-left: 1px solid #BBB;border-right: 1px solid #BBB;} +.filemanager-toolbar{padding: 5px 8px;min-height:22px;} +.fp-pathbar {border-top: 1px solid #BBB;padding: 5px 8px 1px;min-height: 20px;} +.filemanager .fp-pathbar.empty {display:none;} +.filepicker-filelist, +.filemanager-container {background: #FFF;clear: both;overflow:auto;border: 1px solid #BBB;min-height: 140px;position: relative;} +/*.filemanager-container ul{margin:0;padding:0;} +.filemanager-container ul li{white-space:nowrap;list-style-type:none;} +.filemanager-container ul li a{padding:0}*/ +.filemanager .fp-content{overflow: auto;max-height: 400px;} +.filemanager-container, .filepicker-filelist {overflow:hidden;} + +/* + * Icon view (File Manager only) + */ +.filemanager .fp-iconview .fp-reficons1 {position:absolute;height:100%;width:100%;top:0;left:0;z-index:1000;} +.filemanager .fp-iconview .fp-reficons2 {position:absolute;height:100%;width:100%;top:0;left:0;z-index:1001;} +.filemanager .fp-iconview .fp-file.fp-hasreferences .fp-reficons1 {background: url('[[pix:theme|fp/link]]') no-repeat;background-position:bottom right;} +.filemanager .fp-iconview .fp-file.fp-isreference .fp-reficons2 {background: url('[[pix:theme|fp/alias]]') no-repeat;background-position:bottom left;} + +/* + * Table view (File Manager only) + */ +.filemanager .yui3-datatable table {border: 0px solid #BBB;width:100%;} +.filemanager .yui3-datatable-header {background: #FFF;border-bottom: 1px solid #CCC;border-left: 0 solid #FFF;color: #555;} +.filemanager .yui3-datatable-columns .yui3-datatable-sorted, .filemanager .yui3-datatable-sortable-column:hover {background-color: #FFF;} +.filemanager .yui3-datatable-odd .yui3-datatable-cell {background-color: #F6F6F6;border-left: 0px solid #F6F6F6;} +.filemanager .yui3-datatable-data .yui3-datatable-odd .yui3-datatable-sorted {background-color: #F6F6F6;} +.filemanager .yui3-datatable-even .yui3-datatable-cell {background-color: #FFF;border-left: 0px solid #FFF;} +.filemanager .yui3-datatable-data .yui3-datatable-even .yui3-datatable-sorted {background-color: #FFF;} + +/* + * Folder Context Menu (File Manager only) + */ +.filemanager .fp-contextmenu {display:none;} +.filemanager .fp-iconview .fp-folder.fp-hascontextmenu .fp-contextmenu {display:block;position:absolute;right:7px;bottom:5px;z-index: 2000;} +.filemanager .fp-treeview .fp-folder.fp-hascontextmenu .fp-contextmenu, +.filemanager .fp-tableview .fp-folder.fp-hascontextmenu .fp-contextmenu {display:inline;} + +/* + * Drag and drop support (File Manager only) + */ +.filemanager.fm-noitems .fm-empty-container {display:block;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border: 2px dashed #BBB;padding-top:85px;text-align:center;z-index: 3000;} +.filepicker-filelist .dndupload-target, +.filemanager-container .dndupload-target {background:#FFF;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border: 2px dashed #fb7979;padding-top:85px;text-align:center;z-index:3000;-webkit-box-shadow: 0px 0px 0px 10px #FFF;-moz-box-shadow: 0px 0px 0px 10px #FFF;box-shadow: 0px 0px 0px 10px #FFF;z-index: 3000;} +.filepicker-filelist.dndupload-over .dndupload-target, +.filemanager-container.dndupload-over .dndupload-target {background:#FFF;position:absolute;top:10px;bottom:10px;left:10px;right:10px;border: 2px dashed #6c8cd3;padding-top:85px;text-align:center;z-index: 3000;} +.dndupload-message {display:none;} +.dndsupported .dndupload-message {display:inline;} +.dndupload-target {display:none;} +.dndsupported .dndupload-ready .dndupload-target {display:block;} +.dndupload-uploadinprogress {display:none;text-align:center;} +.dndupload-uploading .dndupload-uploadinprogress {display:block;} +.dndupload-arrow {background:url('[[pix:theme|fp/dnd_arrow]]') center no-repeat;width:56px;height:47px;position:absolute;margin-left: -28px;/*right:46%;left:46%;*/animation:mymove 5s infinite;-moz-animation:mymove 5s infinite;-webkit-animation:mymove 5s infinite;} +@keyframes mymove {0%{top:10px;} 12%{top:40px;} 30%{top:20px} 65%{top:35px;} 100%{top:9px;}}@-moz-keyframes mymove{0%{top:10px;} 12%{top:40px;} 30%{top:20px} 65%{top:35px;} 100%{top:9px;}}@-webkit-keyframes mymove {0%{top:10px;} 12%{top:40px;} 30%{top:20px} 65%{top:35px;} 100%{top:9px;}} + +/* + * Select Dialogue (File Manager only) + */ +.filemanager.fp-select .fp-select-loading {display:none;} +.filemanager.fp-select.loading .fp-select-loading {display:block;} +.filemanager.fp-select.loading form {display:none;} +/* disable unavailable actions: */ +/*.filemanager.fp-select.fp-zip .fp-license,*/ +.filemanager.fp-select.fp-folder .fp-license, +/*.filemanager.fp-select.fp-zip .fp-author,*/ +.filemanager.fp-select.fp-folder .fp-author, +.filemanager.fp-select.fp-file .fp-file-unzip, +.filemanager.fp-select.fp-folder .fp-file-unzip, +.filemanager.fp-select.fp-file .fp-file-zip, +.filemanager.fp-select.fp-zip .fp-file-zip {display:none;} +.filemanager.fp-select .fp-file-setmain {display:none;} +.filemanager.fp-select.fp-cansetmain .fp-file-setmain {display:inline-block;} +.filemanager .fp-mainfile .fp-filename {font-weight:bold;} +.filemanager.fp-select.fp-folder .fp-file-download {display:none;} /* to be implemented */ +.fm-operation {font-weight: bold;} + +.filemanager.fp-select .fp-original.fp-unknown {display:none;} +.filemanager.fp-select .fp-original .fp-originloading {display:none;} +.filemanager.fp-select .fp-original.fp-loading .fp-originloading {display:inline;} + +.filemanager.fp-select .fp-reflist.fp-unknown {display:none;} +.filemanager.fp-select .fp-reflist .fp-reflistloading {display:none;} +.filemanager.fp-select .fp-refcount {max-width: 265px;} +.filemanager.fp-select .fp-reflist.fp-loading .fp-reflistloading {display:inline;} +.filemanager.fp-select .fp-reflist .fp-value {background: #FFF;border: 1px solid #BBB;padding: 8px 7px;margin: 0px;max-width: 265px;max-height: 75px;overflow:auto;} +.filemanager.fp-select .fp-reflist .fp-value li {padding-bottom: 7px;} + +/* + * Create folder dialogue (File Manager only) + */ +.filemanager .fp-mkdir-dlg {text-align: center;} +.filemanager .fp-mkdir-dlg p {text-align: left;margin: 40px 20px 0px;} +.filemanager .fp-mkdir-dlg input {margin: 0px 20px 20px;} + +/* + * Confirm dialogue for delete (File Manager only) + */ +.filemanager.fp-dlg {text-align: center;} +.filemanager.fp-dlg .fp-dlg-text {padding: 0px 10px;min-width:200px;max-width:340px;max-height:300px;overflow:auto;line-height: 22px;margin: 40px 20px 20px;font-size: 12px;} + +/* + *file picker search dialog + */ +.file-picker div.bd {text-align:left;} \ No newline at end of file diff --git a/user/filesedit.php b/user/filesedit.php index 716456e569b..86eb52ee7ed 100644 --- a/user/filesedit.php +++ b/user/filesedit.php @@ -57,7 +57,7 @@ $PAGE->set_pagetype('user-files'); $data = new stdClass(); $data->returnurl = $returnurl; -$options = array('subdirs'=>1, 'maxbytes'=>$CFG->userquota, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL); +$options = array('subdirs'=>1, 'maxbytes'=>$CFG->userquota, 'maxfiles'=>-1, 'accepted_types'=>'*'); file_prepare_standard_filemanager($data, 'files', $options, $context, 'user', 'private', 0); $mform = new user_filesedit_form(null, array('data'=>$data, 'options'=>$options)); diff --git a/user/renderer.php b/user/renderer.php index 73212d51697..09f30d73895 100644 --- a/user/renderer.php +++ b/user/renderer.php @@ -64,14 +64,13 @@ class core_user_renderer extends plugin_renderer_base { } $result = '
            '; foreach ($dir['subdirs'] as $subdir) { - $image = $this->output->pix_icon("f/folder", $subdir['dirname'], 'moodle', array('class'=>'icon')); + $image = $this->output->pix_icon(file_folder_icon(), $subdir['dirname'], 'moodle', array('class'=>'icon')); $result .= '
          • '.$image.' '.s($subdir['dirname']).'
            '.$this->htmllize_tree($tree, $subdir).'
          • '; } foreach ($dir['files'] as $file) { $url = file_encode_url("$CFG->wwwroot/pluginfile.php", '/'.$tree->context->id.'/user/private'.$file->get_filepath().$file->get_filename(), true); $filename = $file->get_filename(); - $icon = mimeinfo("icon", $filename); - $image = $this->output->pix_icon("f/$icon", $filename, 'moodle', array('class'=>'icon')); + $image = $this->output->pix_icon(file_file_icon($file), $filename, 'moodle', array('class'=>'icon')); $result .= '
          • '.$image.' '.html_writer::link($url, $filename).'
          • '; } $result .= '
          '; diff --git a/version.php b/version.php index 4dad272d864..2732db6eb73 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012051900.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012052100.00; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes