diff --git a/lib/filebrowser/file_browser.php b/lib/filebrowser/file_browser.php index 8f99d9dfb39..11b68c8e0b5 100644 --- a/lib/filebrowser/file_browser.php +++ b/lib/filebrowser/file_browser.php @@ -60,6 +60,9 @@ require_once("$CFG->libdir/filebrowser/file_info_context_module.php"); */ class file_browser { + /** @var array cached list of enrolled courses. */ + protected $enrolledcourses = null; + /** * Looks up file_info instance * @@ -190,7 +193,7 @@ class file_browser { /** * Returns info about the files at Course category context * - * @param stdClass $context context object + * @param context $context context object * @param string $component component * @param string $filearea file area * @param int $itemid item ID @@ -199,39 +202,36 @@ class file_browser { * @return file_info|null file_info instance or null if not found or access not allowed */ private function get_file_info_context_module($context, $component, $filearea, $itemid, $filepath, $filename) { - global $COURSE, $DB, $CFG; - - static $cachedmodules = array(); - - if (!array_key_exists($context->instanceid, $cachedmodules)) { - $cachedmodules[$context->instanceid] = get_coursemodule_from_id('', $context->instanceid); + if (!($context instanceof context_module)) { + return null; } + $coursecontext = $context->get_course_context(); + $modinfo = get_fast_modinfo($coursecontext->instanceid); + $cm = $modinfo->get_cm($context->instanceid); - if (!($cm = $cachedmodules[$context->instanceid])) { + if (empty($cm->uservisible)) { return null; } - if ($cm->course == $COURSE->id) { - $course = $COURSE; - } else if (!$course = $DB->get_record('course', array('id'=>$cm->course))) { - return null; - } - - $modinfo = get_fast_modinfo($course); - if (empty($modinfo->cms[$cm->id]->uservisible)) { - return null; - } - - $modname = $modinfo->cms[$cm->id]->modname; - - if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) { - return null; - } - - // ok, we know that module exists, and user may access it - - $level = new file_info_context_module($this, $context, $course, $cm, $modname); + $level = new file_info_context_module($this, $context, $cm->get_course(), $cm, $cm->modname); return $level->get_file_info($component, $filearea, $itemid, $filepath, $filename); } + /** + * Check if user is enrolled into the course + * + * This function keeps a cache of enrolled courses because it may be called multiple times for many courses in one request + * + * @param int $courseid + * @return bool + */ + public function is_enrolled($courseid) { + if ($this->enrolledcourses === null || PHPUNIT_TEST) { + // Since get_file_browser() returns a statically cached object we can't rely on cache + // inside the file_browser class in the unittests. + // TODO MDL-59964 remove this caching when it's implemented inside enrol_get_my_courses(). + $this->enrolledcourses = enrol_get_my_courses(['id']); + } + return array_key_exists($courseid, $this->enrolledcourses); + } } diff --git a/lib/filebrowser/file_info_context_course.php b/lib/filebrowser/file_info_context_course.php index 67f842c9f0a..3e9ffa39db8 100644 --- a/lib/filebrowser/file_info_context_course.php +++ b/lib/filebrowser/file_info_context_course.php @@ -36,6 +36,9 @@ class file_info_context_course extends file_info { /** @var stdClass course object */ protected $course; + /** @var file_info_context_module[] cached child modules. See {@link get_child_module()} */ + protected $childrenmodules = []; + /** * Constructor * @@ -68,7 +71,7 @@ class file_info_context_course extends file_info { return null; } - if (!is_viewing($this->context) and !is_enrolled($this->context)) { + if (!is_viewing($this->context) and !$this->browser->is_enrolled($this->course->id)) { // no peaking here if not enrolled or inspector return null; } @@ -86,6 +89,41 @@ class file_info_context_course extends file_info { return null; } + /** + * Returns list of areas inside this course + * + * @param string $extensions Only return areas that have files with these extensions + * @param bool $returnemptyfolders return all areas always, if true it will ignore the previous argument + * @return array + */ + protected function get_course_areas($extensions = '*', $returnemptyfolders = false) { + global $DB; + + $allareas = [ + 'course_summary', + 'course_overviewfiles', + 'course_section', + 'backup_section', + 'backup_course', + 'backup_automated', + 'course_legacy' + ]; + + if ($returnemptyfolders) { + return $allareas; + } + + $params1 = ['contextid' => $this->context->id, 'emptyfilename' => '.']; + $sql1 = "SELECT " . $DB->sql_concat('f.component', "'_'", 'f.filearea') . " + FROM {files} f + WHERE f.filename <> :emptyfilename AND f.contextid = :contextid "; + $sql3 = ' GROUP BY f.component, f.filearea'; + list($sql2, $params2) = $this->build_search_files_sql($extensions); + $areaswithfiles = $DB->get_fieldset_sql($sql1 . $sql2 . $sql3, array_merge($params1, $params2)); + + return array_intersect($allareas, $areaswithfiles); + } + /** * Gets a stored file for the course summary filearea directory * @@ -387,6 +425,28 @@ class file_info_context_course extends file_info { return $this->get_filtered_children('*', false, true); } + /** + * Returns the child module if it is accessible by the current user + * + * @param cm_info|int $cm + * @return file_info_context_module|null + */ + protected function get_child_module($cm) { + $cmid = is_object($cm) ? $cm->id : $cm; + if (!array_key_exists($cmid, $this->childrenmodules)) { + $this->childrenmodules[$cmid] = null; + if (!($cm instanceof cm_info)) { + $cms = get_fast_modinfo($this->course)->cms; + $cm = array_key_exists($cmid, $cms) ? $cms[$cmid] : null; + } + if ($cm && $cm->uservisible) { + $this->childrenmodules[$cmid] = new file_info_context_module($this->browser, + $cm->context, $this->course, $cm, $cm->modname); + } + } + return $this->childrenmodules[$cmid]; + } + /** * Help function to return files matching extensions or their count * @@ -397,46 +457,52 @@ class file_info_context_course extends file_info { * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { - $areas = array( - array('course', 'summary'), - array('course', 'overviewfiles'), - array('course', 'section'), - array('backup', 'section'), - array('backup', 'course'), - array('backup', 'automated'), - array('course', 'legacy') - ); $children = array(); - foreach ($areas as $area) { + + $courseareas = $this->get_course_areas($extensions, $returnemptyfolders); + foreach ($courseareas as $areaname) { + $area = explode('_', $areaname, 2); if ($child = $this->get_file_info($area[0], $area[1], 0, '/', '.')) { - if ($returnemptyfolders || $child->count_non_empty_children($extensions)) { - $children[] = $child; - if (($countonly !== false) && count($children) >= $countonly) { - return $countonly; - } + $children[] = $child; + if (($countonly !== false) && count($children) >= $countonly) { + return $countonly; } } } + $cnt = count($children); if (!has_capability('moodle/course:managefiles', $this->context)) { // 'managefiles' capability is checked in every activity module callback. // Don't even waste time on retrieving the modules if we can't browse the files anyway } else { - // now list all modules - $modinfo = get_fast_modinfo($this->course); - foreach ($modinfo->cms as $cminfo) { - if (empty($cminfo->uservisible)) { - continue; - } - $modcontext = context_module::instance($cminfo->id, IGNORE_MISSING); - if ($child = $this->browser->get_file_info($modcontext)) { - if ($returnemptyfolders || $child->count_non_empty_children($extensions)) { + if ($returnemptyfolders) { + $modinfo = get_fast_modinfo($this->course); + foreach ($modinfo->cms as $cminfo) { + if ($child = $this->get_child_module($cminfo)) { $children[] = $child; - if (($countonly !== false) && count($children) >= $countonly) { - return $countonly; + $cnt++; + } + } + } else if ($moduleareas = $this->get_module_areas_with_files($extensions)) { + // We found files in some of the modules. + // Create array of children modules ordered with the same way as cms in modinfo. + $modulechildren = array_fill_keys(array_keys(get_fast_modinfo($this->course)->get_cms()), null); + foreach ($moduleareas as $area) { + if ($modulechildren[$area->cmid]) { + // We already found non-empty area within the same module, do not analyse other areas. + continue; + } + if ($child = $this->get_child_module($area->cmid)) { + if ($child->get_file_info($area->component, $area->filearea, $area->itemid, null, null)) { + $modulechildren[$area->cmid] = $child; + $cnt++; + if (($countonly !== false) && $cnt >= $countonly) { + return $cnt; + } } } } + $children = array_merge($children, array_values(array_filter($modulechildren))); } } @@ -446,6 +512,52 @@ class file_info_context_course extends file_info { return $children; } + /** + * Returns list of areas inside the course modules that have files with the given extension + * + * @param string $extensions + * @return array + */ + protected function get_module_areas_with_files($extensions = '*') { + global $DB; + + $params1 = ['contextid' => $this->context->id, + 'emptyfilename' => '.', + 'contextlevel' => CONTEXT_MODULE, + 'depth' => $this->context->depth + 1, + 'pathmask' => $this->context->path . '/%']; + $sql1 = "SELECT ctx.id AS contextid, f.component, f.filearea, f.itemid, ctx.instanceid AS cmid, " . + context_helper::get_preload_record_columns_sql('ctx') . " + FROM {files} f + INNER JOIN {context} ctx ON ctx.id = f.contextid + WHERE f.filename <> :emptyfilename + AND ctx.contextlevel = :contextlevel + AND ctx.depth = :depth + AND " . $DB->sql_like('ctx.path', ':pathmask') . " "; + $sql3 = ' GROUP BY ctx.id, f.component, f.filearea, f.itemid, ctx.instanceid, + ctx.path, ctx.depth, ctx.contextlevel + ORDER BY ctx.id, f.component, f.filearea, f.itemid'; + list($sql2, $params2) = $this->build_search_files_sql($extensions); + $areas = []; + if ($rs = $DB->get_recordset_sql($sql1. $sql2 . $sql3, array_merge($params1, $params2))) { + foreach ($rs as $record) { + context_helper::preload_from_record($record); + $areas[] = $record; + } + $rs->close(); + } + + // Sort areas so 'backup' and 'intro' are in the beginning of the list, they are the easiest to check access to. + usort($areas, function($a, $b) { + $aeasy = ($a->filearea === 'intro' && substr($a->component, 0, 4) === 'mod_') || + ($a->filearea === 'activity' && $a->component === 'backup'); + $beasy = ($b->filearea === 'intro' && substr($b->component, 0, 4) === 'mod_') || + ($b->filearea === 'activity' && $b->component === 'backup'); + return $aeasy == $beasy ? 0 : ($aeasy ? -1 : 1); + }); + return $areas; + } + /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. diff --git a/lib/filebrowser/file_info_context_coursecat.php b/lib/filebrowser/file_info_context_coursecat.php index 7c354bde06f..d0b4746f41f 100644 --- a/lib/filebrowser/file_info_context_coursecat.php +++ b/lib/filebrowser/file_info_context_coursecat.php @@ -97,6 +97,10 @@ class file_info_context_coursecat extends file_info { protected function get_area_coursecat_description($itemid, $filepath, $filename) { global $CFG; + if (!$this->category->id) { + // No coursecat description area for "system". + return null; + } if (!$this->category->visible and !has_capability('moodle/category:viewhiddencategories', $this->context)) { return null; } @@ -158,37 +162,92 @@ class file_info_context_coursecat extends file_info { * @return array of file_info instances */ public function get_children() { - global $DB; - $children = array(); if ($child = $this->get_area_coursecat_description(0, '/', '.')) { $children[] = $child; } - $course_cats = $DB->get_records('course_categories', array('parent'=>$this->category->id), 'sortorder', 'id,visible'); - foreach ($course_cats as $category) { + list($coursecats, $hiddencats) = $this->get_categories(); + foreach ($coursecats as $category) { $context = context_coursecat::instance($category->id); - if (!$category->visible and !has_capability('moodle/category:viewhiddencategories', $context)) { - continue; - } - if ($child = $this->browser->get_file_info($context)) { - $children[] = $child; - } + $children[] = new self($this->browser, $context, $category); } - $courses = $DB->get_records('course', array('category'=>$this->category->id), 'sortorder', 'id,visible'); + $courses = $this->get_courses($hiddencats); foreach ($courses as $course) { - $context = context_course::instance($course->id); - if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) { - continue; - } - if ($child = $this->browser->get_file_info($context)) { - $children[] = $child; - } + $children[] = $this->get_child_course($course); } - return $children; + return array_filter($children); + } + + /** + * List of courses in this category and in hidden subcategories + * + * @param array $hiddencats list of categories that are hidden from current user and returned by {@link get_categories()} + * @return array list of courses + */ + protected function get_courses($hiddencats) { + global $DB, $CFG; + require_once($CFG->libdir.'/modinfolib.php'); + + $params = array('category' => $this->category->id, 'contextlevel' => CONTEXT_COURSE); + $sql = 'c.category = :category'; + + foreach ($hiddencats as $category) { + $catcontext = context_coursecat::instance($category->id); + $sql .= ' OR ' . $DB->sql_like('ctx.path', ':path' . $category->id); + $params['path' . $category->id] = $catcontext->path . '/%'; + } + + // Let's retrieve only minimum number of fields from course table - + // what is needed to check access or call get_fast_modinfo(). + $coursefields = array_merge(['id', 'visible'], course_modinfo::$cachedfields); + $fields = 'c.' . join(',c.', $coursefields) . ', ' . + context_helper::get_preload_record_columns_sql('ctx'); + return $DB->get_records_sql('SELECT ' . $fields . ' FROM {course} c + JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel) + WHERE ('.$sql.') ORDER BY c.sortorder', $params); + } + + /** + * Finds accessible and non-accessible direct subcategories + * + * @return array [$coursecats, $hiddencats] - child categories that are visible to the current user and not visible + */ + protected function get_categories() { + global $DB; + $fields = 'c.*, ' . context_helper::get_preload_record_columns_sql('ctx'); + $coursecats = $DB->get_records_sql('SELECT ' . $fields . ' FROM {course_categories} c + LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel) + WHERE c.parent = :parent ORDER BY c.sortorder', + array('parent' => $this->category->id, 'contextlevel' => CONTEXT_COURSECAT)); + + $hiddencats = []; + + foreach ($coursecats as $id => &$category) { + context_helper::preload_from_record($category); + $context = context_coursecat::instance($category->id); + if (!$category->visible && !has_capability('moodle/category:viewhiddencategories', $context)) { + $hiddencats[$id] = $coursecats[$id]; + unset($coursecats[$id]); + } + } + return [$coursecats, $hiddencats]; + } + + /** + * Returns the file info element for a given course or null if course is not accessible + * + * @param stdClass $course may contain context fields for preloading + * @return file_info_context_course|null + */ + protected function get_child_course($course) { + context_helper::preload_from_record($course); + $context = context_course::instance($course->id); + $child = new file_info_context_course($this->browser, $context, $course); + return $child->get_file_info(null, null, null, null, null); } /** @@ -200,53 +259,33 @@ class file_info_context_coursecat extends file_info { * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { - global $DB; $cnt = 0; - if (($child = $this->get_area_coursecat_description(0, '/', '.')) - && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) { - return $cnt; + if ($child = $this->get_area_coursecat_description(0, '/', '.')) { + $cnt += $child->count_non_empty_children($extensions) ? 1 : 0; + if ($cnt >= $limit) { + return $cnt; + } } - $rs = $DB->get_recordset_sql('SELECT ctx.id AS contextid, c.visible - FROM {context} ctx, {course} c - WHERE ctx.instanceid = c.id - AND ctx.contextlevel = :courselevel - AND c.category = :categoryid - ORDER BY c.visible DESC', // retrieve visible courses first - array('categoryid' => $this->category->id, 'courselevel' => CONTEXT_COURSE)); - foreach ($rs as $record) { - $context = context::instance_by_id($record->contextid); - if (!$record->visible and !has_capability('moodle/course:viewhiddencourses', $context)) { - continue; + list($coursecats, $hiddencats) = $this->get_categories(); + foreach ($coursecats as $category) { + $context = context_coursecat::instance($category->id); + $child = new file_info_context_coursecat($this->browser, $context, $category); + $cnt += $child->count_non_empty_children($extensions) ? 1 : 0; + if ($cnt >= $limit) { + return $cnt; } - if (($child = $this->browser->get_file_info($context)) - && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) { - break; - } - } - $rs->close(); - if ($cnt >= $limit) { - return $cnt; } - $rs = $DB->get_recordset_sql('SELECT ctx.id AS contextid, cat.visible - FROM {context} ctx, {course_categories} cat - WHERE ctx.instanceid = cat.id - AND ctx.contextlevel = :catlevel - AND cat.parent = :categoryid - ORDER BY cat.visible DESC', // retrieve visible categories first - array('categoryid' => $this->category->id, 'catlevel' => CONTEXT_COURSECAT)); - foreach ($rs as $record) { - $context = context::instance_by_id($record->contextid); - if (!$record->visible and !has_capability('moodle/category:viewhiddencategories', $context)) { - continue; - } - if (($child = $this->browser->get_file_info($context)) - && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) { - break; + $courses = $this->get_courses($hiddencats); + foreach ($courses as $course) { + if ($child = $this->get_child_course($course)) { + $cnt += $child->count_non_empty_children($extensions) ? 1 : 0; + if ($cnt >= $limit) { + return $cnt; + } } } - $rs->close(); return $cnt; } diff --git a/lib/filebrowser/file_info_context_module.php b/lib/filebrowser/file_info_context_module.php index c62f83adbf5..c2c068ffd1a 100644 --- a/lib/filebrowser/file_info_context_module.php +++ b/lib/filebrowser/file_info_context_module.php @@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die(); class file_info_context_module extends file_info { /** @var stdClass Course object */ protected $course; - /** @var stdClass Course module object */ + /** @var cm_info Course module object */ protected $cm; /** @var string Module name */ protected $modname; @@ -58,23 +58,17 @@ class file_info_context_module extends file_info { parent::__construct($browser, $context); $this->course = $course; - $this->cm = $cm; - $this->modname = $modname; + $this->cm = cm_info::create($cm); + $this->modname = $this->cm->modname; $this->nonemptychildren = null; - include_once("$CFG->dirroot/mod/$modname/lib.php"); - - //find out all supported areas - $functionname = 'mod_'.$modname.'_get_file_areas'; - $functionname_old = $modname.'_get_file_areas'; - - if (function_exists($functionname)) { + if ($functionname = component_callback_exists('mod_'.$modname, 'get_file_areas')) { + $cm = $this->cm->get_course_module_record(); $this->areas = $functionname($course, $cm, $context); - } else if (function_exists($functionname_old)) { - $this->areas = $functionname_old($course, $cm, $context); } else { $this->areas = array(); } + unset($this->areas['intro']); // hardcoded, ignore attempts to override it } @@ -99,14 +93,12 @@ class file_info_context_module extends file_info { return null; } - if (!is_viewing($this->context) and !is_enrolled($this->context)) { + if (!is_viewing($this->context) and !$this->browser->is_enrolled($this->course->id)) { // no peaking here if not enrolled or inspector return null; } - $modinfo = get_fast_modinfo($this->course); - $cminfo = $modinfo->get_cm($this->cm->id); - if (!$cminfo->uservisible) { + if (!$this->cm->uservisible) { // activity hidden sorry return null; } @@ -121,13 +113,10 @@ class file_info_context_module extends file_info { return $this->get_area_backup($itemid, $filepath, $filename); } - $functionname = 'mod_'.$this->modname.'_get_file_info'; - $functionname_old = $this->modname.'_get_file_info'; - - if (function_exists($functionname)) { - return $functionname($this->browser, $this->areas, $this->course, $this->cm, $this->context, $filearea, $itemid, $filepath, $filename); - } else if (function_exists($functionname_old)) { - return $functionname_old($this->browser, $this->areas, $this->course, $this->cm, $this->context, $filearea, $itemid, $filepath, $filename); + if ($functionname = component_callback_exists('mod_'.$this->modname, 'get_file_info')) { + $cm = $this->cm->get_course_module_record(); + return $functionname($this->browser, $this->areas, $this->course, $cm, + $this->context, $filearea, $itemid, $filepath, $filename); } return null; @@ -206,7 +195,7 @@ class file_info_context_module extends file_info { * @return string */ public function get_visible_name() { - return $this->cm->name.' ('.get_string('modulename', $this->cm->modname).')'; + return $this->cm->get_formatted_name().' ('.$this->cm->get_module_type_name().')'; } /** diff --git a/lib/filebrowser/file_info_context_system.php b/lib/filebrowser/file_info_context_system.php index 27144c05a66..a7c4f2808bd 100644 --- a/lib/filebrowser/file_info_context_system.php +++ b/lib/filebrowser/file_info_context_system.php @@ -25,6 +25,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir.'/filebrowser/file_info_context_coursecat.php'); + /** * Represents the system context in the tree navigated by {@link file_browser}. * @@ -32,7 +34,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2008 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class file_info_context_system extends file_info { +class file_info_context_system extends file_info_context_coursecat { /** * Constructor @@ -41,7 +43,7 @@ class file_info_context_system extends file_info { * @param stdClass $context context object */ public function __construct($browser, $context) { - parent::__construct($browser, $context); + parent::__construct($browser, $context, (object)['id' => 0, 'parent' => 0, 'visible' => 1]); } /** @@ -139,71 +141,6 @@ class file_info_context_system extends file_info { return true; } - /** - * Returns list of children. - * - * @return array of file_info instances - */ - public function get_children() { - global $DB; - - $children = array(); - - // Add course categories on the top level that are either visible or user is able to view hidden categories. - $course_cats = $DB->get_records('course_categories', array('parent'=>0), 'sortorder', 'id,visible'); - foreach ($course_cats as $category) { - $context = context_coursecat::instance($category->id); - if (!$category->visible and !has_capability('moodle/category:viewhiddencategories', $context)) { - continue; - } - if ($child = $this->browser->get_file_info($context)) { - $children[] = $child; - } - } - - // Add courses where user is enrolled that are located in hidden course categories because they would not - // be present in the above tree but user may still be able to access files in them. - if ($hiddencontexts = $this->get_inaccessible_coursecat_contexts()) { - $courses = enrol_get_my_courses(); - foreach ($courses as $course) { - $context = context_course::instance($course->id); - $parents = $context->get_parent_context_ids(); - if (array_intersect($hiddencontexts, $parents)) { - // This course has hidden parent category. - if ($child = $this->browser->get_file_info($context)) { - $children[] = $child; - } - } - } - } - - return $children; - } - - /** - * Returns list of course categories contexts that current user can not see - * - * @return array array of course categories contexts ids - */ - protected function get_inaccessible_coursecat_contexts() { - global $DB; - - $sql = context_helper::get_preload_record_columns_sql('ctx'); - $records = $DB->get_records_sql("SELECT ctx.id, $sql - FROM {course_categories} c - JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = ? - WHERE c.visible = ?", [CONTEXT_COURSECAT, 0]); - $hiddencontexts = []; - foreach ($records as $record) { - context_helper::preload_from_record($record); - $context = context::instance_by_id($record->id); - if (!has_capability('moodle/category:viewhiddencategories', $context)) { - $hiddencontexts[] = $record->id; - } - } - return $hiddencontexts; - } - /** * Returns parent file_info instance * diff --git a/lib/filebrowser/tests/file_browser_test.php b/lib/filebrowser/tests/file_browser_test.php new file mode 100644 index 00000000000..3ce057026f3 --- /dev/null +++ b/lib/filebrowser/tests/file_browser_test.php @@ -0,0 +1,244 @@ +. + +/** + * Unit tests for file browser + * + * @package core_files + * @copyright 2017 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Unit tests for file browser + * + * @package core_files + * @copyright 2017 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class file_browser_testcase extends advanced_testcase { + + /** @var stdClass */ + protected $course1; + /** @var stdClass */ + protected $course2; + /** @var stdClass */ + protected $module1; + /** @var stdClass */ + protected $module2; + /** @var stdClass */ + protected $course1filerecord; + /** @var stdClass */ + protected $teacher; + /** @var stdClass */ + protected $teacherrole; + + /** + * Set up + */ + public function setUp() { + global $DB; + $this->resetAfterTest(); + + $this->setAdminUser(); + + $this->getDataGenerator()->create_category(); // Empty category. + $this->course1 = $this->getDataGenerator()->create_course(); // Empty course. + + $this->course2 = $this->getDataGenerator()->create_course(); + + // Add a file to course1 summary. + $coursecontext1 = context_course::instance($this->course1->id); + $this->course1filerecord = array('contextid' => $coursecontext1->id, + 'component' => 'course', + 'filearea' => 'summary', + 'itemid' => '0', + 'filepath' => '/', + 'filename' => 'summaryfile.jpg'); + $fs = get_file_storage(); + $fs->create_file_from_string($this->course1filerecord, 'IMG'); + + $this->module1 = $this->getDataGenerator()->create_module('resource', ['course' => $this->course2->id]); // Contains 1 file. + $this->module2 = $this->getDataGenerator()->create_module('assign', ['course' => $this->course2->id]); // Contains no files. + + $this->teacher = $this->getDataGenerator()->create_user(); + $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course1->id, $this->teacherrole->id); + $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course2->id, $this->teacherrole->id); + + $this->setUser($this->teacher); + } + + /** + * Test "Server files" from the system context + */ + public function test_file_info_context_system() { + + // There is one non-empty category child and two category children. + + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info(context_system::instance()); + $this->assertNotEmpty($fileinfo->count_non_empty_children()); + $this->assertEquals(1, count($fileinfo->get_non_empty_children())); + $categorychildren = array_filter($fileinfo->get_children(), function($a) { + return $a instanceof file_info_context_coursecat; + }); + $this->assertEquals(2, count($categorychildren)); + } + + /** + * Test "Server files" from the system context, hide Misc category + */ + public function test_file_info_context_system_hidden() { + + // Hide the course category that contains our two courses. Teacher does not have cap to view hidden categories. + coursecat::get($this->course1->category)->update(['visible' => 0]); + + // We should have two non-empty children in system context (courses). + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info(context_system::instance()); + $this->assertNotEmpty($fileinfo->count_non_empty_children()); + $this->assertEquals(2, count($fileinfo->get_non_empty_children())); + + // Should be 1 category children (empty category). + $categorychildren = array_filter($fileinfo->get_children(), function($a) { + return $a instanceof file_info_context_coursecat; + }); + $this->assertEquals(1, count($categorychildren)); + + // Should be 2 course children - courses that belonged to hidden subcategory are now direct children of "System". + $coursechildren = array_filter($fileinfo->get_children(), function($a) { + return $a instanceof file_info_context_course; + }); + $this->assertEquals(2, count($coursechildren)); + } + + /** + * Test "Server files" from the course category context + */ + public function test_file_info_context_coursecat() { + + // There are two non-empty courses. + + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info(context_coursecat::instance($this->course2->category)); + $this->assertNotEmpty($fileinfo->count_non_empty_children()); + $this->assertEquals(2, count($fileinfo->get_non_empty_children())); + $coursechildren = array_filter($fileinfo->get_children(), function($a) { + return $a instanceof file_info_context_course; + }); + $this->assertEquals(2, count($coursechildren)); + } + + /** + * Test "Server files" from the course category context, only look for .jpg + */ + public function test_file_info_context_coursecat_jpg() { + + // There is one non-empty category child and two category children. + + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info(context_system::instance()); + $this->assertNotEmpty($fileinfo->count_non_empty_children(['.jpg'])); + $this->assertEquals(1, count($fileinfo->get_non_empty_children(['.jpg']))); + } + + /** + * Test "Server files" from the course context (course1) + */ + public function test_file_info_context_course_1() { + + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info(context_course::instance($this->course1->id)); + // Fileinfo element has only one non-empty child - "Course summary" file area. + $this->assertNotEmpty($fileinfo->count_non_empty_children()); + $nonemptychildren = $fileinfo->get_non_empty_children(); + $this->assertEquals(1, count($nonemptychildren)); + $child = reset($nonemptychildren); + $this->assertTrue($child instanceof file_info_stored); + $this->assertEquals(['filename' => '.'] + $this->course1filerecord, $child->get_params()); + // Filearea "Course summary" has a child that is the actual image file. + $this->assertEquals($this->course1filerecord, $child->get_children()[0]->get_params()); + + // There are six course-level file areas and no modules in this course. + $allchildren = $fileinfo->get_children(); + $this->assertEquals(6, count($allchildren)); + $modulechildren = array_filter($allchildren, function($a) { + return $a instanceof file_info_context_module; + }); + $this->assertEquals(0, count($modulechildren)); + + // Admin can see seven course-level file areas. + $this->setAdminUser(); + $fileinfo = $browser->get_file_info(context_course::instance($this->course1->id)); + $this->assertEquals(7, count($fileinfo->get_children())); + } + + /** + * Test "Server files" from the course context (course1) + */ + public function test_file_info_context_course_2() { + + // 2. Start from the course level. + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info(context_course::instance($this->course2->id)); + $this->assertNotEmpty($fileinfo->count_non_empty_children()); + $nonemptychildren = $fileinfo->get_non_empty_children(); + $this->assertEquals(1, count($nonemptychildren)); + $child = reset($nonemptychildren); + $this->assertTrue($child instanceof file_info_context_module); + $this->assertEquals($this->module1->name.' (File)', $child->get_visible_name()); + $this->assertEquals(1, count($child->get_non_empty_children())); + $this->assertEquals(1, $child->count_non_empty_children()); + $modulechildren = array_filter($fileinfo->get_children(), function($a) { + return $a instanceof file_info_context_module; + }); + $this->assertEquals(2, count($modulechildren)); + } + + /** + * Test "Server files" from the course context (module1) + */ + public function test_file_info_context_module_1() { + + $module1context = context_module::instance($this->module1->cmid); + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info($module1context); + $this->assertEquals($this->module1->name . ' (File)', $fileinfo->get_visible_name()); + $this->assertNotEmpty($fileinfo->count_non_empty_children()); + $nonemptychildren = $fileinfo->get_non_empty_children(); + $this->assertEquals(1, count($nonemptychildren)); + $child = reset($nonemptychildren); + $this->assertTrue($child instanceof file_info_stored); + } + + /** + * Test "Server files" from the course context (module1) + */ + public function test_file_info_context_module_2() { + + $module2context = context_module::instance($this->module2->cmid); + $browser = get_file_browser(); + $fileinfo = $browser->get_file_info($module2context); + $this->assertEquals($this->module2->name.' (Assignment)', $fileinfo->get_visible_name()); + $this->assertEmpty($fileinfo->count_non_empty_children()); + $nonemptychildren = $fileinfo->get_non_empty_children(); + $this->assertEquals(0, count($nonemptychildren)); + + } +} diff --git a/repository/local/lib.php b/repository/local/lib.php index 6c8be642ba4..ec6e19242c6 100644 --- a/repository/local/lib.php +++ b/repository/local/lib.php @@ -166,17 +166,17 @@ class repository_local extends repository { // do not skip files return false; } - if ($fileinfo instanceof file_info_context_coursecat) { + if ($fileinfo instanceof file_info_context_course || + $fileinfo instanceof file_info_context_user || + $fileinfo instanceof file_info_area_course_legacy || + $fileinfo instanceof file_info_context_module || + $fileinfo instanceof file_info_context_system) { + // These instances can never be filearea inside an activity, they will never be skipped. + return false; + } else if ($fileinfo instanceof file_info_context_coursecat) { // This is a course category. For non-admins we do not display categories return empty($CFG->navshowmycoursecategories) && !has_capability('moodle/course:update', context_system::instance()); - } else if ($fileinfo instanceof file_info_context_course || - $fileinfo instanceof file_info_context_user || - $fileinfo instanceof file_info_area_course_legacy || - $fileinfo instanceof file_info_context_module || - $fileinfo instanceof file_info_context_system) { - // these instances can never be filearea inside an activity, they will never be skipped - return false; } else { $params = $fileinfo->get_params(); if (strlen($params['filearea']) &&