* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically.';
+$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
$string['allavailablesteps'] = 'All the available steps definitions';
+$string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
+$string['errorcomposer'] = 'Composer dependencies are not installed.';
+$string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
+$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
+$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.';
$string['giveninfo'] = 'Given. Processes to set up the environment';
$string['infoheading'] = 'Info';
$string['installinfo'] = 'Read {$a} for installation and tests execution info';
-$string['moreinfoin'] = 'More info in {$a}';
$string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
$string['newtestsinfo'] = 'Read {$a} for info about how to write new tests';
$string['nostepsdefinitions'] = 'There aren\'t steps definitions matching this filters';
$string['pluginname'] = 'Acceptance testing';
-$string['runclitool'] = 'To list the steps definitions you need to run the Behat CLI tool to create the $CFG->behat_dataroot directory. Go to your moodle dirroot and run "{$a}"';
$string['stepsdefinitionscomponent'] = 'Area';
$string['stepsdefinitionscontains'] = 'Contains';
$string['stepsdefinitionsfilters'] = 'Steps definitions';
$string['unknownexceptioninfo'] = 'There was a problem with Selenium or your browser. Please ensure you are using the latest version of Selenium. Error:';
$string['viewsteps'] = 'Filter';
$string['wheninfo'] = 'When. Actions that provokes an event';
-$string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:<ul>
-<li>You ran "php admin/tool/behat/cli/init.php" from your moodle root directory</li>
-<li>vendor/bin/behat file has execution permissions</li></ul>';
+$string['wrongbehatsetup'] = 'Something is wrong with the behat setup and so step definitions cannot be listed: <b>{$a->errormsg}</b><br/><br/>Please check:<ul>
+<li>$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot are set in config.php with different values from $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.</li>
+<li>You ran "{$a->behatinit}" from your Moodle root directory.</li>
+<li>Dependencies are installed in vendor/ and {$a->behatcommand} file has execution permissions.</li></ul>';
*/
public function render_stepsdefinitions($stepsdefinitions, $form) {
- $title = get_string('pluginname', 'tool_behat');
-
- // Header.
- $html = $this->output->header();
- $html .= $this->output->heading($title);
-
- // Info.
- $installurl = behat_command::DOCS_URL . '#Installation';
- $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
- $writetestsurl = behat_command::DOCS_URL . '#Writting_features';
- $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
- $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
- $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
- $infos = array(
- get_string('installinfo', 'tool_behat', $installlink),
- get_string('newtestsinfo', 'tool_behat', $writetestslink),
- get_string('newstepsinfo', 'tool_behat', $writestepslink)
- );
-
- // List of steps.
- $html .= $this->output->box_start();
- $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
- $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
- $html .= html_writer::empty_tag('div');
- $html .= html_writer::empty_tag('ul');
- $html .= html_writer::empty_tag('li');
- $html .= implode(html_writer::end_tag('li') . html_writer::empty_tag('li'), $infos);
- $html .= html_writer::end_tag('li');
- $html .= html_writer::end_tag('ul');
- $html .= html_writer::end_tag('div');
- $html .= $this->output->box_end();
+ $html = $this->generic_info();
// Form.
ob_start();
return $html;
}
+
+ /**
+ * Renders an error message adding the generic info about the tool purpose and setup.
+ *
+ * @param string $msg The error message
+ * @return string HTML
+ */
+ public function render_error($msg) {
+
+ $html = $this->generic_info();
+
+ $a = new stdClass();
+ $a->errormsg = $msg;
+ $a->behatcommand = behat_command::get_behat_command();
+ $a->behatinit = 'php admin' . DIRECTORY_SEPARATOR . 'tool' . DIRECTORY_SEPARATOR .
+ 'behat' . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'init.php';
+
+ $msg = get_string('wrongbehatsetup', 'tool_behat', $a);
+
+ // Error box including generic error string + specific error msg.
+ $html .= $this->output->box_start('box errorbox');
+ $html .= html_writer::tag('div', $msg);
+ $html .= $this->output->box_end();
+
+ $html .= $this->output->footer();
+
+ return $html;
+ }
+
+ /**
+ * Generic info about the tool.
+ *
+ * @return string
+ */
+ protected function generic_info() {
+
+ $title = get_string('pluginname', 'tool_behat');
+
+ // Header.
+ $html = $this->output->header();
+ $html .= $this->output->heading($title);
+
+ // Info.
+ $installurl = behat_command::DOCS_URL . '#Installation';
+ $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
+ $writetestsurl = behat_command::DOCS_URL . '#Writting_features';
+ $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
+ $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
+ $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
+ $infos = array(
+ get_string('installinfo', 'tool_behat', $installlink),
+ get_string('newtestsinfo', 'tool_behat', $writetestslink),
+ get_string('newstepsinfo', 'tool_behat', $writestepslink)
+ );
+
+ // List of steps.
+ $html .= $this->output->box_start();
+ $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
+ $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
+ $html .= html_writer::empty_tag('div');
+ $html .= html_writer::empty_tag('ul');
+ $html .= html_writer::empty_tag('li');
+ $html .= implode(html_writer::end_tag('li') . html_writer::empty_tag('li'), $infos);
+ $html .= html_writer::end_tag('li');
+ $html .= html_writer::end_tag('ul');
+ $html .= html_writer::end_tag('div');
+ $html .= $this->output->box_end();
+
+ return $html;
+ }
+
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2013110501;
+$plugin->version = 2014012900;
$plugin->requires = 2013110500; // Requires Moodle 2.5.
$plugin->component = 'tool_behat';
//TODO: assign capability
//display successful notification
- $notification = $OUTPUT->notification(get_string('usersettingssaved', 'webservice'), 'success');
+ $notification = $OUTPUT->notification(get_string('usersettingssaved', 'webservice'), 'notifysuccess');
}
echo $OUTPUT->header();
$sink->close();
// Check that the event is valid.
- $this->assertCount(2, $events);
- $event = $events[0];
- $this->assertInstanceOf('\core\event\user_updated', $event);
- $event = $events[1];
+ $this->assertCount(1, $events);
+ $event = reset($events);
$this->assertInstanceOf('\core\event\user_loggedin', $event);
$this->assertEquals('user', $event->objecttable);
$this->assertEquals('2', $event->objectid);
$criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
$criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
$criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id));
- $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->id => $this->module->id));
+ $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid));
// Set completion for forum activity.
$c = new completion_info($this->course);
$this->assertEquals($url, $event->get_url());
// Comments when block is on module (wiki) page.
- $context = context_module::instance($this->wiki->id);
+ $context = context_module::instance($this->wiki->cmid);
$args = new stdClass;
$args->context = $context;
$args->course = $this->course;
// Checking that the event contains the expected values.
$this->assertInstanceOf('\block_comments\event\comment_created', $event);
$this->assertEquals($context, $event->get_context());
- $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->id));
+ $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEquals($url, $event->get_url());
// Comments when block is on module (wiki) page.
- $context = context_module::instance($this->wiki->id);
+ $context = context_module::instance($this->wiki->cmid);
$args = new stdClass;
$args->context = $context;
$args->course = $this->course;
// Checking that the event contains the expected values.
$this->assertInstanceOf('\block_comments\event\comment_deleted', $event);
$this->assertEquals($context, $event->get_context());
- $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->id));
+ $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
}
$this->assertEventLegacyLogData($arr, $event);
$this->assertEquals("blog_entry_added", $event->get_legacy_eventname());
$this->assertEventLegacyData($blog, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEventLegacyData($blog, $event);
$arr = array (SITEID, 'blog', 'update', 'index.php?userid=' . $this->userid . '&entryid=' . $blog->id, $blog->subject);
$this->assertEventLegacyLogData($arr, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$blog->id);
$this->assertEventLegacyLogData($arr, $event);
$this->assertEventLegacyData($blog, $event);
+ $this->assertEventContextNotUsed($event);
}
$arr = array(SITEID, 'blog', 'add association', 'index.php?userid=' . $this->userid . '&entryid=' . $blog->id,
$blog->subject, $this->cmid, $this->userid);
$this->assertEventLegacyLogData($arr, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEquals($url, $event->get_url());
$arr = array(SITEID, 'blog', 'view', $url2->out(), 'view blog entry');
$this->assertEventLegacyLogData($arr, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEquals($this->postid, $event->other['itemid']);
$url = new moodle_url('/blog/index.php', array('entryid' => $this->postid));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEquals($this->postid, $event->other['itemid']);
$url = new moodle_url('/blog/index.php', array('entryid' => $this->postid));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
}
* Purges the session cache of all data belonging to the current user.
*/
public function purge_current_user() {
- $keys = $this->get_store()->find_all($this->get_key_prefix());
+ $keys = $this->get_store()->find_by_prefix($this->get_key_prefix());
$this->get_store()->delete_many($keys);
}
$this->assertEquals($cohort->contextid, $event->contextid);
$this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
$this->assertEventLegacyData($cohort, $event);
+ $this->assertEventContextNotUsed($event);
}
public function test_cohort_update_cohort() {
$this->assertEquals($updatedcohort->contextid, $event->contextid);
$this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
$this->assertEventLegacyData($cohort, $event);
+ $this->assertEventContextNotUsed($event);
}
public function test_cohort_delete_cohort() {
$this->assertEquals($cohort->id, $event->objectid);
$this->assertEquals($cohort, $event->get_record_snapshot('cohort', $cohort->id));
$this->assertEventLegacyData($cohort, $event);
+ $this->assertEventContextNotUsed($event);
}
public function test_cohort_delete_category() {
$this->assertEquals($user->id, $event->relateduserid);
$this->assertEquals($USER->id, $event->userid);
$this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+ $this->assertEventContextNotUsed($event);
}
public function test_cohort_remove_member() {
$this->assertEquals($user->id, $event->relateduserid);
$this->assertEquals($USER->id, $event->userid);
$this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+ $this->assertEventContextNotUsed($event);
}
public function test_cohort_is_member() {
// will be sent to supportemail.
// $CFG->supportuserid = -20;
//
+// Moodle 2.7 introduces a locking api for critical tasks (e.g. cron).
+// The default locking system to use is DB locking for MySQL and Postgres, and File
+// locking for Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
+// will always be DB locking. It can be manually set to one of the lock
+// factory classes listed below, or one of your own custom classes implementing the
+// \core\lock\lock_factory interface.
+//
+// $CFG->lock_factory = "auto";
+//
+// The list of available lock factories is:
+//
+// "\\core\\lock\\file_lock_factory" - File locking
+// Uses lock files stored by default in the dataroot. Whether this
+// works on clusters depends on the file system used for the dataroot.
+//
+// "\\core\\lock\\db_row_lock_factory" - DB locking based on table rows.
+//
+// "\\core\\lock\\postgres_lock_factory" - DB locking based on postgres advisory locks.
+//
+// "\\core\\lock\\mysql_lock_factory" - DB locking based on mysql lock functions.
+//
+// Settings used by the lock factories
+//
+// Location for lock files used by the File locking factory. This must exist
+// on a shared file system that supports locking.
+// $CFG->lock_file_root = $CFG->dataroot . '/lock';
+//
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================
* is available (or to explain why it is not available)
*/
add_status_div: function() {
- var coursecontents = document.getElementById(this.pagecontentid);
+ var Y = this.Y,
+ coursecontents = Y.one('#' + this.pagecontentid),
+ div,
+ handlefile = (this.handlers.filehandlers.length > 0),
+ handletext = false,
+ handlelink = false,
+ i = 0,
+ styletop,
+ styletopunit;
+
if (!coursecontents) {
return;
}
- var div = document.createElement('div');
- div.id = 'dndupload-status';
- div.style.opacity = 0.0;
- coursecontents.insertBefore(div, coursecontents.firstChild);
+ div = Y.Node.create('<div id="dndupload-status"></div>').setStyle('opacity', '0.0');
+ coursecontents.insert(div, 0);
- var Y = this.Y;
- div = Y.one(div);
- var handlefile = (this.handlers.filehandlers.length > 0);
- var handletext = false;
- var handlelink = false;
- var i;
- for (i=0; i<this.handlers.types.length; i++) {
+ for (i = 0; i < this.handlers.types.length; i++) {
switch (this.handlers.types[i].identifier) {
- case 'text':
- case 'text/html':
- handletext = true;
- break;
- case 'url':
- handlelink = true;
- break;
+ case 'text':
+ case 'text/html':
+ handletext = true;
+ break;
+ case 'url':
+ handlelink = true;
+ break;
}
}
$msgident = 'dndworking';
}
div.setContent(M.util.get_string($msgident, 'moodle'));
+ styletop = div.getStyle('top') || '0px';
+ styletopunit = styletop.replace(/^\d+/, '');
+ styletop = parseInt(styletop.replace(/\D*$/, ''), 10);
+
var fadeanim = new Y.Anim({
node: '#dndupload-status',
from: {
opacity: 0.0,
- top: '-30px'
+ top: (styletop - 30).toString() + styletopunit
},
to: {
opacity: 1.0,
- top: '0px'
+ top: styletop.toString() + styletopunit
},
duration: 0.5
});
$options[$grouping->id] = format_string($grouping->name);
}
}
+ core_collator::asort($options);
$gr_el =& $mform->getElement('defaultgroupingid');
$gr_el->load($options);
}
// Grouping conditions - only if grouping is enabled at site level
if (!empty($CFG->enablegroupmembersonly)) {
$options = array();
- $options[0] = get_string('none');
if ($groupings = $DB->get_records('groupings', array('courseid' => $course->id))) {
foreach ($groupings as $grouping) {
$options[$grouping->id] = format_string(
$grouping->name, true, array('context' => $context));
}
}
+ core_collator::asort($options);
+ $options = array(0 => get_string('none')) + $options;
$mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options);
$mform->addHelpButton('groupingid', 'groupingsection', 'group');
}
if ($this->_features->groupings or $this->_features->groupmembersonly) {
//groupings selector - used for normal grouping mode or also when restricting access with groupmembersonly
$options = array();
- $options[0] = get_string('none');
if ($groupings = $DB->get_records('groupings', array('courseid'=>$COURSE->id))) {
foreach ($groupings as $grouping) {
$options[$grouping->id] = format_string($grouping->name);
}
}
+ core_collator::asort($options);
+ $options = array(0 => get_string('none')) + $options;
$mform->addElement('select', 'groupingid', get_string('grouping', 'group'), $options);
$mform->addHelpButton('groupingid', 'grouping', 'group');
}
defined('MOODLE_INTERNAL') || die();
global $CFG;
-require_once($CFG->dirroot.'/course/lib.php');
-require_once($CFG->dirroot.'/course/tests/fixtures/course_capability_assignment.php');
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php');
+require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php');
class core_course_courselib_testcase extends advanced_testcase {
// Catch the events.
$sink = $this->redirectEvents();
- // Create the course.
- $course = $this->getDataGenerator()->create_course();
+ // Create the course with an id number which is used later when generating a course via the imsenterprise plugin.
+ $data = new stdClass();
+ $data->idnumber = 'idnumber';
+ $course = $this->getDataGenerator()->create_course($data);
// Get course from DB for comparison.
$course = $DB->get_record('course', array('id' => $course->id));
$this->assertEventLegacyData($course, $event);
$expectedlog = array(SITEID, 'course', 'new', 'view.php?id=' . $course->id, $course->fullname . ' (ID ' . $course->id . ')');
$this->assertEventLegacyLogData($expectedlog, $event);
+
+ // Now we want to trigger creating a course via the imsenterprise.
+ // Delete the course we created earlier, as we want the imsenterprise plugin to create this.
+ // We do not want print out any of the text this function generates while doing this, which is why
+ // we are using ob_start() and ob_end_clean().
+ ob_start();
+ delete_course($course);
+ ob_end_clean();
+
+ // Create the XML file we want to use.
+ $imstestcase = new enrol_imsenterprise_testcase();
+ $imstestcase->imsplugin = enrol_get_plugin('imsenterprise');
+ $imstestcase->set_test_config();
+ $imstestcase->set_xml_file(false, array($course));
+
+ // Capture the event.
+ $sink = $this->redirectEvents();
+ $imstestcase->imsplugin->cron();
+ $events = $sink->get_events();
+ $sink->close();
+ $event = $events[0];
+
+ // Validate the event triggered is \core\event\course_created. There is no need to validate the other values
+ // as they have already been validated in the previous steps. Here we only want to make sure that when the
+ // imsenterprise plugin creates a course an event is triggered.
+ $this->assertInstanceOf('\core\event\course_created', $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEventLegacyData($movedcoursehidden, $event);
$expectedlog = array($movedcoursehidden->id, 'course', 'move', 'edit.php?id=' . $movedcoursehidden->id, $movedcoursehidden->id);
$this->assertEventLegacyLogData($expectedlog, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEventLegacyData($course, $event);
$expectedlog = array(SITEID, 'course', 'delete', 'view.php?id=' . $course->id, $course->fullname . '(ID ' . $course->id . ')');
$this->assertEventLegacyLogData($expectedlog, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$course->context = $coursecontext;
$course->options = array();
$this->assertEventLegacyData($course, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEventLegacyData($category2, $event);
$expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category2->name . '(ID ' . $category2->id . ')');
$this->assertEventLegacyLogData($expectedlog, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
'samesite' => $rc->is_samesite()
);
$this->assertEventLegacyData($legacydata, $event);
+ $this->assertEventContextNotUsed($event);
// Destroy the resource controller since we are done using it.
$rc->destroy();
$sectionnum = $section->section;
$expectedlegacydata = array($course->id, "course", "editsection", 'editsection.php?id=' . $id, $sectionnum);
$this->assertEventLegacyLogData($expectedlegacydata, $event);
+ $this->assertEventContextNotUsed($event);
}
public function test_course_integrity_check() {
$arr = array($cm->course, "course", "add mod", "../mod/assign/view.php?id=$mod->id", "assign $cm->instance");
$this->assertEventLegacyLogData($arr, $event);
+ $this->assertEventContextNotUsed($event);
}
$arr = array($cm->course, "course", "update mod", "../mod/forum/view.php?id=$mod->id", "forum $cm->instance");
$this->assertEventLegacyLogData($arr, $event);
-
+ $this->assertEventContextNotUsed($event);
}
/**
} else {
$course->category = $this->get_default_category_id();
}
- $course->timecreated = time();
$course->startdate = time();
// Choose a sort order that puts us at the start of the list!
$course->sortorder = 0;
- $courseid = $DB->insert_record('course', $course);
- // Setup default enrolment plugins.
- $course->id = $courseid;
- enrol_course_updated(true, $course, null);
-
- // Setup the blocks.
- $course = $DB->get_record('course', array('id' => $courseid));
- blocks_add_default_course_blocks($course);
-
- // Create default 0-section.
- course_create_sections_if_missing($course, 0);
-
- add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
+ $course = create_course($course);
$this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
}
/**
* @var $imsplugin enrol_imsenterprise_plugin IMS plugin instance.
*/
- protected $imsplugin;
+ public $imsplugin;
/**
* Setup required for all tests.
/**
* Sets the plugin configuration for testing
*/
- protected function set_test_config() {
+ public function set_test_config() {
$this->imsplugin->set_config('mailadmins', false);
$this->imsplugin->set_config('prev_path', '');
$this->imsplugin->set_config('createnewusers', true);
* @param bool|array $users false or array of users StdClass
* @param bool|array $courses false or of courses StdClass
*/
- protected function set_xml_file($users = false, $courses = false) {
+ public function set_xml_file($users = false, $courses = false) {
$xmlcontent = '<enterprise>';
$timeend = $timestart + $extendperiod;
}
$enrol_manual->enrol_user($instance, $adduser->id, $roleid, $timestart, $timeend);
- add_to_log($course->id, 'course', 'enrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
}
$potentialuserselector->invalidate_selected_users();
if (!empty($userstounassign)) {
foreach($userstounassign as $removeuser) {
$enrol_manual->unenrol_user($instance, $removeuser->id);
- add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
}
$potentialuserselector->invalidate_selected_users();
if ($confirm and confirm_sesskey()) {
$plugin->unenrol_user($instance, $USER->id);
- add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere!
+
redirect(new moodle_url('/index.php'));
}
$expectedlegacyeventdata->enrol = 'meta';
$expectedlegacyeventdata->courseid = $course2->id;
$this->assertEventLegacyData($expectedlegacyeventdata, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEquals(0, $DB->count_records('user_enrolments'));
$this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
$this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+ $this->assertEventContextNotUsed($event);
}
/**
$expectedlegacyeventdata->enrol = 'meta';
$expectedlegacyeventdata->courseid = $course2->id;
$this->assertEventLegacyData($expectedlegacyeventdata, $event);
+ $this->assertEventContextNotUsed($event);
}
}
if ($confirm and confirm_sesskey()) {
$plugin->unenrol_user($instance, $USER->id);
- add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
+
redirect(new moodle_url('/index.php'));
}
}
$this->enrol_user($instance, $USER->id, $instance->roleid, $timestart, $timeend);
- add_to_log($instance->courseid, 'course', 'enrol', '../enrol/users.php?id='.$instance->courseid, $instance->courseid); //TODO: There should be userid somewhere!
if ($instance->password and $instance->customint1 and $data->enrolpassword !== $instance->password) {
// It must be a group enrolment, let's assign group too.
if ($confirm and confirm_sesskey()) {
$plugin->unenrol_user($instance, $USER->id);
- add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere!
+
redirect(new moodle_url('/index.php'));
}
$dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $admin->id));
$this->assertInstanceOf('\core\event\user_enrolment_created', $event);
$this->assertEquals($dbuserenrolled->id, $event->objectid);
+ $this->assertEquals(context_course::instance($course1->id), $event->get_context());
$this->assertEquals('user_enrolled', $event->get_legacy_eventname());
$expectedlegacyeventdata = $dbuserenrolled;
$expectedlegacyeventdata->enrol = $manual->get_name();
$expectedlegacyeventdata->courseid = $course1->id;
$this->assertEventLegacyData($expectedlegacyeventdata, $event);
+ $expected = array($course1->id, 'course', 'enrol', '../enrol/users.php?id=' . $course1->id, $course1->id);
+ $this->assertEventLegacyLogData($expected, $event);
+ $this->assertEventContextNotUsed($event);
+ }
+
+ /**
+ * Test user_enrolment_deleted event.
+ */
+ public function test_user_enrolment_deleted_event() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ $manualplugin = enrol_get_plugin('manual');
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course();
+ $student = $DB->get_record('role', array('shortname' => 'student'));
+
+ $enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+
+ // Enrol user.
+ $manualplugin->enrol_user($enrol, $user->id, $student->id);
+
+ // Get the user enrolment information, used to validate legacy event data.
+ $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $user->id));
+
+ // Unenrol user and capture event.
+ $sink = $this->redirectEvents();
+ $manualplugin->unenrol_user($enrol, $user->id);
+ $events = $sink->get_events();
+ $sink->close();
+ $event = array_pop($events);
+
+ // Validate the event.
+ $this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
+ $this->assertEquals(context_course::instance($course->id), $event->get_context());
+ $this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+ $expectedlegacyeventdata = $dbuserenrolled;
+ $expectedlegacyeventdata->enrol = $manualplugin->get_name();
+ $expectedlegacyeventdata->courseid = $course->id;
+ $expectedlegacyeventdata->lastenrol = true;
+ $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+ $expected = array($course->id, 'course', 'unenrol', '../enrol/users.php?id=' . $course->id, $course->id);
+ $this->assertEventLegacyLogData($expected, $event);
+ $this->assertEventContextNotUsed($event);
}
}
// Insert the information about the file.
$contentid = $DB->insert_record('data_content', $datacontent);
// Required information for uploading a file.
- $context = context_module::instance($module->id);
+ $context = context_module::instance($module->cmid);
$usercontext = context_user::instance($USER->id);
$component = 'mod_data';
$filearea = 'content';
$modified = 0;
// Context level and instance ID are used to determine what the context is.
$contextlevel = 'module';
- $instanceid = $module->id;
+ $instanceid = $module->cmid;
$testfilelisting = core_files_external::get_files($nocontext, $component, $filearea, $itemid, '/', $filename, $modified, $contextlevel, $instanceid);
$this->assertEquals($testfilelisting, $testdata);
}
$scale = $this->getDataGenerator()->create_scale();
$course = $this->getDataGenerator()->create_course();
$assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
- $modulecontext = context_module::instance($assign->id);
+ $modulecontext = context_module::instance($assign->cmid);
// The generator returns a dummy object, lets get the real assign object.
$assign = new assign($modulecontext, false, false);
$cm = $assign->get_course_module();
// Create a teacher and give them capabilities.
$coursecontext = context_course::instance($course->id);
$roleid = $this->assignUserCapability('moodle/course:viewparticipants', $coursecontext->id, 3);
- $modulecontext = context_module::instance($cm->id);
+ $modulecontext = context_module::instance($cm->cmid);
$this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
// Create the teacher's enrolment record.
$DB->insert_record('gradingform_rubric_levels', $rubriclevel2);
// Call the external function.
- $cmids = array ($cm->id);
+ $cmids = array ($cm->cmid);
$areaname = 'submissions';
$result = core_grading_external::get_definitions($cmids, $areaname);
// Create a teacher and give them capabilities.
$coursecontext = context_course::instance($course->id);
$roleid = $this->assignUserCapability('moodle/course:viewparticipants', $coursecontext->id, 3);
- $modulecontext = context_module::instance($assign->id);
+ $modulecontext = context_module::instance($assign->cmid);
$this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
// Create the teacher's enrolment record.
$data = array();
if ($groupings = $DB->get_records('groupings', array('courseid'=>$course->id), 'name')) {
$canchangeidnumber = has_capability('moodle/course:changeidnumber', $context);
+ foreach ($groupings as $gid => $grouping) {
+ $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context));
+ }
+ core_collator::asort_objects_by_property($groupings, 'formattedname');
foreach($groupings as $grouping) {
$line = array();
- $line[0] = format_string($grouping->name);
+ $line[0] = $grouping->formattedname;
if ($groups = groups_get_all_groups($courseid, 0, $grouping->id)) {
$groupnames = array();
$strnogroups = get_string('nogroups', 'group');
$strdescription = get_string('description');
-// Get all groupings
+// Get all groupings and sort them by formatted name.
$groupings = $DB->get_records('groupings', array('courseid'=>$courseid), 'name');
+foreach ($groupings as $gid => $grouping) {
+ $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context));
+}
+core_collator::asort_objects_by_property($groupings, 'formattedname');
$members = array();
foreach ($groupings as $grouping) {
$members[$grouping->id] = array();
$options = array();
$options[0] = get_string('all');
foreach ($groupings as $grouping) {
- $options[$grouping->id] = strip_tags(format_string($grouping->name));
+ $options[$grouping->id] = strip_tags($grouping->formattedname);
}
$popupurl = new moodle_url($rooturl.'&group='.$groupid);
$select = new single_select($popupurl, 'grouping', $options, $groupingid, array());
if ($gpgid < 0) {
echo $OUTPUT->heading($strnotingrouping, 3);
} else {
- echo $OUTPUT->heading(format_string($groupings[$gpgid]->name), 3);
+ echo $OUTPUT->heading($groupings[$gpgid]->formattedname, 3);
$description = file_rewrite_pluginfile_urls($groupings[$gpgid]->description, 'pluginfile.php', $context->id, 'grouping', 'description', $gpgid);
$options = new stdClass;
$options->noclean = true;
$string['downloadedfilecheckfailed'] = 'La vérification du fichier téléchargé à échoué';
$string['invalidmd5'] = 'Le code de contrôle md5 n\'est pas valide';
$string['missingrequiredfield'] = 'Un champ obligatoire n\'est pas renseigné';
-$string['remotedownloaderror'] = 'Le téléchargement de composants sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L\'extension cURL de PHP est vivement recommandée.<br /><br />Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l\'emplacement « {$a->dest} » et le décompresser à cet endroit';
+$string['remotedownloaderror'] = '<p>Le téléchargement du composant sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L\'extension cURL de PHP est vivement recommandée.</p>
+<p>Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l\'emplacement « {$a->dest} » et le décompresser à cet endroit.</p>';
$string['wrongdestpath'] = 'Chemin de destination incorrect';
$string['wrongsourcebase'] = 'Adresse URL de base de la source incorrecte';
$string['wrongzipfilename'] = 'Nom de fichier ZIP incorrect';
/**
* Checks if behat is set up and working
*
- * Uses notice() instead of behat_error() because is
- * also called from web interface
+ * Notifies failures both from CLI and web interface.
*
* It checks behat dependencies have been installed and runs
* the behat help command to ensure it works as expected
public static function behat_setup_problem() {
global $CFG;
- $clibehaterrorstr = "Behat dependencies not installed. Ensure you ran the composer installer. " . self::DOCS_URL . "#Installation\n";
-
// Moodle setting.
if (!self::are_behat_dependencies_installed()) {
-
- // With HTML.
- if (!CLI_SCRIPT) {
-
- $msg = get_string('wrongbehatsetup', 'tool_behat');
- $docslink = self::DOCS_URL . '#Installation';
- $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
- $msg .= get_string('moreinfoin', 'tool_behat', $docslink);
- } else {
- $msg = $clibehaterrorstr;
- }
-
- self::output_msg($msg);
+ // Returning composer error code to avoid conflicts with behat and moodle error codes.
+ self::output_msg(get_string('errorcomposer', 'tool_behat'));
return BEHAT_EXITCODE_COMPOSER;
}
list($output, $code) = self::run(' --help');
if ($code != 0) {
+
// Returning composer error code to avoid conflicts with behat and moodle error codes.
- if (!CLI_SCRIPT) {
- $msg = get_string('wrongbehatsetup', 'tool_behat');
- } else {
- $msg = $clibehaterrorstr;
- }
- self::output_msg($msg);
+ self::output_msg(get_string('errorbehatcommand', 'tool_behat', self::get_behat_command()));
return BEHAT_EXITCODE_COMPOSER;
}
+ // No empty values.
+ if (empty($CFG->behat_dataroot) || empty($CFG->behat_prefix) || empty($CFG->behat_wwwroot)) {
+ self::output_msg(get_string('errorsetconfig', 'tool_behat'));
+ return BEHAT_EXITCODE_CONFIG;
+
+ }
+
+ // Not repeated values.
+ // We only need to check this when the behat site is not running as
+ // at this point, when it is running, all $CFG->behat_* vars have
+ // already been copied to $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
+ if (!defined('BEHAT_SITE_RUNNING') &&
+ ($CFG->behat_prefix == $CFG->prefix ||
+ $CFG->behat_dataroot == $CFG->dataroot ||
+ $CFG->behat_wwwroot == $CFG->wwwroot ||
+ (!empty($CFG->phpunit_prefix) && $CFG->phpunit_prefix == $CFG->behat_prefix) ||
+ (!empty($CFG->phpunit_dataroot) && $CFG->phpunit_dataroot == $CFG->behat_dataroot)
+ )) {
+ self::output_msg(get_string('erroruniqueconfig', 'tool_behat'));
+ return BEHAT_EXITCODE_CONFIG;
+ }
+
// Checking behat dataroot existence otherwise echo about admin/tool/behat/cli/init.php.
if (!empty($CFG->behat_dataroot)) {
$CFG->behat_dataroot = realpath($CFG->behat_dataroot);
}
if (empty($CFG->behat_dataroot) || !is_dir($CFG->behat_dataroot) || !is_writable($CFG->behat_dataroot)) {
- self::output_msg(get_string('runclitool', 'tool_behat', 'php admin/tool/behat/cli/init.php'));
+ self::output_msg(get_string('errordataroot', 'tool_behat'));
return BEHAT_EXITCODE_CONFIG;
}
* @return void
*/
protected static function output_msg($msg) {
+ global $CFG, $PAGE;
+ // If we are using the web interface we want pretty messages.
if (!CLI_SCRIPT) {
- // General info about the tool purpose.
- $msg = get_string('aim', 'tool_behat') . '<br /><br />' . $msg;
- notice($msg);
+
+ $renderer = $PAGE->get_renderer('tool_behat');
+ echo $renderer->render_error($msg);
+
+ // Stopping execution.
+ exit(1);
+
} else {
- echo $msg;
+
+ // We continue execution after this.
+ $clibehaterrorstr = "Ensure you set \$CFG->behat_* vars in config.php " .
+ "and you ran admin/tool/behat/cli/init.php.\n" .
+ "More info in " . self::DOCS_URL . "#Installation\n\n";
+
+ echo 'Error: ' . $msg . "\n\n" . $clibehaterrorstr;
}
}
if ($editorid = $this->get_editor_id()) {
// Set the value to the iframe and save it to the textarea.
+ $value = str_replace('"', '\"', $value);
$this->session->executeScript('
tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
tinyMCE.get("'.$editorid.'").save();
// Textareas are considered text based elements.
$tagname = strtolower($this->field->getTagName());
if ($tagname == 'textarea') {
- return false;
- }
- if ($tagname == 'input') {
+ if (!$this->running_javascript()) {
+ return false;
+ }
+
+ // If there is an iframe with $id + _ifr there a TinyMCE editor loaded.
+ $xpath = '//iframe[@id="' . $this->field->getAttribute('id') . '_ifr"]';
+ if (!$this->session->getPage()->find('xpath', $xpath)) {
+
+ // Generic one if it is a normal textarea.
+ return false;
+ }
+
+ $classname = 'behat_form_editor';
+
+ } else if ($tagname == 'input') {
$type = $this->field->getAttribute('type');
switch ($type) {
case 'text':
return;
}
+ // Wrapped in try & catch as the element may disappear if an AJAX request was submitted.
+ try {
+ $multiple = $this->field->hasAttribute('multiple');
+ } catch (Exception $e) {
+ // We do not specify any specific Exception type as there are
+ // different exceptions that can be thrown by the driver and
+ // we can not control them all, also depending on the selenium
+ // version the exception type can change.
+ return;
+ }
+
+ // Wait for all the possible AJAX requests that have been
+ // already triggered by selectOption() to be finished.
+ $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
+
// Single select sometimes needs an extra click in the option.
- if (!$this->field->hasAttribute('multiple')) {
+ if (!$multiple) {
// Using the driver direcly because Element methods are messy when dealing
// with elements inside containers.
// Wrapped in a try & catch as we can fall into race conditions
// and the element may not be there.
try {
-
- // Wait for all the possible AJAX requests that have been
- // already triggered by selectOption() to be finished.
- $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
current($optionnodes)->click();
} catch (Exception $e) {
// We continue and return as this means that the element is not there or it is not the same.
} else {
- // Wait for all the possible AJAX requests that have been
- // already triggered by selectOption() to be finished.
- $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
// Wrapped in a try & catch as we can fall into race conditions
// and the element may not be there.
try {
* @return void
*/
protected function validate_data() {
- if (!$this->context->contextlevel === CONTEXT_MODULE) {
+ if (!$this->contextlevel === CONTEXT_MODULE) {
throw new \coding_exception('Content level must be CONTEXT_MODULE.');
}
}
* @return void
*/
protected function validate_data() {
- if (!$this->context->contextlevel === CONTEXT_MODULE) {
+ if (!$this->contextlevel === CONTEXT_MODULE) {
throw new \coding_exception('Content level must be CONTEXT_MODULE.');
} else if (!isset($this->other['pathnamehashes']) || !is_array($this->other['pathnamehashes'])) {
throw new \coding_exception('pathnamehashes must be set in $other and must be an array.');
if (isset($this->context)) {
return $this->context;
}
- $this->context = \context::instance_by_id($this->data['contextid'], false);
+ $this->context = \context::instance_by_id($this->data['contextid'], IGNORE_MISSING);
return $this->context;
}
* @return string
*/
public function get_description() {
- return "Blog association added between entry id $this->other['blogid'] and $this->other['associatetype'] with id
- $this->other['associateid']";
+ return "Blog association added between entry id {$this->other['blogid']} and {$this->other['associatetype']} with id
+ {$this->other['associateid']}";
}
/**
* @return \moodle_url
*/
public function get_url() {
- return $this->context->get_url();
+ $context = $this->get_context();
+ if ($context) {
+ return $context->get_url();
+ } else {
+ return null;
+ }
}
/**
* @return \moodle_url
*/
public function get_url() {
- return $this->context->get_url();
+ $context = $this->get_context();
+ if ($context) {
+ return $context->get_url();
+ } else {
+ return null;
+ }
}
/**
* @return \moodle_url
*/
public function get_url() {
- return $this->context->get_url();
+ $context = $this->get_context();
+ if ($context) {
+ return $context->get_url();
+ } else {
+ return null;
+ }
}
}
* @return void
*/
protected function validate_data() {
- if ($this->context->contextlevel !== CONTEXT_COURSE) {
+ if ($this->contextlevel !== CONTEXT_COURSE) {
throw new \coding_exception('The context must be a course level context.');
}
}
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url("/mod/$this->objecttable/view.php", array('id' => $this->context->instanceid));
+ return new \moodle_url("/mod/$this->objecttable/view.php", array('id' => $this->contextinstanceid));
}
/**
* @return array|null
*/
protected function get_legacy_logdata() {
- return array($this->courseid, $this->objecttable, 'view', 'view.php?id=' . $this->context->instanceid, $this->objectid,
- $this->context->instanceid);
+ return array($this->courseid, $this->objecttable, 'view', 'view.php?id=' . $this->contextinstanceid, $this->objectid,
+ $this->contextinstanceid);
}
/**
return $legacyeventdata;
}
+ /**
+ * Return legacy data for add_to_log().
+ *
+ * @return array
+ */
+ protected function get_legacy_logdata() {
+ return array($this->courseid, 'course', 'enrol', '../enrol/users.php?id=' . $this->courseid, $this->courseid);
+ }
+
/**
* Custom validation.
*
return (object)$this->other['userenrolment'];
}
+ /**
+ * Return legacy data for add_to_log().
+ *
+ * @return array
+ */
+ protected function get_legacy_logdata() {
+ return array($this->courseid, 'course', 'unenrol', '../enrol/users.php?id=' . $this->courseid, $this->courseid);
+ }
/**
* Custom validation.
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is a db record locking factory.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This is a db record locking factory.
+ *
+ * This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
+ * value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
+ * will always be slower than some shared memory type locking function.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class db_record_lock_factory implements lock_factory {
+
+ /** @var \moodle_database $db Hold a reference to the global $DB */
+ protected $db;
+
+ /** @var string $type Used to prefix lock keys */
+ protected $type;
+
+ /** @var array $openlocks - List of held locks - used by auto-release */
+ protected $openlocks = array();
+
+ /**
+ * Is available.
+ * @return boolean - True if this lock type is available in this environment.
+ */
+ public function is_available() {
+ return true;
+ }
+
+ /**
+ * Almighty constructor.
+ * @param string $type - Used to prefix lock keys.
+ */
+ public function __construct($type) {
+ global $DB;
+
+ $this->type = $type;
+ // Save a reference to the global $DB so it will not be released while we still have open locks.
+ $this->db = $DB;
+
+ \core_shutdown_manager::register_function(array($this, 'auto_release'));
+ }
+
+ /**
+ * Return information about the blocking behaviour of the lock type on this platform.
+ * @return boolean - True
+ */
+ public function supports_timeout() {
+ return true;
+ }
+
+ /**
+ * Will this lock type will be automatically released when a process ends.
+ *
+ * @return boolean - True (shutdown handler)
+ */
+ public function supports_auto_release() {
+ return true;
+ }
+
+ /**
+ * Multiple locks for the same resource can be held by a single process.
+ * @return boolean - False - not process specific.
+ */
+ public function supports_recursion() {
+ return false;
+ }
+
+ /**
+ * This function generates a unique token for the lock to use.
+ * It is important that this token is not solely based on time as this could lead
+ * to duplicates in a clustered environment (especially on VMs due to poor time precision).
+ */
+ protected function generate_unique_token() {
+ $uuid = '';
+
+ if (function_exists("uuid_create")) {
+ $context = null;
+ uuid_create($context);
+
+ uuid_make($context, UUID_MAKE_V4);
+ uuid_export($context, UUID_FMT_STR, $uuid);
+ } else {
+ // Fallback uuid generation based on:
+ // "http://www.php.net/manual/en/function.uniqid.php#94959".
+ $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+
+ // 32 bits for "time_low".
+ mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+
+ // 16 bits for "time_mid".
+ mt_rand(0, 0xffff),
+
+ // 16 bits for "time_hi_and_version",
+ // four most significant bits holds version number 4.
+ mt_rand(0, 0x0fff) | 0x4000,
+
+ // 16 bits, 8 bits for "clk_seq_hi_res",
+ // 8 bits for "clk_seq_low",
+ // two most significant bits holds zero and one for variant DCE1.1.
+ mt_rand(0, 0x3fff) | 0x8000,
+
+ // 48 bits for "node".
+ mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
+ }
+ return trim($uuid);
+ }
+
+
+ /**
+ * Create and get a lock
+ * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+ * @param int $timeout - The number of seconds to wait for a lock before giving up.
+ * @param int $maxlifetime - Unused by this lock type.
+ * @return boolean - true if a lock was obtained.
+ */
+ public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+
+ $token = $this->generate_unique_token();
+ $now = time();
+ $giveuptime = $now + $timeout;
+ $expires = $now + $maxlifetime;
+
+ if (!$this->db->record_exists('lock_db', array('resourcekey' => $resource))) {
+ $record = new \stdClass();
+ $record->resourcekey = $resource;
+ $result = $this->db->insert_record('lock_db', $record);
+ }
+
+ $params = array('expires' => $expires,
+ 'token' => $token,
+ 'resourcekey' => $resource,
+ 'now' => $now);
+ $sql = 'UPDATE {lock_db}
+ SET
+ expires = :expires,
+ owner = :token
+ WHERE
+ resourcekey = :resourcekey AND
+ (owner IS NULL OR expires < :now)';
+
+ do {
+ $now = time();
+ $params['now'] = $now;
+ $this->db->execute($sql, $params);
+
+ $countparams = array('owner' => $token, 'resourcekey' => $resource);
+ $result = $this->db->count_records('lock_db', $countparams);
+ $locked = $result === 1;
+ if (!$locked) {
+ usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+ }
+ // Try until the giveup time.
+ } while (!$locked && $now < $giveuptime);
+
+ if ($locked) {
+ $this->openlocks[$token] = 1;
+ return new lock($token, $this);
+ }
+
+ return false;
+ }
+
+ /**
+ * Release a lock that was previously obtained with @lock.
+ * @param lock $lock - a lock obtained from this factory.
+ * @return boolean - true if the lock is no longer held (including if it was never held).
+ */
+ public function release_lock(lock $lock) {
+ $params = array('noexpires' => null,
+ 'token' => $lock->get_key(),
+ 'noowner' => null);
+
+ $sql = 'UPDATE {lock_db}
+ SET
+ expires = :noexpires,
+ owner = :noowner
+ WHERE
+ owner = :token';
+ $result = $this->db->execute($sql, $params);
+ if ($result) {
+ unset($this->openlocks[$lock->get_key()]);
+ }
+ return $result;
+ }
+
+ /**
+ * Extend a lock that was previously obtained with @lock.
+ * @param lock $lock - a lock obtained from this factory.
+ * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+ * @return boolean - true if the lock was extended.
+ */
+ public function extend_lock(lock $lock, $maxlifetime = 86400) {
+ $now = time();
+ $expires = $now + $maxlifetime;
+ $params = array('expires' => $expires,
+ 'token' => $lock->get_key());
+
+ $sql = 'UPDATE {lock_db}
+ SET
+ expires = :expires,
+ WHERE
+ owner = :token';
+
+ $this->db->execute($sql, $params);
+ $countparams = array('owner' => $lock->get_key());
+ $result = $this->count_records('lock_db', $countparams);
+
+ return $result === 0;
+ }
+
+ /**
+ * Auto release any open locks on shutdown.
+ * This is required, because we may be using persistent DB connections.
+ */
+ public function auto_release() {
+ // Called from the shutdown handler. Must release all open locks.
+ foreach ($this->openlocks as $key => $unused) {
+ $lock = new lock($key, $this);
+ $this->release_lock($lock);
+ }
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Flock based file locking factory.
+ *
+ * The file lock factory returns file locks locked with the flock function. Works OK, except on some
+ * NFS, exotic shared storage and exotic server OSes (like windows). On windows, a second attempt to get a
+ * lock will block indefinitely instead of timing out.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Flock based file locking factory.
+ *
+ * The file lock factory returns file locks locked with the flock function. Works OK, except on some
+ * NFS, exotic shared storage and exotic server OSes (like windows). On windows, a second attempt to get a
+ * lock will block indefinitely instead of timing out.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class file_lock_factory implements lock_factory {
+
+ /** @var string $type - The type of lock, e.g. cache, cron, session. */
+ protected $type;
+
+ /** @var string $lockdirectory - Full system path to the directory used to store file locks. */
+ protected $lockdirectory;
+
+ /** @var boolean $verbose - If true, debugging info about the owner of the lock will be written to the lock file. */
+ protected $verbose;
+
+ /**
+ * Create this lock factory.
+ *
+ * @param string $type - The type, e.g. cron, cache, session
+ */
+ public function __construct($type) {
+ global $CFG;
+
+ $this->type = $type;
+ if (!isset($CFG->file_lock_root)) {
+ $this->lockdirectory = $CFG->dataroot . '/lock';
+ } else {
+ $this->lockdirectory = $CFG->file_lock_root;
+ }
+ $this->verbose = false;
+ if ($CFG->debugdeveloper) {
+ $this->verbose = true;
+ }
+ }
+
+ /**
+ * Return information about the blocking behaviour of the lock type on this platform.
+ * @return boolean - False if attempting to get a lock will block indefinitely.
+ */
+ public function supports_timeout() {
+ global $CFG;
+
+ return $CFG->ostype !== 'WINDOWS';
+ }
+
+ /**
+ * This lock type will be automatically released when a process ends.
+ * @return boolean - True
+ */
+ public function supports_auto_release() {
+ return true;
+ }
+
+ /**
+ * Is available.
+ * @return boolean - True if preventfilelocking is not set - or the file_lock_root is not in dataroot.
+ */
+ public function is_available() {
+ global $CFG;
+ $preventfilelocking = !empty($CFG->preventfilelocking);
+ $lockdirisdataroot = true;
+ if (!empty($CFG->file_lock_root) && strpos($CFG->file_lock_root, $CFG->dataroot) !== 0) {
+ $lockdirisdataroot = false;
+ }
+ return !$preventfilelocking || !$lockdirisdataroot;
+ }
+
+ /**
+ * Multiple locks for the same resource cannot be held from a single process.
+ * @return boolean - False
+ */
+ public function supports_recursion() {
+ return false;
+ }
+
+ /**
+ * Get some info that might be useful for debugging.
+ * @return boolean - string
+ */
+ protected function get_debug_info() {
+ return 'host:' . php_uname('n') . ', pid:' . getmypid() . ', time:' . time();
+ }
+
+ /**
+ * Get a lock within the specified timeout or return false.
+ * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+ * @param int $timeout - The number of seconds to wait for a lock before giving up.
+ * @param int $maxlifetime - Unused by this lock type.
+ * @return boolean - true if a lock was obtained.
+ */
+ public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+ $giveuptime = time() + $timeout;
+
+ $hash = md5($this->type . '_' . $resource);
+ $lockdir = $this->lockdirectory . '/' . substr($hash, 0, 2);
+
+ if (!check_dir_exists($lockdir, true, true)) {
+ return false;
+ }
+
+ $lockfilename = $lockdir . '/' . $hash;
+
+ $filehandle = fopen($lockfilename, "wb");
+
+ // Could not open the lock file.
+ if (!$filehandle) {
+ return false;
+ }
+
+ do {
+ // Will block on windows. So sad.
+ $wouldblock = false;
+ $locked = flock($filehandle, LOCK_EX | LOCK_NB, $wouldblock);
+ if (!$locked && $wouldblock) {
+ usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+ }
+ // Try until the giveup time.
+ } while (!$locked && $wouldblock && time() < $giveuptime);
+
+ if (!$locked) {
+ fclose($filehandle);
+ return false;
+ }
+ if ($this->verbose) {
+ fwrite($filehandle, $this->get_debug_info());
+ }
+ return new lock($filehandle, $this);
+ }
+
+ /**
+ * Release a lock that was previously obtained with @lock.
+ * @param lock $lock - A lock obtained from this factory.
+ * @return boolean - true if the lock is no longer held (including if it was never held).
+ */
+ public function release_lock(lock $lock) {
+ $handle = $lock->get_key();
+
+ if (!$handle) {
+ // We didn't have a lock.
+ return false;
+ }
+
+ $result = flock($handle, LOCK_UN);
+ fclose($handle);
+ return $result;
+ }
+
+ /**
+ * Extend a lock that was previously obtained with @lock.
+ * @param lock $lock - not used
+ * @param int $maxlifetime - not used
+ * @return boolean - true if the lock was extended.
+ */
+ public function extend_lock(lock $lock, $maxlifetime = 86400) {
+ // Not supported by this factory.
+ return false;
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class representing a lock
+ *
+ * The methods available for a specific lock type are only known by it's factory.
+ *
+ * @package core
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class representing a lock
+ *
+ * The methods available for a specific lock type are only known by it's factory.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock {
+
+ /** @var string|int $key A unique key representing a held lock */
+ protected $key = '';
+
+ /** @var lock_factory $factory The factory that generated this lock */
+ protected $factory;
+
+ /** @var bool $released Has this lock been released? If a lock falls out of scope without being released - show a warning. */
+ protected $released;
+
+ /**
+ * Construct a lock containing the unique key required to release it.
+ * @param mixed $key - The lock key. The type of this is up to the lock_factory being used.
+ * For file locks this is a file handle. For MySQL this is a string.
+ * @param lock_factory $factory - The factory that generated this lock.
+ */
+ public function __construct($key, $factory) {
+ $this->factory = $factory;
+ $this->key = $key;
+ $this->released = false;
+ }
+
+ /**
+ * Return the unique key representing this lock.
+ * @return string|int lock key.
+ */
+ public function get_key() {
+ return $this->key;
+ }
+
+ /**
+ * Extend the lifetime of this lock. Not supported by all factories.
+ * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+ * @return bool
+ */
+ public function extend($maxlifetime = 86400) {
+ if ($this->factory) {
+ return $this->factory->extend_lock($this, $maxlifetime);
+ }
+ return false;
+ }
+
+ /**
+ * Release this lock
+ * @return bool
+ */
+ public function release() {
+ $this->released = true;
+ if (empty($this->factory)) {
+ return false;
+ }
+ $result = $this->factory->release_lock($this);
+ // Release any held references to the factory.
+ unset($this->factory);
+ $this->factory = null;
+ $this->key = '';
+ return $result;
+ }
+
+ /**
+ * Print debugging if this lock falls out of scope before being released.
+ */
+ public function __destruct() {
+ if (!$this->released && defined('PHPUNIT_TEST')) {
+ $this->release();
+ throw new \coding_exception('\core\lock\lock(' . $this->key . ') has fallen out of scope ' .
+ 'without being released.' . "\n" .
+ 'Locks must ALWAYS be released by calling $mylock->release().');
+ }
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Lock configuration class, used to get an instance of the currently configured lock factory.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Lock configuration class, used to get an instance of the currently configured lock factory.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_config {
+
+ /**
+ * Get an instance of the currently configured locking subclass.
+ *
+ * @param string $type - Unique namespace for the locks generated by this factory. e.g. core_cron
+ * @return \core\lock\lock_factory
+ * @throws \coding_exception
+ */
+ public static function get_lock_factory($type) {
+ global $CFG, $DB;
+ $lockfactory = null;
+
+ if (isset($CFG->lock_factory) && $CFG->lock_factory != 'auto') {
+ if (!class_exists($CFG->lock_factory)) {
+ // In this case I guess it is not safe to continue. Different cluster nodes could end up using different locking
+ // types because of an installation error.
+ throw new \coding_exception('Lock factory set in $CFG does not exist: ' . $CFG->lock_factory);
+ }
+ $lockfactoryclass = $CFG->lock_factory;
+ $lockfactory = new $lockfactoryclass($type);
+ } else {
+ $dbtype = clean_param($DB->get_dbfamily(), PARAM_ALPHA);
+
+ // DB Specific lock factory is preferred - should support auto-release.
+ $lockfactoryclass = "\\core\\lock\\${dbtype}_lock_factory";
+ if (!class_exists($lockfactoryclass)) {
+ $lockfactoryclass = '\core\lock\file_lock_factory';
+ }
+ /* @var lock_factory $lockfactory */
+ $lockfactory = new $lockfactoryclass($type);
+ if (!$lockfactory->is_available()) {
+ // Final fallback - DB row locking.
+ $lockfactory = new \core\lock\db_record_lock_factory($type);
+ }
+ }
+
+ return $lockfactory;
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Defines abstract factory class for generating locks.
+ *
+ * @package core
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Defines abstract factory class for generating locks.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface lock_factory {
+
+ /**
+ * Define the constructor signature required by the lock_config class.
+ *
+ * @param string $type - The type this lock is used for (e.g. cron, cache)
+ */
+ public function __construct($type);
+
+ /**
+ * Return information about the blocking behaviour of the locks on this platform.
+ *
+ * @return boolean - False if attempting to get a lock will block indefinitely.
+ */
+ public function supports_timeout();
+
+ /**
+ * Will this lock be automatically released when the process ends.
+ * This should never be relied upon in code - but is useful in the case of
+ * fatal errors. If a lock type does not support this auto release,
+ * the max lock time parameter must be obeyed to eventually clean up a lock.
+ *
+ * @return boolean - True if this lock type will be automatically released when the current process ends.
+ */
+ public function supports_auto_release();
+
+ /**
+ * Supports recursion.
+ *
+ * @return boolean - True if attempting to get 2 locks on the same resource will "stack"
+ */
+ public function supports_recursion();
+
+ /**
+ * Is available.
+ *
+ * @return boolean - True if this lock type is available in this environment.
+ */
+ public function is_available();
+
+ /**
+ * Get a lock within the specified timeout or return false.
+ *
+ * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+ * @param int $timeout - The number of seconds to wait for a lock before giving up.
+ * Not all lock types will support this.
+ * @param int $maxlifetime - The number of seconds to wait before reclaiming a stale lock.
+ * Not all lock types will use this - e.g. if they support auto releasing
+ * a lock when a process ends.
+ * @return \core\lock\lock|boolean - An instance of \core\lock\lock if the lock was obtained, or false.
+ */
+ public function get_lock($resource, $timeout, $maxlifetime = 86400);
+
+ /**
+ * Release a lock that was previously obtained with @lock.
+ *
+ * @param lock $lock - The lock to release.
+ * @return boolean - True if the lock is no longer held (including if it was never held).
+ */
+ public function release_lock(lock $lock);
+
+ /**
+ * Extend the timeout on a held lock.
+ *
+ * @param lock $lock - lock obtained from this factory
+ * @param int $maxlifetime - new max time to hold the lock
+ * @return boolean - True if the lock was extended.
+ */
+ public function extend_lock(lock $lock, $maxlifetime = 86400);
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Postgres advisory locking factory.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Postgres advisory locking factory.
+ *
+ * Postgres locking implementation using advisory locks. Some important points. Postgres has
+ * 2 different forms of lock functions, some accepting a single int, and some accepting 2 ints. This implementation
+ * uses the 2 int version so that it uses a separate namespace from the session locking. The second note,
+ * is because postgres uses integer keys for locks, we first need to map strings to a unique integer. This is
+ * done by storing the strings in the lock_db table and using the auto-id returned. There is a static cache for
+ * id's in this function.
+ *
+ * @package core
+ * @category lock
+ * @copyright Damyon Wiese 2013
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class postgres_lock_factory implements lock_factory {
+
+ /** @var int $dblockid - used as a namespace for these types of locks (separate from session locks) */
+ protected $dblockid = -1;
+
+ /** @var array $lockidcache - static cache for string -> int conversions required for pg advisory locks. */
+ protected static $lockidcache = array();
+
+ /** @var \moodle_database $db Hold a reference to the global $DB */
+ protected $db;
+
+ /** @var string $type Used to prefix lock keys */
+ protected $type;
+
+ /** @var array $openlocks - List of held locks - used by auto-release */
+ protected $openlocks = array();
+
+ /**
+ * Calculate a unique instance id based on the database name and prefix.
+ * @return int.
+ */
+ protected function get_unique_db_instance_id() {
+ global $CFG;
+
+ $strkey = $CFG->dbname . ':' . $CFG->prefix;
+ $intkey = crc32($strkey);
+ // Normalize between 64 bit unsigned int and 32 bit signed ints. Php could return either from crc32.
+ if (PHP_INT_SIZE == 8) {
+ if ($intkey > 0x7FFFFFFF) {
+ $intkey -= 0x100000000;
+ }
+ }
+
+ return $intkey;
+ }
+
+ /**
+ * Almighty constructor.
+ * @param string $type - Used to prefix lock keys.
+ */
+ public function __construct($type) {
+ global $DB;
+
+ $this->type = $type;
+ $this->dblockid = $this->get_unique_db_instance_id();
+ // Save a reference to the global $DB so it will not be released while we still have open locks.
+ $this->db = $DB;
+
+ \core_shutdown_manager::register_function(array($this, 'auto_release'));
+ }
+
+ /**
+ * Is available.
+ * @return boolean - True if this lock type is available in this environment.
+ */
+ public function is_available() {
+ return $this->db->get_dbfamily() === 'postgres';
+ }
+
+ /**
+ * Return information about the blocking behaviour of the lock type on this platform.
+ * @return boolean - Defer to the DB driver.
+ */
+ public function supports_timeout() {
+ return true;
+ }
+
+ /**
+ * Will this lock type will be automatically released when a process ends.
+ *
+ * @return boolean - Via shutdown handler.
+ */
+ public function supports_auto_release() {
+ return true;
+ }
+
+ /**
+ * Multiple locks for the same resource can be held by a single process.
+ * @return boolean - Defer to the DB driver.
+ */
+ public function supports_recursion() {
+ return true;
+ }
+
+ /**
+ * This function generates the unique index for a specific lock key.
+ * Once an index is assigned to a key, it never changes - so this is
+ * statically cached.
+ *
+ * @param string $key
+ * @return int
+ * @throws \moodle_exception
+ */
+ protected function get_index_from_key($key) {
+ if (isset(self::$lockidcache[$key])) {
+ return self::$lockidcache[$key];
+ }
+
+ $index = 0;
+ $record = $this->db->get_record('lock_db', array('resourcekey' => $key));
+ if ($record) {
+ $index = $record->id;
+ }
+
+ if (!$index) {
+ $record = new \stdClass();
+ $record->resourcekey = $key;
+ try {
+ $index = $this->db->insert_record('lock_db', $record);
+ } catch (\dml_exception $de) {
+ // Race condition - never mind - now the value is guaranteed to exist.
+ $record = $this->db->get_record('lock_db', array('resourcekey' => $key));
+ if ($record) {
+ $index = $record->id;
+ }
+ }
+ }
+
+ if (!$index) {
+ throw new \moodle_exception('Could not generate unique index for key');
+ }
+
+ self::$lockidcache[$key] = $index;
+ return $index;
+ }
+
+ /**
+ * Create and get a lock
+ * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+ * @param int $timeout - The number of seconds to wait for a lock before giving up.
+ * @param int $maxlifetime - Unused by this lock type.
+ * @return boolean - true if a lock was obtained.
+ */
+ public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+ $giveuptime = time() + $timeout;
+
+ $token = $this->get_index_from_key($resource);
+
+ $params = array('locktype' => $this->dblockid,
+ 'token' => $token);
+
+ $locked = false;
+
+ do {
+ $result = $this->db->get_record_sql('SELECT pg_try_advisory_lock(:locktype, :token) AS locked', $params);
+ $locked = $result->locked === 't';
+ if (!$locked) {
+ usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+ }
+ // Try until the giveup time.
+ } while (!$locked && time() < $giveuptime);
+
+ if ($locked) {
+ $this->openlocks[$token] = 1;
+ return new lock($token, $this);
+ }
+ return false;
+ }
+
+ /**
+ * Release a lock that was previously obtained with @lock.
+ * @param lock $lock - a lock obtained from this factory.
+ * @return boolean - true if the lock is no longer held (including if it was never held).
+ */
+ public function release_lock(lock $lock) {
+ $params = array('locktype' => $this->dblockid,
+ 'token' => $lock->get_key());
+ $result = $this->db->get_record_sql('SELECT pg_advisory_unlock(:locktype, :token) AS unlocked', $params);
+ $result = $result->unlocked === 't';
+ if ($result) {
+ unset($this->openlocks[$lock->get_key()]);
+ }
+ return $result;
+ }
+
+ /**
+ * Extend a lock that was previously obtained with @lock.
+ * @param lock $lock - a lock obtained from this factory.
+ * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+ * @return boolean - true if the lock was extended.
+ */
+ public function extend_lock(lock $lock, $maxlifetime = 86400) {
+ // Not supported by this factory.
+ return false;
+ }
+
+ /**
+ * Auto release any open locks on shutdown.
+ * This is required, because we may be using persistent DB connections.
+ */
+ public function auto_release() {
+ // Called from the shutdown handler. Must release all open locks.
+ foreach ($this->openlocks as $key => $unused) {
+ $lock = new lock($key, $this);
+ $this->release_lock($lock);
+ }
+ }
+
+}
$this->installedplugins = array();
+ // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
if ($CFG->version < 2013092001.02) {
// We did not upgrade the database yet.
$modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20140112" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20140115" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
+ <TABLE NAME="lock_db" COMMENT="Stores active and inactive lock types for db locking method.">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+ <FIELD NAME="resourcekey" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="String identifying the resource to be locked. Should use frankenstyle format."/>
+ <FIELD NAME="expires" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Expiry time for an active lock."/>
+ <FIELD NAME="owner" TYPE="char" LENGTH="36" NOTNULL="false" SEQUENCE="false" COMMENT="uuid indicating the owner of the lock."/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ </KEYS>
+ <INDEXES>
+ <INDEX NAME="resourcekey_uniq" UNIQUE="true" FIELDS="resourcekey" COMMENT="Unique index for resourcekey"/>
+ <INDEX NAME="expires_idx" UNIQUE="false" FIELDS="expires" COMMENT="Index on expires column"/>
+ <INDEX NAME="owner_idx" UNIQUE="false" FIELDS="owner" COMMENT="Index on owner"/>
+ </INDEXES>
+ </TABLE>
</TABLES>
</XMLDB>
upgrade_main_savepoint(true, 2014011701.00);
}
+ if ($oldversion < 2014012300.01) {
+ // Remove deleted users home pages.
+ $sql = "DELETE FROM {my_pages}
+ WHERE EXISTS (SELECT {user}.id
+ FROM {user}
+ WHERE {user}.id = {my_pages}.userid
+ AND {user}.deleted = 1)
+ AND {my_pages}.private = 1";
+ $DB->execute($sql);
+
+ // Reached main savepoint.
+ upgrade_main_savepoint(true, 2014012300.01);
+ }
+
+ if ($oldversion < 2014012400.00) {
+ // Define table lock_db to be created.
+ $table = new xmldb_table('lock_db');
+
+ // Adding fields to table lock_db.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('resourcekey', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('expires', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+ $table->add_field('owner', XMLDB_TYPE_CHAR, '36', null, null, null, null);
+
+ // Adding keys to table lock_db.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+ // Adding indexes to table lock_db.
+ $table->add_index('resourcekey_uniq', XMLDB_INDEX_UNIQUE, array('resourcekey'));
+ $table->add_index('expires_idx', XMLDB_INDEX_NOTUNIQUE, array('expires'));
+ $table->add_index('owner_idx', XMLDB_INDEX_NOTUNIQUE, array('owner'));
+
+ // Conditionally launch create table for lock_db.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2014012400.00);
+ }
+
return true;
}
/**
* This function will execute an array of SQL commands.
*
- * @param array $sqlarr Array of sql statements to execute.
- * @throws ddl_exception This exception is thrown if any error is found.
+ * @param string[] $sqlarr Array of sql statements to execute.
+ * @throws ddl_change_structure_exception This exception is thrown if any error is found.
*/
protected function execute_sql_arr(array $sqlarr) {
- foreach ($sqlarr as $sql) {
- $this->execute_sql($sql);
- }
+ $this->mdb->change_database_structure($sqlarr);
}
/**
* Execute a given sql command string.
*
* @param string $sql The sql string you wish to be executed.
- * @throws ddl_exception This exception is thrown if any error is found.
+ * @throws ddl_change_structure_exception This exception is thrown if any error is found.
*/
protected function execute_sql($sql) {
- if (!$this->mdb->change_database_structure($sql)) {
- // in case driver does not throw exceptions yet ;-)
- throw new ddl_change_structure_exception($this->mdb->get_last_error(), $sql);
- }
+ $this->mdb->change_database_structure($sql);
}
/**
$sqlarr = parent::getCreateTableSQL($xmldb_table);
- // Let's inject the extra MySQL tweaks.
- foreach ($sqlarr as $i=>$sql) {
- if (strpos($sql, 'CREATE TABLE ') === 0) {
+ // This is a very nasty hack that tries to use just one query per created table
+ // because MySQL is stupidly slow when modifying empty tables.
+ // Note: it is safer to inject everything on new lines because there might be some trailing -- comments.
+ $sqls = array();
+ $prevcreate = null;
+ $matches = null;
+ foreach ($sqlarr as $sql) {
+ if (preg_match('/^CREATE TABLE ([^ ]+)/', $sql, $matches)) {
+ $prevcreate = $matches[1];
+ $sql = preg_replace('/\s*\)\s*$/s', '/*keyblock*/)', $sql);
+ // Let's inject the extra MySQL tweaks here.
if ($engine) {
- $sqlarr[$i] .= " ENGINE = $engine";
+ $sql .= "\n ENGINE = $engine";
}
if ($collation) {
if (strpos($collation, 'utf8_') === 0) {
- $sqlarr[$i] .= " DEFAULT CHARACTER SET utf8";
+ $sql .= "\n DEFAULT CHARACTER SET utf8";
}
- $sqlarr[$i] .= " DEFAULT COLLATE = $collation";
+ $sql .= "\n DEFAULT COLLATE = $collation";
}
+ $sqls[] = $sql;
+ continue;
}
+ if ($prevcreate) {
+ if (preg_match('/^ALTER TABLE '.$prevcreate.' COMMENT=(.*)$/s', $sql, $matches)) {
+ $prev = array_pop($sqls);
+ $prev .= "\n COMMENT=$matches[1]";
+ $sqls[] = $prev;
+ continue;
+ }
+ if (preg_match('/^CREATE INDEX ([^ ]+) ON '.$prevcreate.' (.*)$/s', $sql, $matches)) {
+ $prev = array_pop($sqls);
+ if (strpos($prev, '/*keyblock*/')) {
+ $prev = str_replace('/*keyblock*/', "\n, KEY $matches[1] $matches[2]/*keyblock*/", $prev);
+ $sqls[] = $prev;
+ continue;
+ } else {
+ $sqls[] = $prev;
+ }
+ }
+ if (preg_match('/^CREATE UNIQUE INDEX ([^ ]+) ON '.$prevcreate.' (.*)$/s', $sql, $matches)) {
+ $prev = array_pop($sqls);
+ if (strpos($prev, '/*keyblock*/')) {
+ $prev = str_replace('/*keyblock*/', "\n, UNIQUE KEY $matches[1] $matches[2]/*keyblock*/", $prev);
+ $sqls[] = $prev;
+ continue;
+ } else {
+ $sqls[] = $prev;
+ }
+ }
+ }
+ $prevcreate = null;
+ $sqls[] = $sql;
}
- return $sqlarr;
+ foreach ($sqls as $key => $sql) {
+ $sqls[$key] = str_replace('/*keyblock*/', "\n", $sql);
+ }
+
+ return $sqls;
}
/**
protected $reads = 0;
/** @var int The database writes (performance counter).*/
protected $writes = 0;
+ /** @var float Time queries took to finish, seconds with microseconds.*/
+ protected $queriestime = 0;
/** @var int Debug level. */
protected $debug = 0;
$logerrors = !empty($this->dboptions['logerrors']);
$iserror = ($error !== false);
- $time = microtime(true) - $this->last_time;
+ $time = $this->query_time();
+
+ // Will be shown or not depending on MDL_PERF values rather than in dboptions['log*].
+ $this->queriestime = $this->queriestime + $time;
if ($logall or ($logslow and ($logslow < ($time+0.00001))) or ($iserror and $logerrors)) {
$this->loggingquery = true;
}
}
+ /**
+ * Returns the time elapsed since the query started.
+ * @return float Seconds with microseconds
+ */
+ protected function query_time() {
+ return microtime(true) - $this->last_time;
+ }
+
/**
* Returns database server info array
* @return array Array containing 'description' and 'version' at least.
if (!$this->get_debug()) {
return;
}
- $time = microtime(true) - $this->last_time;
+ $time = $this->query_time();
$message = "Query took: {$time} seconds.\n";
if (CLI_SCRIPT) {
echo $message;
/**
* Do NOT use in code, this is for use by database_manager only!
- * @param string $sql query
+ * @param string|array $sql query or array of queries
* @return bool true
- * @throws dml_exception A DML specific exception is thrown for any errors.
+ * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public abstract function change_database_structure($sql);
*/
public abstract function insert_record($table, $dataobject, $returnid=true, $bulk=false);
+ /**
+ * Insert multiple records into database as fast as possible.
+ *
+ * Order of inserts is maintained, but the operation is not atomic,
+ * use transactions if necessary.
+ *
+ * This method is intended for inserting of large number of small objects,
+ * do not use for huge objects with text or binary fields.
+ *
+ * @since 2.7
+ *
+ * @param string $table The database table to be inserted into
+ * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+ * @return void does not return new record ids
+ *
+ * @throws coding_exception if data objects have different structure
+ * @throws dml_exception A DML specific exception is thrown for any errors.
+ */
+ public function insert_records($table, $dataobjects) {
+ if (!is_array($dataobjects) and !($dataobjects instanceof Traversable)) {
+ throw new coding_exception('insert_records() passed non-traversable object');
+ }
+
+ $fields = null;
+ // Note: override in driver if there is a faster way.
+ foreach ($dataobjects as $dataobject) {
+ if (!is_array($dataobject) and !is_object($dataobject)) {
+ throw new coding_exception('insert_records() passed invalid record object');
+ }
+ $dataobject = (array)$dataobject;
+ if ($fields === null) {
+ $fields = array_keys($dataobject);
+ } else if ($fields !== array_keys($dataobject)) {
+ throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+ }
+ $this->insert_record($table, $dataobject, false);
+ }
+ }
+
/**
* Import a record into a table, id field is required.
* Safety checks are NOT carried out. Lobs are supported.
public function perf_get_queries() {
return $this->writes + $this->reads;
}
+
+ /**
+ * Time waiting for the database engine to finish running all queries.
+ * @return float Number of seconds with microseconds
+ */
+ public function perf_get_queries_time() {
+ return $this->queriestime;
+ }
}
/**
* Do NOT use in code, to be used by database_manager only!
- * @param string $sql query
+ * @param string|array $sql query
* @return bool true
- * @throws dml_exception A DML specific exception is thrown for any errors.
+ * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql) {
- $this->reset_caches();
+ $this->get_manager(); // Includes DDL exceptions classes ;-)
+ $sqls = (array)$sql;
- $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
- $result = mssql_query($sql, $this->mssql);
- $this->query_end($result);
+ try {
+ foreach ($sqls as $sql) {
+ $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+ $result = mssql_query($sql, $this->mssql);
+ $this->query_end($result);
+ }
+ } catch (ddl_change_structure_exception $e) {
+ $this->reset_caches();
+ throw $e;
+ }
+ $this->reset_caches();
return true;
}
*/
class mysqli_native_moodle_database extends moodle_database {
+ /** @var mysqli $mysqli */
protected $mysqli = null;
private $transactions_supported = null;
* Returns detailed information about columns in table. This information is cached internally.
* @param string $table name
* @param bool $usecache
- * @return array array of database_column_info objects indexed with column names
+ * @return database_column_info[] array of database_column_info objects indexed with column names
*/
public function get_columns($table, $usecache=true) {
/**
* Do NOT use in code, to be used by database_manager only!
- * @param string $sql query
+ * @param string|array $sql query
* @return bool true
- * @throws dml_exception A DML specific exception is thrown for any errors.
+ * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql) {
- $this->reset_caches();
+ $this->get_manager(); // Includes DDL exceptions classes ;-)
+ if (is_array($sql)) {
+ $sql = implode("\n;\n", $sql);
+ }
- $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
- $result = $this->mysqli->query($sql);
- $this->query_end($result);
+ try {
+ $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+ $result = $this->mysqli->multi_query($sql);
+ if ($result === false) {
+ $this->query_end(false);
+ }
+ while ($this->mysqli->more_results()) {
+ $result = $this->mysqli->next_result();
+ if ($result === false) {
+ $this->query_end(false);
+ }
+ }
+ $this->query_end(true);
+ } catch (ddl_change_structure_exception $e) {
+ while (@$this->mysqli->more_results()) {
+ @$this->mysqli->next_result();
+ }
+ $this->reset_caches();
+ throw $e;
+ }
+ $this->reset_caches();
return true;
}
return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
}
+ /**
+ * Insert multiple records into database as fast as possible.
+ *
+ * Order of inserts is maintained, but the operation is not atomic,
+ * use transactions if necessary.
+ *
+ * This method is intended for inserting of large number of small objects,
+ * do not use for huge objects with text or binary fields.
+ *
+ * @since 2.7
+ *
+ * @param string $table The database table to be inserted into
+ * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+ * @return void does not return new record ids
+ *
+ * @throws coding_exception if data objects have different structure
+ * @throws dml_exception A DML specific exception is thrown for any errors.
+ */
+ public function insert_records($table, $dataobjects) {
+ if (!is_array($dataobjects) and !$dataobjects instanceof Traversable) {
+ throw new coding_exception('insert_records() passed non-traversable object');
+ }
+
+ // MySQL has a relatively small query length limit by default,
+ // make sure 'max_allowed_packet' in my.cnf is high enough
+ // if you change the following default...
+ static $chunksize = null;
+ if ($chunksize === null) {
+ if (!empty($this->dboptions['bulkinsertsize'])) {
+ $chunksize = (int)$this->dboptions['bulkinsertsize'];
+
+ } else {
+ if (PHP_INT_SIZE === 4) {
+ // Bad luck for Windows, we cannot do any maths with large numbers.
+ $chunksize = 5;
+ } else {
+ $sql = "SHOW VARIABLES LIKE 'max_allowed_packet'";
+ $this->query_start($sql, null, SQL_QUERY_AUX);
+ $result = $this->mysqli->query($sql);
+ $this->query_end($result);
+ $size = 0;
+ if ($rec = $result->fetch_assoc()) {
+ $size = $rec['Value'];
+ }
+ $result->close();
+ // Hopefully 200kb per object are enough.
+ $chunksize = (int)($size / 200000);
+ if ($chunksize > 50) {
+ $chunksize = 50;
+ }
+ }
+ }
+ }
+
+ $columns = $this->get_columns($table, true);
+ $fields = null;
+ $count = 0;
+ $chunk = array();
+ foreach ($dataobjects as $dataobject) {
+ if (!is_array($dataobject) and !is_object($dataobject)) {
+ throw new coding_exception('insert_records() passed invalid record object');
+ }
+ $dataobject = (array)$dataobject;
+ if ($fields === null) {
+ $fields = array_keys($dataobject);
+ $columns = array_intersect_key($columns, $dataobject);
+ unset($columns['id']);
+ } else if ($fields !== array_keys($dataobject)) {
+ throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+ }
+
+ $count++;
+ $chunk[] = $dataobject;
+
+ if ($count === $chunksize) {
+ $this->insert_chunk($table, $chunk, $columns);
+ $chunk = array();
+ $count = 0;
+ }
+ }
+
+ if ($count) {
+ $this->insert_chunk($table, $chunk, $columns);
+ }
+ }
+
+ /**
+ * Insert records in chunks.
+ *
+ * Note: can be used only from insert_records().
+ *
+ * @param string $table
+ * @param array $chunk
+ * @param database_column_info[] $columns
+ */
+ protected function insert_chunk($table, array $chunk, array $columns) {
+ $fieldssql = '('.implode(',', array_keys($columns)).')';
+
+ $valuessql = '('.implode(',', array_fill(0, count($columns), '?')).')';
+ $valuessql = implode(',', array_fill(0, count($chunk), $valuessql));
+
+ $params = array();
+ foreach ($chunk as $dataobject) {
+ foreach ($columns as $field => $column) {
+ $params[] = $this->normalise_value($column, $dataobject[$field]);
+ }
+ }
+
+ $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
+
+ list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
+ $rawsql = $this->emulate_bound_params($sql, $params);
+
+ $this->query_start($sql, $params, SQL_QUERY_INSERT);
+ $result = $this->mysqli->query($rawsql);
+ $this->query_end($result);
+ }
+
/**
* Import a record into a table, id field is required.
* Safety checks are NOT carried out. Lobs are supported.
/**
* Do NOT use in code, to be used by database_manager only!
- * @param string $sql query
+ * @param string|array $sql query
* @return bool true
- * @throws dml_exception A DML specific exception is thrown for any errors.
+ * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql) {
- $this->reset_caches();
-
- $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
- $stmt = $this->parse_query($sql);
- $result = oci_execute($stmt, $this->commit_status);
- $this->query_end($result, $stmt);
- oci_free_statement($stmt);
+ $this->get_manager(); // Includes DDL exceptions classes ;-)
+ $sqls = (array)$sql;
+
+ try {
+ foreach ($sqls as $sql) {
+ $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+ $stmt = $this->parse_query($sql);
+ $result = oci_execute($stmt, $this->commit_status);
+ $this->query_end($result, $stmt);
+ oci_free_statement($stmt);
+ }
+ } catch (ddl_change_structure_exception $e) {
+ $this->reset_caches();
+ throw $e;
+ }
+ $this->reset_caches();
return true;
}
/**
* Do NOT use in code, to be used by database_manager only!
- * @param string $sql query
- * @return bool success
+ * @param string|array $sql query
+ * @return bool true
+ * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql) {
- $result = true;
- $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+ $this->get_manager(); // Includes DDL exceptions classes ;-)
+ $sqls = (array)$sql;
try {
- $this->pdb->exec($sql);
+ foreach ($sqls as $sql) {
+ $result = true;
+ $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+
+ try {
+ $this->pdb->exec($sql);
+ } catch (PDOException $ex) {
+ $this->lastError = $ex->getMessage();
+ $result = false;
+ }
+ $this->query_end($result);
+ }
+ } catch (ddl_change_structure_exception $e) {
$this->reset_caches();
- } catch (PDOException $ex) {
- $this->lastError = $ex->getMessage();
- $result = false;
+ throw $e;
}
- $this->query_end($result);
- return $result;
+
+ $this->reset_caches();
+ return true;
}
public function delete_records_select($table, $select, array $params=null) {
*/
class pgsql_native_moodle_database extends moodle_database {
+ /** @var resource $pgsql database resource */
protected $pgsql = null;
protected $bytea_oid = null;
* Returns detailed information about columns in table. This information is cached internally.
* @param string $table name
* @param bool $usecache
- * @return array array of database_column_info objects indexed with column names
+ * @return database_column_info[] array of database_column_info objects indexed with column names
*/
public function get_columns($table, $usecache=true) {
if ($usecache) {
/**
* Do NOT use in code, to be used by database_manager only!
- * @param string $sql query
+ * @param string|array $sql query
* @return bool true
- * @throws dml_exception A DML specific exception is thrown for any errors.
+ * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql) {
- $this->reset_caches();
+ $this->get_manager(); // Includes DDL exceptions classes ;-)
+ if (is_array($sql)) {
+ $sql = implode("\n;\n", $sql);
+ }
+ if (!$this->is_transaction_started()) {
+ // It is better to do all or nothing, this helps with recovery...
+ $sql = "BEGIN ISOLATION LEVEL SERIALIZABLE;\n$sql\n; COMMIT";
+ }
- $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
- $result = pg_query($this->pgsql, $sql);
- $this->query_end($result);
+ try {
+ $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+ $result = pg_query($this->pgsql, $sql);
+ $this->query_end($result);
+ pg_free_result($result);
+ } catch (ddl_change_structure_exception $e) {
+ if (!$this->is_transaction_started()) {
+ $result = @pg_query($this->pgsql, "ROLLBACK");
+ @pg_free_result($result);
+ }
+ $this->reset_caches();
+ throw $e;
+ }
- pg_free_result($result);
+ $this->reset_caches();
return true;
}
}
+ /**
+ * Insert multiple records into database as fast as possible.
+ *
+ * Order of inserts is maintained, but the operation is not atomic,
+ * use transactions if necessary.
+ *
+ * This method is intended for inserting of large number of small objects,
+ * do not use for huge objects with text or binary fields.
+ *
+ * @since 2.7
+ *
+ * @param string $table The database table to be inserted into
+ * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+ * @return void does not return new record ids
+ *
+ * @throws coding_exception if data objects have different structure
+ * @throws dml_exception A DML specific exception is thrown for any errors.
+ */
+ public function insert_records($table, $dataobjects) {
+ if (!is_array($dataobjects) and !($dataobjects instanceof Traversable)) {
+ throw new coding_exception('insert_records() passed non-traversable object');
+ }
+
+ // PostgreSQL does not seem to have problems with huge queries.
+ $chunksize = 500;
+ if (!empty($this->dboptions['bulkinsertsize'])) {
+ $chunksize = (int)$this->dboptions['bulkinsertsize'];
+ }
+
+ $columns = $this->get_columns($table, true);
+
+ // Make sure there are no nasty blobs!
+ foreach ($columns as $column) {
+ if ($column->binary) {
+ parent::insert_records($table, $dataobjects);
+ return;
+ }
+ }
+
+ $fields = null;
+ $count = 0;
+ $chunk = array();
+ foreach ($dataobjects as $dataobject) {
+ if (!is_array($dataobject) and !is_object($dataobject)) {
+ throw new coding_exception('insert_records() passed invalid record object');
+ }
+ $dataobject = (array)$dataobject;
+ if ($fields === null) {
+ $fields = array_keys($dataobject);
+ $columns = array_intersect_key($columns, $dataobject);
+ unset($columns['id']);
+ } else if ($fields !== array_keys($dataobject)) {
+ throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+ }
+
+ $count++;
+ $chunk[] = $dataobject;
+
+ if ($count === $chunksize) {
+ $this->insert_chunk($table, $chunk, $columns);
+ $chunk = array();
+ $count = 0;
+ }
+ }
+
+ if ($count) {
+ $this->insert_chunk($table, $chunk, $columns);
+ }
+ }
+
+ /**
+ * Insert records in chunks, no binary support, strict param types...
+ *
+ * Note: can be used only from insert_records().
+ *
+ * @param string $table
+ * @param array $chunk
+ * @param database_column_info[] $columns
+ */
+ protected function insert_chunk($table, array $chunk, array $columns) {
+ $i = 1;
+ $params = array();
+ $values = array();
+ foreach ($chunk as $dataobject) {
+ $vals = array();
+ foreach ($columns as $field => $column) {
+ $params[] = $this->normalise_value($column, $dataobject[$field]);
+ $vals[] = "\$".$i++;
+ }
+ $values[] = '('.implode(',', $vals).')';
+ }
+
+ $fieldssql = '('.implode(',', array_keys($columns)).')';
+ $valuessql = implode(',', $values);
+
+ $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
+ $this->query_start($sql, $params, SQL_QUERY_INSERT);
+ $result = pg_query_params($this->pgsql, $sql, $params);
+ $this->query_end($result);
+ pg_free_result($result);
+ }
+
/**
* Import a record into a table, id field is required.
* Safety checks are NOT carried out. Lobs are supported.
/**
* Do NOT use in code, to be used by database_manager only!
- * @param string $sql query
+ * @param string|array $sql query
* @return bool true
- * @throws dml_exception A DML specific exception is thrown for any errors.
+ * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql) {
- $this->reset_caches();
-
- $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
- $result = sqlsrv_query($this->sqlsrv, $sql);
- $this->query_end($result);
+ $this->get_manager(); // Includes DDL exceptions classes ;-)
+ $sqls = (array)$sql;
+
+ try {
+ foreach ($sqls as $sql) {
+ $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+ $result = sqlsrv_query($this->sqlsrv, $sql);
+ $this->query_end($result);
+ }
+ } catch (ddl_change_structure_exception $e) {
+ $this->reset_caches();
+ throw $e;
+ }
+ $this->reset_caches();
return true;
}
}
}
+ public function test_insert_records() {
+ $DB = $this->tdb;
+ $dbman = $DB->get_manager();
+
+ $table = $this->get_test_table();
+ $tablename = $table->getName();
+
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
+ $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
+ $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
+ $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $dbman->create_table($table);
+
+ $this->assertCount(0, $DB->get_records($tablename));
+
+ $record = new stdClass();
+ $record->id = '1';
+ $record->course = '1';
+ $record->oneint = null;
+ $record->onenum = '1.00';
+ $record->onechar = 'a';
+ $record->onetext = 'aaa';
+
+ $expected = array();
+ $records = array();
+ for ($i = 1; $i <= 2000; $i++) { // This may take a while, it should be higher than defaults in DML drivers.
+ $rec = clone($record);
+ $rec->id = (string)$i;
+ $rec->oneint = (string)$i;
+ $expected[$i] = $rec;
+ $rec = clone($rec);
+ unset($rec->id);
+ $records[$i] = $rec;
+ }
+
+ $DB->insert_records($tablename, $records);
+ $stored = $DB->get_records($tablename, array(), 'id ASC');
+ $this->assertEquals($expected, $stored);
+
+ // Test there can be some extra properties including id.
+ $count = $DB->count_records($tablename);
+ $rec1 = (array)$record;
+ $rec1['xxx'] = 1;
+ $rec2 = (array)$record;
+ $rec2['xxx'] = 2;
+
+ $records = array($rec1, $rec2);
+ $DB->insert_records($tablename, $records);
+ $this->assertEquals($count + 2, $DB->count_records($tablename));
+
+ // Test not all properties are necessary.
+ $rec1 = (array)$record;
+ unset($rec1['course']);
+ $rec2 = (array)$record;
+ unset($rec2['course']);
+
+ $records = array($rec1, $rec2);
+ $DB->insert_records($tablename, $records);
+
+ // Make sure no changes in data object structure are tolerated.
+ $rec1 = (array)$record;
+ unset($rec1['id']);
+ $rec2 = (array)$record;
+ unset($rec2['id']);
+
+ $records = array($rec1, $rec2);
+ $DB->insert_records($tablename, $records);
+
+ $rec2['xx'] = '1';
+ $records = array($rec1, $rec2);
+ try {
+ $DB->insert_records($tablename, $records);
+ $this->fail('coding_exception expected when insert_records receives different object data structures');
+ } catch (moodle_exception $e) {
+ $this->assertInstanceOf('coding_exception', $e);
+ }
+
+ unset($rec2['xx']);
+ unset($rec2['course']);
+ $rec2['course'] = '1';
+ $records = array($rec1, $rec2);
+ try {
+ $DB->insert_records($tablename, $records);
+ $this->fail('coding_exception expected when insert_records receives different object data structures');
+ } catch (moodle_exception $e) {
+ $this->assertInstanceOf('coding_exception', $e);
+ }
+
+ $records = 1;
+ try {
+ $DB->insert_records($tablename, $records);
+ $this->fail('coding_exception expected when insert_records receives non-traversable data');
+ } catch (moodle_exception $e) {
+ $this->assertInstanceOf('coding_exception', $e);
+ }
+
+ $records = array(1);
+ try {
+ $DB->insert_records($tablename, $records);
+ $this->fail('coding_exception expected when insert_records receives non-objet record');
+ } catch (moodle_exception $e) {
+ $this->assertInstanceOf('coding_exception', $e);
+ }
+ }
+
public function test_import_record() {
// All the information in this test is fetched from DB by get_recordset() so we
// have such method properly tested against nulls, empties and friends...
'Failed to close output file: ' . $this->currentfile);
}
- // Update modified time.
- touch($this->currentfile, $this->currentmtime);
+ // At this point we should touch the file to set its modified
+ // time to $this->currentmtime. However, when extracting to the
+ // temp directory, cron will delete files more than a week old,
+ // so to avoid problems we leave all files at their current time.
}
if ($this->currentarchivepath === tgz_packer::ARCHIVE_INDEX_FILE) {
*/
function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
global $CFG, $PAGE, $SITE;
- $issite = (is_object($courseorid) and $courseorid->id == SITEID)
- or (!is_object($courseorid) and $courseorid == SITEID);
+ $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
+ or (!is_object($courseorid) and $courseorid == SITEID));
if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
// Note: nearly all pages call get_fast_modinfo anyway and it does not make any
// db queries so this is not really a performance concern, however it is obviously
* @return bool Always returns true
*/
function update_user_login_times() {
- global $USER, $DB, $CFG;
-
- require_once($CFG->dirroot.'/user/lib.php');
+ global $USER, $DB;
if (isguestuser()) {
// Do not update guest access times/ips for performance.
$USER->lastaccess = $user->lastaccess = $now;
$USER->lastip = $user->lastip = getremoteaddr();
- user_update_user($user, false);
+ // Note: do not call user_update_user() here because this is part of the login process,
+ // the login event means that these fields were updated.
+ $DB->update_record('user', $user);
return true;
}
// Remove users private keys.
$DB->delete_records('user_private_key', array('userid' => $user->id));
+ // Remove users customised pages.
+ $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
+
// Force logout - may fail if file based sessions used, sorry.
\core\session\manager::kill_user_sessions($user->id);
$info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
$info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
+ $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
+ $info['html'] .= '<span class="dbtime">DB queries time: '.$info['dbtime'].' secs</span> ';
+ $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
+
if (function_exists('posix_times')) {
$ptimes = posix_times();
if (is_array($ptimes)) {
<li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&warnp2n3e=1&url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
</ul></div>';
}
- if (!empty($CFG->additionalhtmlfooter)) {
- $output .= "\n".$CFG->additionalhtmlfooter;
- }
return $output;
}
/**
* The standard tags (typically script tags that are not needed earlier) that
- * should be output after everything else, . Designed to be called in theme layout.php files.
+ * should be output after everything else. Designed to be called in theme layout.php files.
*
* @return string HTML fragment.
*/
public function standard_end_of_body_html() {
+ global $CFG;
+
// This function is normally called from a layout.php file in {@link core_renderer::header()}
// but some of the content won't be known until later, so we return a placeholder
// for now. This will be replaced with the real content in {@link core_renderer::footer()}.
- return $this->unique_end_html_token;
+ $output = '';
+ if (!empty($CFG->additionalhtmlfooter)) {
+ $output .= "\n".$CFG->additionalhtmlfooter;
+ }
+ $output .= $this->unique_end_html_token;
+ return $output;
}
/**
/** reduction factor for small font */
define('K_SMALL_RATIO', 2/3);
+/** Throw exceptions from errors so they can be caught and recovered from. */
+define('K_TCPDF_THROW_EXCEPTION_ERROR', true);
+
require_once(dirname(__FILE__).'/tcpdf/tcpdf.php');
/**
$this->assertEquals($expected, $legacydata, $message);
}
+ /**
+ * Assert that an event is not using event->contxet.
+ * While restoring context might not be valid and it should not be used by event url
+ * or description methods.
+ *
+ * @param \core\event\base $event the event object.
+ * @param string $message
+ * @return void
+ */
+ public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
+ // Save current event->context and set it to false.
+ $eventcontext = phpunit_event_mock::testable_get_event_context($event);
+ phpunit_event_mock::testable_set_event_context($event, false);
+ if ($message === '') {
+ $message = 'Event should not use context property of event in any method.';
+ }
+
+ // Test event methods should not use event->context.
+ $event->get_url();
+ $event->get_description();
+ $event->get_legacy_eventname();
+ phpunit_event_mock::testable_get_legacy_eventdata($event);
+ phpunit_event_mock::testable_get_legacy_logdata($event);
+
+ // Restore event->context.
+ phpunit_event_mock::testable_set_event_context($event, $eventcontext);
+ }
+
/**
* Stores current time as the base for assertTimeCurrent().
*
/**
* Returns the log data of the event.
+ *
+ * @param \core\event\base $event event to get legacy eventdata from.
* @return array
*/
public static function testable_get_legacy_eventdata($event) {
/**
* Returns the log data of the event.
+ *
+ * @param \core\event\base $event event to get legacy logdata from.
* @return array
*/
public static function testable_get_legacy_logdata($event) {
return $event->get_legacy_logdata();
}
+ /**
+ * Returns event context.
+ *
+ * @param \core\event\base $event event to get context for.
+ * @return context event context
+ */
+ public static function testable_get_event_context($event) {
+ return $event->context;
+ }
+
+ /**
+ * Sets event context.
+ *
+ * @param \core\event\base $event event to set context for.
+ * @param context $context context to set.
+ */
+ public static function testable_set_event_context($event, $context) {
+ $event->context = $context;
+ }
}
*/
function redirect_if_major_upgrade_required() {
global $CFG;
- $lastmajordbchanges = 2013100400.02;
+ $lastmajordbchanges = 2014012400.00;
if (empty($CFG->version) or (float)$CFG->version < $lastmajordbchanges or
during_initial_install() or !empty($CFG->adminsetuppending)) {
try {
$this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
$this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
$this->assertNotEmpty($ACCESSLIB_PRIVATE->accessdatabyuser);
- accesslib_clear_all_caches(true);
+ accesslib_clear_all_caches_for_unit_testing();
$this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
$this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
$this->assertEmpty($ACCESSLIB_PRIVATE->dirtycontexts);
unassign_capability('moodle/site:accessallgroups', $allroles['teacher'], $frontpagecontext->id, true);
unset($rc);
- accesslib_clear_all_caches(false); // Must be done after assign_capability().
+ accesslib_clear_all_caches_for_unit_testing(); // Must be done after assign_capability().
// Test role_assign(), role_unassign(), role_unassign_all() functions.
$this->assertEquals(0, $DB->count_records('role_assignments', array('contextid'=>$context->id)));
unset($context);
- accesslib_clear_all_caches(false); // Just in case.
+ accesslib_clear_all_caches_for_unit_testing(); // Just in case.
// Test has_capability(), get_users_by_capability(), role_switch(), reload_all_capabilities() and friends functions.
assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $systemcontext, true);
- accesslib_clear_all_caches(false); // Must be done after assign_capability().
+ accesslib_clear_all_caches_for_unit_testing(); /// Must be done after assign_capability().
// Extra tests for guests and not-logged-in users because they can not be verified by cross checking
// with get_users_by_capability() where they are ignored.
unset($permissions);
unset($roles);
- accesslib_clear_all_caches(false); // Must be done after assign_capability().
+ accesslib_clear_all_caches_for_unit_testing(); // must be done after assign_capability().
// Test time - let's set up some real user, just in case the logic for USER affects the others...
$USER = $DB->get_record('user', array('id'=>$testusers[3]));
// All the run screenshots in the same parent dir.
if (!$screenshotsdirname = self::get_run_screenshots_dir()) {
- $screenshotsdirname = self::$screenshotsdirname = date('Ymd_Hi');
+ $screenshotsdirname = self::$screenshotsdirname = date('Ymd_His');
$dir = $CFG->behat_screenshots_path . DIRECTORY_SEPARATOR . $screenshotsdirname;
$this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
$this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
$this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
- $this->assertEquals(context_module::instance($forum->id), $event->get_context());
+ $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
$this->assertEquals($USER->id, $event->userid);
$this->assertEquals($this->user->id, $event->other['relateduserid']);
$this->assertInstanceOf('moodle_url', $event->get_url());
* Test the countall function
*/
public function test_count_all() {
- // There should be just the default category.
- $this->assertEquals(1, coursecat::count_all());
+ global $DB;
+ // Dont assume there is just one. An add-on might create a category as part of the install.
+ $numcategories = $DB->count_records('course_categories');
+ $this->assertEquals($numcategories, coursecat::count_all());
$category1 = coursecat::create(array('name' => 'Cat1'));
$category2 = coursecat::create(array('name' => 'Cat2', 'parent' => $category1->id));
$category3 = coursecat::create(array('name' => 'Cat3', 'parent' => $category2->id, 'visible' => 0));
- // Now we've got four.
- $this->assertEquals(4, coursecat::count_all());
+ // Now we've got three more.
+ $this->assertEquals($numcategories + 3, coursecat::count_all());
cache_helper::purge_by_event('changesincoursecat');
// We should still have 4.
- $this->assertEquals(4, coursecat::count_all());
+ $this->assertEquals($numcategories + 3, coursecat::count_all());
}
/**
$result = $sink->get_events();
$event = $result[1];
$this->assertEventLegacyLogData(null, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$pageevent = \core_tests\event\content_viewed::create();
$pageevent->set_page_detail();
$pageevent->trigger();
+ $this->assertEventContextNotUsed($pageevent);
}
}
$this->assertEventLegacyLogData($legacydata, $event);
$url = new moodle_url('/mod/unittests/index.php', array('id' => $course->id));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
$this->assertSame('feedback', $event->objecttable);
$url = new moodle_url('/mod/feedback/view.php', array('id' => $cm->id));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
$this->assertSame($event->get_data(), $data);
}
+
+ /**
+ * @expectedException PHPUnit_Framework_Error_Notice
+ */
+ public function test_context_not_used() {
+ $event = \core_tests\event\context_used_in_event::create(array('courseid' => 1, 'other' => array('sample' => 1, 'xx' => 10)));
+ $this->assertEventContextNotUsed($event);
+
+ $eventcontext = phpunit_event_mock::testable_get_event_context($event);
+ phpunit_event_mock::testable_set_event_context($event, null);
+ $this->assertEventContextNotUsed($event);
+ }
}
$this->assertEquals(context_coursecat::instance($category->id), $event->get_context());
$expected = array(SITEID, 'category', 'add', 'editcategory.php?id=' . $category->id, $category->id);
$this->assertEventLegacyLogData($expected, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEquals(context_coursecat::instance($category2->id), $event->get_context());
$expected = array(SITEID, 'category', 'show', 'editcategory.php?id=' . $category2->id, $category2->id);
$this->assertEventLegacyLogData($expected, $event);
+ $this->assertEventContextNotUsed($event);
}
/**
$this->assertEquals(context_system::instance(), $event->get_context());
$expected = array(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: The email failed to send!');
$this->assertEventLegacyLogData($expected, $event);
+ $this->assertEventContextNotUsed($event);
}
}
class course_module_viewed_noinit extends \core\event\course_module_viewed {
}
+/**
+ * Event to test context used in event functions
+ */
+class context_used_in_event extends \core\event\base {
+ public function get_description() {
+ return $this->context->instanceid . " Description";
+ }
+
+ protected function init() {
+ $this->data['crud'] = 'u';
+ $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+ $this->context = \context_system::instance();
+ }
+
+ public function get_url() {
+ return new \moodle_url('/somepath/somefile.php', array('id' => $this->context->instanceid));
+ }
+
+ protected function get_legacy_eventdata() {
+ return array($this->data['courseid'], $this->context->instanceid);
+ }
+
+ protected function get_legacy_logdata() {
+ return array($this->data['courseid'], 'core_unittest', 'view', 'unittest.php?id=' . $this->context->instanceid);
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * lock unit tests
+ *
+ * @package core
+ * @category lock
+ * @copyright 2013 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Unit tests for our locking configuration.
+ *
+ * @package core
+ * @category lock
+ * @copyright 2013 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_config_testcase extends advanced_testcase {
+
+ /**
+ * Tests the static parse charset method
+ * @return void
+ */
+ public function test_lock_config() {
+ global $CFG;
+ $original = null;
+ if (isset($CFG->lock_factory)) {
+ $original = $CFG->lock_factory;
+ }
+
+ // Test no configuration.
+ unset($CFG->lock_factory);
+
+ $factory = \core\lock\lock_config::get_lock_factory('cache');
+
+ $this->assertNotEmpty($factory, 'Get a default factory with no configuration');
+
+ $CFG->lock_factory = '\core\lock\file_lock_factory';
+
+ $factory = \core\lock\lock_config::get_lock_factory('cache');
+ $this->assertTrue($factory instanceof \core\lock\file_lock_factory,
+ 'Get a default factory with a set configuration');
+
+ $CFG->lock_factory = '\core\lock\db_record_lock_factory';
+
+ $factory = \core\lock\lock_config::get_lock_factory('cache');
+ $this->assertTrue($factory instanceof \core\lock\db_record_lock_factory,
+ 'Get a default factory with a changed configuration');
+
+ if ($original) {
+ $CFG->lock_factory = $original;
+ } else {
+ unset($CFG->lock_factory);
+ }
+ }
+}
+
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * lock unit tests
+ *
+ * @package core
+ * @category test
+ * @copyright 2013 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Unit tests for our locking implementations.
+ *
+ * @package core
+ * @category test
+ * @copyright 2013 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_testcase extends advanced_testcase {
+
+ /**
+ * Some lock types will store data in the database.
+ */
+ protected function setUp() {
+ $this->resetAfterTest(true);
+ }
+
+ /**
+ * Run a suite of tests on a lock factory.
+ * @param \core\lock\lock_factory $lockfactory - A lock factory to test
+ */
+ protected function run_on_lock_factory(\core\lock\lock_factory $lockfactory) {
+
+ if ($lockfactory->is_available()) {
+ // This should work.
+ $lock1 = $lockfactory->get_lock('abc', 2);
+ $this->assertNotEmpty($lock1, 'Get a lock');
+
+ if ($lockfactory->supports_timeout()) {
+ if ($lockfactory->supports_recursion()) {
+ $lock2 = $lockfactory->get_lock('abc', 2);
+ $this->assertNotEmpty($lock2, 'Get a stacked lock');
+ $this->assertTrue($lock2->release(), 'Release a stacked lock');
+ } else {
+ // This should timeout.
+ $lock2 = $lockfactory->get_lock('abc', 2);
+ $this->assertFalse($lock2, 'Cannot get a stacked lock');
+ }
+ }
+ // Release the lock.
+ $this->assertTrue($lock1->release(), 'Release a lock');
+ // Get it again.
+ $lock3 = $lockfactory->get_lock('abc', 2);
+
+ $this->assertNotEmpty($lock3, 'Get a lock again');
+ // Release the lock again.
+ $this->assertTrue($lock3->release(), 'Release a lock again');
+ // Release the lock again (shouldn't hurt).
+ $this->assertFalse($lock3->release(), 'Release a lock that is not held');
+ if (!$lockfactory->supports_auto_release()) {
+ // Test that a lock can be claimed after the timeout period.
+ $lock4 = $lockfactory->get_lock('abc', 2, 2);
+ $this->assertNotEmpty($lock4, 'Get a lock');
+ sleep(3);
+
+ $lock5 = $lockfactory->get_lock('abc', 2, 2);
+ $this->assertNotEmpty($lock5, 'Get another lock after a timeout');
+ $this->assertTrue($lock5->release(), 'Release the lock');
+ $this->assertTrue($lock4->release(), 'Release the lock');
+ }
+ }
+ }
+
+ /**
+ * Tests the testable lock factories.
+ * @return void
+ */
+ public function test_locks() {
+ // Run the suite on the current configured default (may be non-core).
+ $defaultfactory = \core\lock\lock_config::get_lock_factory('default');
+ $this->run_on_lock_factory($defaultfactory);
+
+ // Manually create the core no-configuration factories.
+ $dblockfactory = new \core\lock\db_record_lock_factory('test');
+ $this->run_on_lock_factory($dblockfactory);
+
+ $filelockfactory = new \core\lock\file_lock_factory('test');
+ $this->run_on_lock_factory($filelockfactory);
+
+ }
+
+}
+
// however mod_quiz doesn't have a data generator.
// Instead we're going to use backup notifications and give and take away the capability at various levels.
$assign = $this->getDataGenerator()->create_module('assign', array('course'=>$course->id));
- $modulecontext = context_module::instance($assign->id);
+ $modulecontext = context_module::instance($assign->cmid);
// Create and enrol a teacher.
$teacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'), '*', MUST_EXIST);
// They should now be able to see the backup message.
assign_capability('moodle/site:config', CAP_ALLOW, $teacherrole->id, $modulecontext->id, true);
accesslib_clear_all_caches_for_unit_testing();
- $modulecontext = context_module::instance($assign->id);
+ $modulecontext = context_module::instance($assign->cmid);
$this->assertTrue(has_capability('moodle/site:config', $modulecontext));
$providers = message_get_providers_for_user($teacher->id);
// They should not be able to see the backup message.
assign_capability('moodle/site:config', CAP_PROHIBIT, $teacherrole->id, $coursecontext->id, true);
accesslib_clear_all_caches_for_unit_testing();
- $modulecontext = context_module::instance($assign->id);
+ $modulecontext = context_module::instance($assign->cmid);
$this->assertFalse(has_capability('moodle/site:config', $modulecontext));
$providers = message_get_providers_for_user($teacher->id);
$this->assertSame($eventdata['other']['picture'], $user->picture);
$this->assertSame($eventdata['other']['mnethostid'], $user->mnethostid);
$this->assertEquals($user, $event->get_record_snapshot('user', $event->objectid));
+ $this->assertEventContextNotUsed($event);
// Try invalid params.
$record = new stdClass();
$events = $sink->get_events();
$sink->close();
- $this->assertCount(2, $events);
- $event = $events[0];
- $this->assertInstanceOf('\core\event\user_updated', $event);
- $event = $events[1];
+ $this->assertCount(1, $events);
+ $event = reset($events);
$this->assertInstanceOf('\core\event\user_loggedin', $event);
$this->assertEquals('user', $event->objecttable);
$this->assertEquals($user->id, $event->objectid);
$this->assertEquals(context_system::instance()->id, $event->contextid);
+ $this->assertEventContextNotUsed($event);
$user = $DB->get_record('user', array('id'=>$user->id));
$this->assertTimeCurrent($USER->firstaccess);
$this->assertTimeCurrent($USER->lastaccess);
- $this->assertTimeCurrent($USER->timemodified);
$this->assertTimeCurrent($USER->currentlogin);
$this->assertSame(sesskey(), $USER->sesskey);
$this->assertTimeCurrent($USER->preference['_lastloaded']);
$expectedlogdata = array(SITEID, 'user', 'logout', 'view.php?id='.$event->objectid.'&course='.SITEID, $event->objectid, 0,
$event->objectid);
$this->assertEventLegacyLogData($expectedlogdata, $event);
+ $this->assertEventContextNotUsed($event);
}
public function test_email_to_user() {
$this->assertEquals(context_user::instance($user->id), $event->get_context());
$expectedlogdata = array(SITEID, 'user', 'update', 'view.php?id='.$user->id, '');
$this->assertEventLegacyLogData($expectedlogdata, $event);
+ $this->assertEventContextNotUsed($event);
}
}
* $core_renderer->block_move_target() changed to support more verbose move-block-here descriptions.
DEPRECATIONS:
+* $module uses in mod/xxx/version.php files is now deprecated. Please use $plugin instead. It will be removed in Moodle 2.10.
* Update init methods in all event classes - "level" property was renamed to "edulevel", the level property is now deprecated.
* Abstract class \core\event\course_module_instances_list_viewed is deprecated now, use \core\event\instances_list_viewed instead.
* mod_book\event\instances_list_viewed has been deprecated. Please use mod_book\event\course_module_instance_list_viewed instead.
* The findChildNodes global function has been deprecated. Y.all should
be used instead.
+* New locking api and admin settings to configure the system locking type.
+
=== 2.6 ===
* Use new methods from core_component class instead of get_core_subsystems(), get_plugin_types(),
throw new plugin_defective_exception($component, 'Missing version.php');
}
+ // TODO: Support for $module will end with Moodle 2.10 by MDL-43896. Was deprecated for Moodle 2.7 by MDL-43040.
$plugin = new stdClass();
$plugin->version = null;
$module = $plugin;
- require($fullmod .'/version.php'); // Defines $module/$plugin with version etc.
+ require($fullmod .'/version.php'); // Defines $plugin with version etc.
$plugin = clone($module);
unset($module->version);
unset($module->component);
}
}
+ // If the dialogue is larger than a reasonable minimum height, we
+ // disable the page scrollbars.
+ if (newheight > this.get('minheight')) {
+ // Disable the page scrollbars.
+ if (this.panel.lockScroll && !this.panel.lockScroll.isActive()) {
+ this.panel.lockScroll.enableScrollLock();
+ }
+ } else {
+ // Re-enable the page scrollbars.
+ if (this.panel.lockScroll && this.panel.lockScroll.isActive()) {
+ this.panel.lockScroll.disableScrollLock();
+ }
+ }
+
// Take off 15px top and bottom for borders, plus 40px each for the title and button area before setting the
// new max-height
totalheight = newheight;
--- /dev/null
+{
+ "name": "moodle-core-lockscroll",
+ "builds": {
+ "moodle-core-lockscroll": {
+ "jsfiles": [
+ "lockscroll.js"
+ ]
+ }
+ }
+}
--- /dev/null
+/**
+ * Provides the ability to lock the scroll for a page, allowing nested
+ * locking.
+ *
+ * @module moodle-core-lockscroll
+ */
+
+/**
+ * Provides the ability to lock the scroll for a page.
+ *
+ * This is achieved by applying the class 'lockscroll' to the body Node.
+ *
+ * Nested widgets are also supported and the scroll lock is only removed
+ * when the final plugin instance is disabled.
+ *
+ * @class M.core.LockScroll
+ * @extends Plugin.Base
+ */
+Y.namespace('M.core').LockScroll = Y.Base.create('lockScroll', Y.Plugin.Base, [], {
+
+ /**
+ * Whether the LockScroll has been activated.
+ *
+ * @property _enabled
+ * @type Boolean
+ * @protected
+ */
+ _enabled: false,
+
+ /**
+ * Handle destruction of the lockScroll instance, including disabling
+ * of the current instance.
+ *
+ * @method destructor
+ */
+ destructor: function() {
+ this.disableScrollLock();
+ },
+
+ /**
+ * Start locking the page scroll.
+ *
+ * This is achieved by applying the lockscroll class to the body Node.
+ *
+ * A count of the total number of active, and enabled, lockscroll instances is also kept on
+ * the body to ensure that premature disabling does not occur.
+ *
+ * @method enableScrollLock
+ * @chainable
+ */
+ enableScrollLock: function() {
+ if (this.isActive()) {
+ Y.log('LockScroll already active. Ignoring enable request', 'warn', 'moodle-core-lockscroll');
+ return;
+ }
+
+ Y.log('Enabling LockScroll.', 'debug', 'moodle-core-lockscroll');
+ this._enabled = true;
+ var body = Y.one(Y.config.doc.body);
+
+ // We use a CSS class on the body to handle the actual locking.
+ body.addClass('lockscroll');
+
+ // Increase the count of active instances - this is used to ensure that we do not
+ // remove the locking when parent windows are still open.
+ // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them.
+ var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 0,
+ newCount = currentCount + 1;
+ body.setAttribute('data-activeScrollLocks', newCount);
+ Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount,
+ 'debug', 'moodle-core-lockscroll');
+
+ return this;
+ },
+
+ /**
+ * Stop locking the page scroll.
+ *
+ * The instance may be disabled but the scroll lock not removed if other instances of the
+ * plugin are also active.
+ *
+ * @method disableScrollLock
+ * @chainable
+ */
+ disableScrollLock: function() {
+ if (this.isActive()) {
+ Y.log('Disabling LockScroll.', 'debug', 'moodle-core-lockscroll');
+ this._enabled = false;
+
+ var body = Y.one(Y.config.doc.body);
+
+ // Decrease the count of active instances.
+ // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them.
+ var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 1,
+ newCount = currentCount - 1;
+
+ if (currentCount === 1) {
+ body.removeClass('lockscroll');
+ }
+
+ body.setAttribute('data-activeScrollLocks', currentCount - 1);
+ Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount,
+ 'debug', 'moodle-core-lockscroll');
+ }
+
+ return this;
+ },
+
+ /**
+ * Return whether scroll locking is active.
+ *
+ * @method isActive
+ * @return Boolean
+ */
+ isActive: function() {
+ return this._enabled;
+ }
+
+}, {
+ NS: 'lockScroll',
+ ATTRS: {
+ }
+});
--- /dev/null
+{
+ "moodle-core-lockscroll": {
+ "requires": [
+ "plugin",
+ "base-build"
+ ]
+ }
+}
}
this.set('COUNT', COUNT);
+ if (this.get('modal')) {
+ this.plug(Y.M.core.LockScroll);
+ }
+
// Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
// and allow setting of z-index in theme.
bb = this.get('boundingBox');
header = this.headerNode,
content = this.bodyNode;
+ // Lock scroll if the plugin is present.
+ if (this.lockScroll) {
+ this.lockScroll.enableScrollLock();
+ }
+
result = DIALOGUE.superclass.show.call(this);
if (header && header !== '') {
header.focus();
}
return result;
},
+
+ hide: function() {
+ // Unlock scroll if the plugin is present.
+ if (this.lockScroll) {
+ this.lockScroll.disableScrollLock();
+ }
+
+ return DIALOGUE.superclass.hide.call(this, arguments);
+ },
/**
* Setup key delegation to keep tabbing within the open dialogue.
*
"node",
"panel",
"event-key",
- "dd-plugin"
+ "dd-plugin",
+ "moodle-core-lockscroll"
]
},
"moodle-core-notification-alert": {
--- /dev/null
+@core @core_message
+Feature: An user can message course participants
+ In order to communicate efficiently with my students
+ As a teacher
+ I need to message them all
+
+ @javascript
+ Scenario: An user can message multiple course participants including him/her self
+ Given the following "users" exists:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ | student1 | Student | 1 | student1@asd.com |
+ | student2 | Student | 2 | student2@asd.com |
+ | student3 | Student | 3 | student3@asd.com |
+ And the following "courses" exists:
+ | fullname | shortname | format |
+ | Course 1 | C1 | topics |
+ And the following "course enrolments" exists:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I follow "Participants"
+ When I click on "input[type='checkbox']" "css_element" in the "Teacher 1" "table_row"
+ And I click on "input[type='checkbox']" "css_element" in the "Student 1" "table_row"
+ And I select "Send a message" from "With selected users..."
+ And I fill the moodle form with:
+ | messagebody | Here it is, the message content |
+ And I press "Preview"
+ And I press "Send message"
+ And I follow "Home"
+ And I expand "My profile" node
+ And I follow "Messages"
+ And I select "Recent conversations" from "Message navigation:"
+ Then I should see "Here it is, the message content"
+ And I should see "Student 1"
+ And I click on "this conversation" "link" in the "//div[@class='singlemessage'][contains(., 'Teacher 1')]" "xpath_element"
+ And I should see "Here it is, the message content"
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
protected function get_legacy_eventdata() {
$eventdata = new \stdClass();
$eventdata->modulename = 'assign';
- $eventdata->cmid = $this->context->instanceid;
+ $eventdata->cmid = $this->contextinstanceid;
$eventdata->itemid = $this->objectid;
$eventdata->courseid = $this->courseid;
$eventdata->userid = $this->userid;
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
$commenttext = $feedbackcomments->commenttext;
}
}
- return optional_param('quickgrade_comments_' . $userid, '', PARAM_TEXT) != $commenttext;
+ // Note that this handles the difference between empty and not in the quickgrading
+ // form at all (hidden column).
+ $newvalue = optional_param('quickgrade_comments_' . $userid, false, PARAM_TEXT);
+ return ($newvalue !== false) && ($newvalue != $commenttext);
}
public function save_quickgrading_changes($userid, $grade) {
global $DB;
$feedbackcomment = $this->get_feedback_comments($grade->id);
+ $feedbackpresent = optional_param('quickgrade_comments_' . $userid, false, PARAM_TEXT) !== false;
+ if (!$feedbackpresent) {
+ // Nothing to save (e.g. hidden column).
+ return true;
+ }
if ($feedbackcomment) {
$feedbackcomment->commenttext = optional_param('quickgrade_comments_' . $userid, '', PARAM_TEXT);
return $DB->update_record('assignfeedback_comments', $feedbackcomment);
$tmpfile = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
@unlink($tmpfile);
- $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
+ try {
+ $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
+ } catch (\Exception $e) {
+ debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER);
+ // TCPDF does not recover from errors so we need to re-initialise the class.
+ $pagecount = 0;
+ }
if ($pagecount == 0) {
// We at least want a single blank page.
+ debugging('TCPDF did not produce a valid pdf:' . $tmpfile . '. Replacing with a blank pdf.', DEBUG_DEVELOPER);
+ $pdf = new pdf();
$pdf->AddPage();
@unlink($tmpfile);
$files = false;
$fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
+ // Detect corrupt generated pdfs and replace with a blank one.
+ if ($files) {
+ $pagecount = $pdf->load_pdf($tmpfile);
+ if ($pagecount <= 0) {
+ $files = false;
+ }
+ }
+
if (!$files) {
// This was a blank pdf.
+ unset($pdf);
+ $pdf = new pdf();
$content = $pdf->Output(self::COMBINED_PDF_FILENAME, 'S');
$file = $fs->create_file_from_string($record, $content);
} else {
// This was a combined pdf.
$file = $fs->create_file_from_pathname($record, $tmpfile);
@unlink($tmpfile);
+
+ // Test the generated file for correctness.
+ $compatiblepdf = pdf::ensure_pdf_compatible($file);
}
return $file;
public function combine_pdfs($pdflist, $outfilename) {
raise_memory_limit(MEMORY_EXTRA);
+ $olddebug = error_reporting(0);
$this->setPageUnit('pt');
$this->setPrintHeader(false);
}
$this->save_pdf($outfilename);
+ error_reporting($olddebug);
return $totalpagecount;
}
*/
public function load_pdf($filename) {
raise_memory_limit(MEMORY_EXTRA);
+ $olddebug = error_reporting(0);
$this->setPageUnit('pt');
$this->scale = 72.0 / 100.0;
$this->pagecount = $this->setSourceFile($filename);
$this->filename = $filename;
+ error_reporting($olddebug);
return $this->pagecount;
}
* @param string $filename the filename for the PDF (including the full path)
*/
public function save_pdf($filename) {
+ $olddebug = error_reporting(0);
$this->Output($filename, 'F');
+ error_reporting($olddebug);
}
/**
* @return string path to copy or converted pdf (false == fail)
*/
public static function ensure_pdf_compatible(\stored_file $file) {
- global $CFG;
-
- $fp = $file->get_content_file_handle();
- $ident = fread($fp, 10);
- if (substr_compare('%PDF-', $ident, 0, 5) !== 0) {
- return false; // This is not a PDF file at all.
- }
- $ident = substr($ident, 5); // Remove the '%PDF-' part.
- $ident = explode('\x0A', $ident); // Truncate to first '0a' character.
- list($major, $minor) = explode('.', $ident[0]); // Split the major / minor version.
- $major = intval($major);
- $minor = intval($minor);
- if ($major == 0 || $minor == 0) {
- return false; // Not a valid PDF version number.
- }
$temparea = \make_temp_directory('assignfeedback_editpdf');
$hash = $file->get_contenthash(); // Use the contenthash to make sure the temp files have unique names.
$tempsrc = $temparea . "/src-$hash.pdf";
$tempdst = $temparea . "/dst-$hash.pdf";
+ $file->copy_content_to($tempsrc); // Copy the file.
- if ($major = 1 && $minor<=4) {
- // PDF is valid version - just create a copy we can use.
- $file->copy_content_to($tempdst); // Copy the file.
- return $tempdst;
+ $pdf = new pdf();
+ $pagecount = 0;
+ try {
+ $pagecount = $pdf->load_pdf($tempsrc);
+ } catch (\Exception $e) {
+ // PDF was not valid - try running it through ghostscript to clean it up.
+ $pagecount = 0;
+ }
+
+ if ($pagecount > 0) {
+ // Page is valid and can be read by tcpdf.
+ return $tempsrc;
}
- $file->copy_content_to($tempsrc); // Copy the file.
$gsexec = \escapeshellarg(\get_config('assignfeedback_editpdf', 'gspath'));
$tempdstarg = \escapeshellarg($tempdst);
return false;
}
+ $pdf = new pdf();
+ $pagecount = 0;
+ try {
+ $pagecount = $pdf->load_pdf($tempdst);
+ } catch (\Exception $e) {
+ // PDF was not valid - try running it through ghostscript to clean it up.
+ $pagecount = 0;
+ }
+ if ($pagecount <= 0) {
+ @unlink($tempdst);
+ // Could not parse the converted pdf.
+ return false;
+ }
+
return $tempdst;
}
id="selectuser_' . $row->userid . '"
name="selectedusers"
value="' . $row->userid . '"/>';
+ $selectcol .= '<input type="hidden"
+ name="grademodified_' . $row->userid . '"
+ value="' . $row->timemarked . '"/>';
return $selectcol;
}
maxlength="10"
class="quickgrade"/>';
$o .= ' / ' . format_float($this->get_instance()->grade, 2);
- $o .= '<input type="hidden"
- name="grademodified_' . $userid . '"
- value="' . $modified . '"/>';
return $o;
} else {
- $o .= '<input type="hidden" name="grademodified_' . $userid . '" value="' . $modified . '"/>';
if ($grade == -1 || $grade === null) {
$o .= '-';
} else {
$o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
}
$o .= '</select>';
- $o .= '<input type="hidden" ' .
- 'name="grademodified_' . $userid . '" ' .
- 'value="' . $modified . '"/>';
return $o;
} else {
$scaleid = (int)$grade;
$record->userid = $userid;
if ($modified >= 0) {
$record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
- $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', '', PARAM_TEXT);
- $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', '', PARAM_INT);
+ $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_TEXT);
+ $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
} else {
// This user was not in the grading table.
continue;
foreach ($currentgrades as $current) {
$modified = $users[(int)$current->userid];
$grade = $this->get_user_grade($modified->userid, false);
+ // Check to see if the grade column was even visible.
+ $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
// Check to see if the outcomes were modified.
if ($CFG->enableoutcomes) {
$oldoutcome = $outcome->grades[$modified->userid]->grade;
$paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
$newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
- if ($oldoutcome != $newoutcome) {
+ // Check to see if the outcome column was even visible.
+ $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
+ if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
// Can't check modified time for outcomes because it is not reported.
$modifiedusers[$modified->userid] = $modified;
continue;
// Let plugins participate.
foreach ($this->feedbackplugins as $plugin) {
if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
+ // The plugins must handle is_quickgrading_modified correctly - ie
+ // handle hidden columns.
if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
if ((int)$current->lastmodified > (int)$modified->lastmodified) {
return get_string('errorrecordmodified', 'assign');
if ($current->grade !== null) {
$current->grade = floatval($current->grade);
}
- if ($current->grade !== $modified->grade ||
- ($this->get_instance()->markingallocation && $current->allocatedmarker != $modified->allocatedmarker ) ||
- ($this->get_instance()->markingworkflow && $current->workflowstate !== $modified->workflowstate )) {
-
+ $gradechanged = $gradecolpresent && $current->grade !== $modified->grade;
+ $markingallocationchanged = $this->get_instance()->markingallocation &&
+ ($modified->allocatedmarker !== false) &&
+ ($current->allocatedmarker != $modified->allocatedmarker);
+ $workflowstatechanged = $this->get_instance()->markingworkflow &&
+ ($modified->workflowstate !== false) &&
+ ($current->workflowstate != $modified->workflowstate);
+ if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
// Grade changed.
if ($this->grading_disabled($modified->userid)) {
continue;
$flags = $this->get_user_flags($userid, true);
$grade->grade= grade_floatval(unformat_float($modified->grade));
$grade->grader= $USER->id;
+ $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
// Save plugins data.
foreach ($this->feedbackplugins as $plugin) {
}
}
- if ($flags->workflowstate != $modified->workflowstate ||
- $flags->allocatedmarker != $modified->allocatedmarker) {
+ // These will be set to false if they are not present in the quickgrading
+ // form (e.g. column hidden).
+ $workflowstatemodified = ($modified->workflowstate !== false) &&
+ ($flags->workflowstate != $modified->workflowstate);
+
+ $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
+ ($flags->allocatedmarker != $modified->allocatedmarker);
+ if ($workflowstatemodified) {
$flags->workflowstate = $modified->workflowstate;
+ }
+ if ($allocatedmarkermodified) {
$flags->allocatedmarker = $modified->allocatedmarker;
+ }
+ if ($workflowstatemodified || $allocatedmarkermodified) {
$this->update_user_flags($flags);
}
$this->update_grade($grade);
foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
$oldoutcome = $outcome->grades[$modified->userid]->grade;
$paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
- $newoutcome = optional_param($paramname, -1, PARAM_INT);
- if ($oldoutcome != $newoutcome) {
+ // This will be false if the input was not in the quickgrading
+ // form (e.g. column hidden).
+ $newoutcome = optional_param($paramname, false, PARAM_INT);
+ if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
$data[$outcomeid] = $newoutcome;
}
}
// Checking that the event contains the expected values.
$this->assertInstanceOf('\assignsubmission_comments\event\comment_created', $event);
$this->assertEquals($context, $event->get_context());
- $url = new moodle_url('/mod/assign/view.php', array('id' => $submission->id));
+ $url = new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
/**
// Checking that the event contains the expected values.
$this->assertInstanceOf('\assignsubmission_comments\event\comment_deleted', $event);
$this->assertEquals($context, $event->get_context());
- $url = new moodle_url('/mod/assign/view.php', array('id' => $submission->id));
+ $url = new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id));
$this->assertEquals($url, $event->get_url());
+ $this->assertEventContextNotUsed($event);
}
}
protected function get_legacy_eventdata() {
$eventdata = new \stdClass();
$eventdata->modulename = 'assign';
- $eventdata->cmid = $this->context->instanceid;
+ $eventdata->cmid = $this->contextinstanceid;
$eventdata->itemid = $this->objectid;
$eventdata->courseid = $this->courseid;
$eventdata->userid = $this->userid;
* @return \moodle_url
*/
public function get_url() {
- return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+ return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
}
/**
$expected->files = $files;
$expected->pathnamehashes = array($fi->get_pathnamehash(), $fi2->get_pathnamehash());
$this->assertEventLegacyData($expected, $event);
+ $this->assertEventContextNotUsed($event);
}
}