diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 0195d7756a7..d1ea1ddbb37 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -1747,6 +1747,31 @@ class cm_info implements IteratorAggregate { : null; } + /** + * Creates a cm_info object from a database record (also accepts cm_info + * in which case it is just returned unchanged). + * + * @param stdClass|cm_info|null $cm Stdclass or cm_info (or null) + * @param int $userid Optional userid (default to current) + * @return cm_info|null Object as cm_info, or null if input was null + */ + public static function create($cm, $userid = 0) { + // Nulls get passed through. + if (is_null($cm)) { + return null; + } + // If it is already a cm_info object, just return it. + if ($cm instanceof cm_info) { + return $cm; + } + // Otherwise load modinfo. + if (empty($cm->id) || empty($cm->course)) { + throw new coding_exception('$cm must contain ->id and ->course'); + } + $modinfo = get_fast_modinfo($cm->course, $userid); + return $modinfo->get_cm($cm->id); + } + /** * If dynamic data for this course-module is not yet available, gets it. * @@ -2062,6 +2087,165 @@ function get_fast_modinfo($courseorid, $userid = 0, $resetonly = false) { return course_modinfo::instance($courseorid, $userid); } +/** + * Efficiently retrieves the $course (stdclass) and $cm (cm_info) objects, given + * a cmid. If module name is also provided, it will ensure the cm is of that type. + * + * Usage: + * list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'forum'); + * + * Using this method has a performance advantage because it works by loading + * modinfo for the course - which will then be cached and it is needed later + * in most requests. It also guarantees that the $cm object is a cm_info and + * not a stdclass. + * + * The $course object can be supplied if already known and will speed + * up this function - although it is more efficient to use this function to + * get the course if you are starting from a cmid. + * + * To avoid security problems and obscure bugs, you should always specify + * $modulename if the cmid value came from user input. + * + * By default this obtains information (for example, whether user can access + * the activity) for current user, but you can specify a userid if required. + * + * @param stdClass|int $cmorid Id of course-module, or database object + * @param string $modulename Optional modulename (improves security) + * @param stdClass|int $courseorid Optional course object if already loaded + * @param int $userid Optional userid (default = current) + * @return array Array with 2 elements $course and $cm + * @throws moodle_exception If the item doesn't exist or is of wrong module name + */ +function get_course_and_cm_from_cmid($cmorid, $modulename = '', $courseorid = 0, $userid = 0) { + global $DB; + if (is_object($cmorid)) { + $cmid = $cmorid->id; + if (isset($cmorid->course)) { + $courseid = (int)$cmorid->course; + } else { + $courseid = 0; + } + } else { + $cmid = (int)$cmorid; + $courseid = 0; + } + + // Validate module name if supplied. + if ($modulename && !core_component::is_valid_plugin_name('mod', $modulename)) { + throw new coding_exception('Invalid modulename parameter'); + } + + // Get course from last parameter if supplied. + $course = null; + if (is_object($courseorid)) { + $course = $courseorid; + } else if ($courseorid) { + $courseid = (int)$courseorid; + } + + if (!$course) { + if ($courseid) { + // If course ID is known, get it using normal function. + $course = get_course($courseid); + } else { + // Get course record in a single query based on cmid. + $course = $DB->get_record_sql(" + SELECT c.* + FROM {course_modules} cm + JOIN {course} c ON c.id = cm.course + WHERE cm.id = ?", array($cmid), MUST_EXIST); + } + } + + // Get cm from get_fast_modinfo. + $modinfo = get_fast_modinfo($course, $userid); + $cm = $modinfo->get_cm($cmid); + if ($modulename && $cm->modname !== $modulename) { + throw new moodle_exception('invalidcoursemodule', 'error'); + } + return array($course, $cm); +} + +/** + * Efficiently retrieves the $course (stdclass) and $cm (cm_info) objects, given + * an instance id or record and module name. + * + * Usage: + * list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum'); + * + * Using this method has a performance advantage because it works by loading + * modinfo for the course - which will then be cached and it is needed later + * in most requests. It also guarantees that the $cm object is a cm_info and + * not a stdclass. + * + * The $course object can be supplied if already known and will speed + * up this function - although it is more efficient to use this function to + * get the course if you are starting from an instance id. + * + * By default this obtains information (for example, whether user can access + * the activity) for current user, but you can specify a userid if required. + * + * @param stdclass|int $instanceorid Id of module instance, or database object + * @param string $modulename Modulename (required) + * @param stdClass|int $courseorid Optional course object if already loaded + * @param int $userid Optional userid (default = current) + * @return array Array with 2 elements $course and $cm + * @throws moodle_exception If the item doesn't exist or is of wrong module name + */ +function get_course_and_cm_from_instance($instanceorid, $modulename, $courseorid = 0, $userid = 0) { + global $DB; + + // Get data from parameter. + if (is_object($instanceorid)) { + $instanceid = $instanceorid->id; + if (isset($instanceorid->course)) { + $courseid = (int)$instanceorid->course; + } else { + $courseid = 0; + } + } else { + $instanceid = (int)$instanceorid; + $courseid = 0; + } + + // Get course from last parameter if supplied. + $course = null; + if (is_object($courseorid)) { + $course = $courseorid; + } else if ($courseorid) { + $courseid = (int)$courseorid; + } + + // Validate module name if supplied. + if (!core_component::is_valid_plugin_name('mod', $modulename)) { + throw new coding_exception('Invalid modulename parameter'); + } + + if (!$course) { + if ($courseid) { + // If course ID is known, get it using normal function. + $course = get_course($courseid); + } else { + // Get course record in a single query based on instance id. + $pagetable = '{' . $modulename . '}'; + $course = $DB->get_record_sql(" + SELECT c.* + FROM $pagetable instance + JOIN {course} c ON c.id = instance.course + WHERE instance.id = ?", array($instanceid), MUST_EXIST); + } + } + + // Get cm from get_fast_modinfo. + $modinfo = get_fast_modinfo($course, $userid); + $instances = $modinfo->get_instances_of($modulename); + if (!array_key_exists($instanceid, $instances)) { + throw new moodle_exception('invalidmoduleid', 'error', $instanceid); + } + return array($course, $instances[$instanceid]); +} + + /** * Rebuilds or resets the cached list of course activities stored in MUC. * diff --git a/lib/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php index 5839547cf5f..f3e043d2730 100644 --- a/lib/tests/modinfolib_test.php +++ b/lib/tests/modinfolib_test.php @@ -965,4 +965,236 @@ class core_modinfolib_testcase extends advanced_testcase { $this->assertCount(0, $groups); $this->assertArrayNotHasKey($group1->id, $groups); } + + /** + * Tests the function for constructing a cm_info from mixed data. + */ + public function test_create() { + global $CFG, $DB; + $this->resetAfterTest(); + + // Create a course and an activity. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $page = $generator->create_module('page', array('course' => $course->id, + 'name' => 'Annie')); + + // Null is passed through. + $this->assertNull(cm_info::create(null)); + + // Stdclass object turns into cm_info. + $cm = cm_info::create( + (object)array('id' => $page->cmid, 'course' => $course->id)); + $this->assertInstanceOf('cm_info', $cm); + $this->assertEquals('Annie', $cm->name); + + // A cm_info object stays as cm_info. + $this->assertSame($cm, cm_info::create($cm)); + + // Invalid object (missing fields) causes error. + try { + cm_info::create((object)array('id' => $page->cmid)); + $this->fail(); + } catch (Exception $e) { + $this->assertInstanceOf('coding_exception', $e); + } + + // Create a second hidden activity. + $hiddenpage = $generator->create_module('page', array('course' => $course->id, + 'name' => 'Annie', 'visible' => 0)); + + // Create 2 user accounts, one is a manager who can see everything. + $user = $generator->create_user(); + $generator->enrol_user($user->id, $course->id); + $manager = $generator->create_user(); + $generator->enrol_user($manager->id, $course->id, + $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST)); + + // User can see the normal page but not the hidden one. + $cm = cm_info::create((object)array('id' => $page->cmid, 'course' => $course->id), + $user->id); + $this->assertTrue($cm->uservisible); + $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id), + $user->id); + $this->assertFalse($cm->uservisible); + + // Manager can see the hidden one too. + $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id), + $manager->id); + $this->assertTrue($cm->uservisible); + } + + /** + * Tests function for getting $course and $cm at once quickly from modinfo + * based on cmid or cm record. + */ + public function test_get_course_and_cm_from_cmid() { + global $CFG, $DB; + $this->resetAfterTest(); + + // Create a course and an activity. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(array('shortname' => 'Halls')); + $page = $generator->create_module('page', array('course' => $course->id, + 'name' => 'Annie')); + + // Successful usage. + list($course, $cm) = get_course_and_cm_from_cmid($page->cmid); + $this->assertEquals('Halls', $course->shortname); + $this->assertInstanceOf('cm_info', $cm); + $this->assertEquals('Annie', $cm->name); + + // Specified module type. + list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page'); + $this->assertEquals('Annie', $cm->name); + + // With id in object. + $fakecm = (object)array('id' => $page->cmid); + list($course, $cm) = get_course_and_cm_from_cmid($fakecm); + $this->assertEquals('Halls', $course->shortname); + $this->assertEquals('Annie', $cm->name); + + // With both id and course in object. + $fakecm->course = $course->id; + list($course, $cm) = get_course_and_cm_from_cmid($fakecm); + $this->assertEquals('Halls', $course->shortname); + $this->assertEquals('Annie', $cm->name); + + // With supplied course id. + list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course->id); + $this->assertEquals('Annie', $cm->name); + + // With supplied course object (modified just so we can check it is + // indeed reusing the supplied object). + $course->silly = true; + list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course); + $this->assertEquals('Annie', $cm->name); + $this->assertTrue($course->silly); + + // Incorrect module type. + try { + get_course_and_cm_from_cmid($page->cmid, 'forum'); + $this->fail(); + } catch (moodle_exception $e) { + $this->assertEquals('invalidcoursemodule', $e->errorcode); + } + + // Invalid module name. + try { + get_course_and_cm_from_cmid($page->cmid, 'pigs can fly'); + $this->fail(); + } catch (coding_exception $e) { + $this->assertContains('Invalid modulename parameter', $e->getMessage()); + } + + // Doesn't exist. + try { + get_course_and_cm_from_cmid($page->cmid + 1); + $this->fail(); + } catch (moodle_exception $e) { + $this->assertInstanceOf('dml_exception', $e); + } + + // Create a second hidden activity. + $hiddenpage = $generator->create_module('page', array('course' => $course->id, + 'name' => 'Annie', 'visible' => 0)); + + // Create 2 user accounts, one is a manager who can see everything. + $user = $generator->create_user(); + $generator->enrol_user($user->id, $course->id); + $manager = $generator->create_user(); + $generator->enrol_user($manager->id, $course->id, + $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST)); + + // User can see the normal page but not the hidden one. + list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id); + $this->assertTrue($cm->uservisible); + list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id); + $this->assertFalse($cm->uservisible); + + // Manager can see the hidden one too. + list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id); + $this->assertTrue($cm->uservisible); + } + + /** + * Tests function for getting $course and $cm at once quickly from modinfo + * based on instance id or record. + */ + public function test_get_course_and_cm_from_instance() { + global $CFG, $DB; + $this->resetAfterTest(); + + // Create a course and an activity. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(array('shortname' => 'Halls')); + $page = $generator->create_module('page', array('course' => $course->id, + 'name' => 'Annie')); + + // Successful usage. + list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page'); + $this->assertEquals('Halls', $course->shortname); + $this->assertInstanceOf('cm_info', $cm); + $this->assertEquals('Annie', $cm->name); + + // With id in object. + $fakeinstance = (object)array('id' => $page->id); + list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page'); + $this->assertEquals('Halls', $course->shortname); + $this->assertEquals('Annie', $cm->name); + + // With both id and course in object. + $fakeinstance->course = $course->id; + list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page'); + $this->assertEquals('Halls', $course->shortname); + $this->assertEquals('Annie', $cm->name); + + // With supplied course id. + list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course->id); + $this->assertEquals('Annie', $cm->name); + + // With supplied course object (modified just so we can check it is + // indeed reusing the supplied object). + $course->silly = true; + list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course); + $this->assertEquals('Annie', $cm->name); + $this->assertTrue($course->silly); + + // Doesn't exist (or is wrong type). + try { + get_course_and_cm_from_instance($page->id, 'forum'); + $this->fail(); + } catch (moodle_exception $e) { + $this->assertInstanceOf('dml_exception', $e); + } + + // Invalid module name. + try { + get_course_and_cm_from_cmid($page->cmid, '1337 h4x0ring'); + $this->fail(); + } catch (coding_exception $e) { + $this->assertContains('Invalid modulename parameter', $e->getMessage()); + } + + // Create a second hidden activity. + $hiddenpage = $generator->create_module('page', array('course' => $course->id, + 'name' => 'Annie', 'visible' => 0)); + + // Create 2 user accounts, one is a manager who can see everything. + $user = $generator->create_user(); + $generator->enrol_user($user->id, $course->id); + $manager = $generator->create_user(); + $generator->enrol_user($manager->id, $course->id, + $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST)); + + // User can see the normal page but not the hidden one. + list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id); + $this->assertTrue($cm->uservisible); + list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id); + $this->assertFalse($cm->uservisible); + + // Manager can see the hidden one too. + list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id); + $this->assertTrue($cm->uservisible); + } } diff --git a/lib/upgrade.txt b/lib/upgrade.txt index b39242beb24..eee1b489bca 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -7,6 +7,11 @@ information provided here is intended especially for developers. * renderers: We now remove the suffix _renderable when looking for a render method for a renderable. If you have a renderable class named like "blah_renderable" and have a method on a renderer named "render_blah_renderable" you will need to change the name of your render method to "render_blah" instead, as renderable at the end is no longer accepted. +* New functions get_course_and_cm_from_cmid($cmorid, $modulename) and + get_course_and_cm_from_instance($instanceorid, $modulename) can be used to + more efficiently load these basic data objects at the start of a script. +* New function cm_info::create($cm) can be used when you need a cm_info + object, but have a $cm which might only be a standard database record. DEPRECATIONS: * completion_info->get_incomplete_criteria() is deprecated and will be removed in Moodle 3.0.