// Get all the block_position objects pending to match
$params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
- $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
// Process block positions, creating them or accumulating for final step
foreach($rs as $posrec) {
- // Get the complete position object (stored as info)
- $position = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'block_position', $posrec->itemid)->info;
+ // Get the complete position object out of the info field.
+ $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
// If position is for one already mapped (known) contextid
// process it now, creating the position, else nothing to
// do, position finally discarded
// Get all the module_availability objects to process
$params = array('backupid' => $this->get_restoreid(), 'itemname' => 'module_availability');
- $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
// Process availabilities, creating them if everything matches ok
foreach($rs as $availrec) {
$allmatchesok = true;
// Get the complete availabilityobject
- $availability = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_availability', $availrec->itemid)->info;
+ $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
// Map the sourcecmid if needed and possible
if (!empty($availability->sourcecmid)) {
$newcm = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'course_module', $availability->sourcecmid);
// Iterate over aliases in the queue.
foreach ($rs as $record) {
- $info = unserialize(base64_decode($record->info));
+ $info = restore_dbops::decode_backup_temp_info($record->info);
// Try to pick a repository instance that should serve the alias.
$repository = $this->choose_repository($info);
$source = null;
foreach ($candidates as $candidate) {
- $candidateinfo = unserialize(base64_decode($candidate->info));
+ $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
if ($candidateinfo->filename === $reference['filename']
and $candidateinfo->filepath === $reference['filepath']
and !is_null($candidate->newcontextid)
method is not available anymore. Temp tables must be created
inline always.
+* Using the info field from backup_ids_temp or backup_files_temp
+ must now go via backup_controller_dbops::decode_backup_temp_info() and
+ backup_controller_dbops::encode_backup_temp_info(). The implementation
+ of the encoding has changed. These new functions encapsulate any future
+ changes to the encoding.
+
=== 2.5 ===
* New optional param $sortby in backup set_source_table() allows to
$dbman->drop_table($table); // And drop it
}
+ /**
+ * Decode the info field from backup_ids_temp or backup_files_temp.
+ *
+ * @param mixed $info The info field data to decode, may be an object or a simple integer.
+ * @return mixed The decoded information. For simple types it returns, for complex ones we decode.
+ */
+ public static function decode_backup_temp_info($info) {
+ // We encode all data except null.
+ if ($info != null) {
+ if (extension_loaded('zlib')) {
+ return unserialize(gzuncompress(base64_decode($info)));
+ } else {
+ return unserialize(base64_decode($info));
+ }
+ }
+ return $info;
+ }
+
+ /**
+ * Encode the info field for backup_ids_temp or backup_files_temp.
+ *
+ * @param mixed $info string The info field data to encode.
+ * @return string An encoded string of data or null if the input is null.
+ */
+ public static function encode_backup_temp_info($info) {
+ // We encode if there is any information to keep the translations simpler.
+ if ($info != null) {
+ // We compress if possible. It reduces db, network and memory storage. The saving is greater than CPU compression cost.
+ // Compression level 1 is chosen has it produces good compression with the smallest possible overhead, see MDL-40618.
+ if (extension_loaded('zlib')) {
+ return base64_encode(gzcompress(serialize($info), 1));
+ } else {
+ return base64_encode(serialize($info));
+ }
+ }
+ return $info;
+ }
+
/**
* Given one type and id from controller, return the corresponding courseid
*/
$problems = array(); // To store warnings/errors
// Get loaded roles from backup_ids
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info');
foreach ($rs as $recrole) {
// If the rolemappings->modified flag is set, that means that we are coming from
// manually modified mappings (by UI), so accept those mappings an put them to backup_ids
// Else, we haven't any info coming from UI, let's calculate the mappings, matching
// in multiple ways and checking permissions. Note mapping to 0 means "skip"
} else {
- $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info;
+ $role = (object)backup_controller_dbops::decode_backup_temp_info($recrole->info);
$match = self::get_best_assignable_role($role, $courseid, $userid, $samesite);
// Send match to backup_ids
self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match);
// Build the rolemappings element for controller
unset($role->id);
unset($role->nameincourse);
- unset($role->nameincourse);
$role->targetroleid = $match;
$rolemappings->mappings[$recrole->itemid] = $role;
// Prepare warning if no match found
global $DB;
$results = array();
- $qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid
+ $qcats = $DB->get_recordset_sql("SELECT itemid, parentitemid AS contextid, info
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question_category'", array($restoreid));
foreach ($qcats as $qcat) {
// If this qcat context haven't been acummulated yet, do that
if (!isset($results[$qcat->contextid])) {
- $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
+ $info = backup_controller_dbops::decode_backup_temp_info($qcat->info);
// Filter by contextlevel if necessary
- if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) {
- $results[$qcat->contextid] = $temprec->info->contextlevel;
+ if (is_null($contextlevel) || $contextlevel == $info->contextlevel) {
+ $results[$qcat->contextid] = $info->contextlevel;
}
}
}
+ $qcats->close();
// Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE)
asort($results);
return $results;
global $DB;
$results = array();
- $qcats = $DB->get_records_sql("SELECT itemid
+ $qcats = $DB->get_recordset_sql("SELECT itemid, info
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question_category'
AND parentitemid = ?", array($restoreid, $contextid));
foreach ($qcats as $qcat) {
- $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
- $results[$qcat->itemid] = $temprec->info;
+ $results[$qcat->itemid] = backup_controller_dbops::decode_backup_temp_info($qcat->info);
}
+ $qcats->close();
+
return $results;
}
global $DB;
$results = array();
- $qs = $DB->get_records_sql("SELECT itemid
+ $qs = $DB->get_recordset_sql("SELECT itemid, info
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question'
AND parentitemid = ?", array($restoreid, $qcatid));
foreach ($qs as $q) {
- $temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid);
- $results[$q->itemid] = $temprec->info;
+ $results[$q->itemid] = backup_controller_dbops::decode_backup_temp_info($q->info);
}
+ $qs->close();
return $results;
}
$basepath = $basepath . '/files/';// Get backup file pool base
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $rec) {
- $file = (object)unserialize(base64_decode($rec->info));
+ $file = (object)backup_controller_dbops::decode_backup_temp_info($rec->info);
// ignore root dirs (they are created automatically)
if ($file->filepath == '/' && $file->filename == '.') {
$themes = get_list_of_themes(); // Get themes for quick search later
// Iterate over all the included users with newitemid = 0, have to create them
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid, info');
foreach ($rs as $recuser) {
- $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+ $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
// if user lang doesn't exist here, use site default
if (!array_key_exists($user->lang, $languages)) {
}
// Iterate over all the included users
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid, info');
foreach ($rs as $recuser) {
- $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+ $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
// Find the correct mnethostid for user before performing any further check
if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) {
global $DB;
// Store external files info in `info` field
- $filerec->info = base64_encode(serialize($filerec)); // Serialize the whole rec in info
+ $filerec->info = backup_controller_dbops::encode_backup_temp_info($filerec); // Encode the whole record into info.
$filerec->backupid = $restoreid;
$DB->insert_record('backup_files_temp', $filerec);
}
$extrarecord['parentitemid'] = $parentitemid;
}
if ($info != null) {
- $extrarecord['info'] = base64_encode(serialize($info));
+ $extrarecord['info'] = backup_controller_dbops::encode_backup_temp_info($info);
}
self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord);
public static function get_backup_ids_record($restoreid, $itemname, $itemid) {
$dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid);
+ // We must test if info is a string, as the cache stores info in object form.
if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) {
- $dbrec->info = unserialize(base64_decode($dbrec->info));
+ $dbrec->info = backup_controller_dbops::decode_backup_temp_info($dbrec->info);
}
return $dbrec;
// Get the course context
$coursectx = context_course::instance($courseid);
// Get all the mapped roles we have
- $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+ $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info, newitemid');
foreach ($rs as $recrole) {
- // Get the complete temp_ids record
- $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid);
+ $info = backup_controller_dbops::decode_backup_temp_info($recrole->info);
// If it's one mapped role and we have one name for it
- if (!empty($role->newitemid) && !empty($role->info['nameincourse'])) {
+ if (!empty($recrole->newitemid) && !empty($info['nameincourse'])) {
// If role name doesn't exist, add it
$rolename = new stdclass();
- $rolename->roleid = $role->newitemid;
+ $rolename->roleid = $recrole->newitemid;
$rolename->contextid = $coursectx->id;
if (!$DB->record_exists('role_names', (array)$rolename)) {
- $rolename->name = $role->info['nameincourse'];
+ $rolename->name = $info['nameincourse'];
$DB->insert_record('role_names', $rolename);
}
}
// Drop and check it doesn't exists anymore
backup_controller_dbops::drop_backup_ids_temp_table('testingid');
$this->assertFalse($dbman->table_exists('backup_ids_temp'));
+
+ // Test encoding/decoding of backup_ids_temp,backup_files_temp encode/decode functions.
+ // We need to handle both objects and data elements.
+ $object = new stdClass();
+ $object->item1 = 10;
+ $object->item2 = 'a String';
+ $testarray = array($object, 10, null, 'string', array('a' => 'b', 1 => 1));
+ foreach ($testarray as $item) {
+ $encoded = backup_controller_dbops::encode_backup_temp_info($item);
+ $decoded = backup_controller_dbops::decode_backup_temp_info($encoded);
+ $this->assertEquals($item, $decoded);
+ }
}
/**
$this->definition = $definition;
$hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
$this->path = $this->filestorepath.'/'.$hash;
- make_writable_directory($this->path);
+ make_writable_directory($this->path, false);
if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
$this->prescan = false;
}
return $this->path . '/' . $key . '.cache';
} else {
// We are using a single subdirectory to achieve 1 level.
- $subdir = substr($key, 0, 3);
+ // We suffix the subdir so it does not clash with any windows
+ // reserved filenames like 'con'.
+ $subdir = substr($key, 0, 3) . '-cache';
$dir = $this->path . '/' . $subdir;
if ($create) {
// Create the directory. This function does it recursivily!
- make_writable_directory($dir);
+ make_writable_directory($dir, false);
}
return $dir . '/' . $key . '.cache';
}
}
}
+ /**
+ * Tests for cache keys that would break on windows.
+ */
+ public function test_windows_nasty_keys() {
+ $instance = cache_config_phpunittest::instance();
+ $instance->phpunit_add_definition('phpunit/windowskeytest', array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'component' => 'phpunit',
+ 'area' => 'windowskeytest',
+ 'simplekeys' => true,
+ 'simpledata' => true
+ ));
+ $cache = cache::make('phpunit', 'windowskeytest');
+ $this->assertTrue($cache->set('contest', 'test data 1'));
+ $this->assertEquals('test data 1', $cache->get('contest'));
+ }
+
/**
* Tests the default application cache
*/
* @param cache_loader $cache
*/
protected function run_on_cache(cache_loader $cache) {
- $key = 'testkey';
+ $key = 'contestkey';
$datascalars = array('test data', null);
- $dataarray = array('test' => 'data', 'part' => 'two');
+ $dataarray = array('contest' => 'data', 'part' => 'two');
$dataobject = (object)$dataarray;
foreach ($datascalars as $datascalar) {
// OK data added, data invalidated, and invalidation time has been set.
// Now we need to manually add back the data and adjust the invalidation time.
$hash = md5(cache_store::MODE_APPLICATION.'/phpunit/eventinvalidationtest/'.$CFG->wwwroot.'phpunit');
- $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-$hash.cache";
+ $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache";
// Make sure the file is correct.
$this->assertTrue(file_exists($timefile));
$timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
file_put_contents($timefile, $timecont);
$this->assertTrue(file_exists($timefile));
- $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-$hash.cache";
+ $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes-cache/testkey1-$hash.cache";
$datacont = serialize("test data 1");
make_writable_directory(dirname($datafile));
file_put_contents($datafile, $datacont);
$cm = $instances[$grade_object->iteminstance];
if (!$cm->uservisible) {
// Further checks are required to determine whether the activity is entirely hidden or just greyed out.
- if ($cm->is_user_access_restricted_by_group() || $cm->is_user_access_restricted_by_conditional_access()) {
+ if ($cm->is_user_access_restricted_by_group() || $cm->is_user_access_restricted_by_conditional_access() ||
+ $cm->is_user_access_restricted_by_capability()) {
$hide = true;
}
}
}
// Wrapping the output.
- echo '<div class="phpdebugmessage">' . PHP_EOL;
+ echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL;
echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL;
echo '</div>';
upgrade_main_savepoint(true, 2013071500.02);
}
+ if ($oldversion < 2013072600.01) {
+ upgrade_mssql_nvarcharmax();
+ upgrade_mssql_varbinarymax();
+
+ upgrade_main_savepoint(true, 2013072600.01);
+ }
+
return true;
}
$pbar->update($i, $tablecount, "Converted unsigned/lob columns in MySQL database - $i/$tablecount.");
}
}
+
+/**
+ * Migrate NTEXT to NVARCHAR(MAX).
+ */
+function upgrade_mssql_nvarcharmax() {
+ global $DB;
+
+ if ($DB->get_dbfamily() !== 'mssql') {
+ return;
+ }
+
+ $pbar = new progress_bar('mssqlconvertntext', 500, true);
+
+ $prefix = $DB->get_prefix();
+ $tables = $DB->get_tables(false);
+
+ $tablecount = count($tables);
+ $i = 0;
+ foreach ($tables as $table) {
+ $i++;
+
+ $columns = array();
+
+ $sql = "SELECT column_name
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE table_name = '{{$table}}' AND UPPER(data_type) = 'NTEXT'";
+ $rs = $DB->get_recordset_sql($sql);
+ foreach ($rs as $column) {
+ $columns[] = $column->column_name;
+ }
+ $rs->close();
+
+ if ($columns) {
+ // Set appropriate timeout - 1 minute per thousand of records should be enough, min 60 minutes just in case.
+ $count = $DB->count_records($table, array());
+ $timeout = ($count/1000)*60;
+ $timeout = ($timeout < 60*60) ? 60*60 : (int)$timeout;
+ upgrade_set_timeout($timeout);
+
+ $updates = array();
+ foreach ($columns as $column) {
+ // Change the definition.
+ $sql = "ALTER TABLE {$prefix}$table ALTER COLUMN $column NVARCHAR(MAX)";
+ $DB->change_database_structure($sql);
+ $updates[] = "$column = $column";
+ }
+
+ // Now force the migration of text data to new optimised storage.
+ $sql = "UPDATE {{$table}} SET ".implode(', ', $updates);
+ $DB->execute($sql);
+ }
+
+ $pbar->update($i, $tablecount, "Converted NTEXT to NVARCHAR(MAX) columns in MS SQL Server database - $i/$tablecount.");
+ }
+}
+
+/**
+ * Migrate IMAGE to VARBINARY(MAX).
+ */
+function upgrade_mssql_varbinarymax() {
+ global $DB;
+
+ if ($DB->get_dbfamily() !== 'mssql') {
+ return;
+ }
+
+ $pbar = new progress_bar('mssqlconvertimage', 500, true);
+
+ $prefix = $DB->get_prefix();
+ $tables = $DB->get_tables(false);
+
+ $tablecount = count($tables);
+ $i = 0;
+ foreach ($tables as $table) {
+ $i++;
+
+ $columns = array();
+
+ $sql = "SELECT column_name
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE table_name = '{{$table}}' AND UPPER(data_type) = 'IMAGE'";
+ $rs = $DB->get_recordset_sql($sql);
+ foreach ($rs as $column) {
+ $columns[] = $column->column_name;
+ }
+ $rs->close();
+
+ if ($columns) {
+ // Set appropriate timeout - 1 minute per thousand of records should be enough, min 60 minutes just in case.
+ $count = $DB->count_records($table, array());
+ $timeout = ($count/1000)*60;
+ $timeout = ($timeout < 60*60) ? 60*60 : (int)$timeout;
+ upgrade_set_timeout($timeout);
+
+ foreach ($columns as $column) {
+ // Change the definition.
+ $sql = "ALTER TABLE {$prefix}$table ALTER COLUMN $column VARBINARY(MAX)";
+ $DB->change_database_structure($sql);
+ }
+
+ // Binary columns should not be used, do not waste time optimising the storage.
+ }
+
+ $pbar->update($i, $tablecount, "Converted IMAGE to VARBINARY(MAX) columns in MS SQL Server database - $i/$tablecount.");
+ }
+}
$dbtype .= '(' . $xmldb_length . ')';
break;
case XMLDB_TYPE_TEXT:
- $dbtype = 'NTEXT';
+ $dbtype = 'NVARCHAR(MAX)';
break;
case XMLDB_TYPE_BINARY:
- $dbtype = 'IMAGE';
+ $dbtype = 'VARBINARY(MAX)';
break;
case XMLDB_TYPE_DATETIME:
$dbtype = 'DATETIME';
// id columns being auto_incremnt are PK by definition
$info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
- // Put correct length for character and LOB types
- $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
- $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+ if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
+ // This is NVARCHAR(MAX), not a normal NVARCHAR.
+ $info->max_length = -1;
+ $info->meta_type = 'X';
+ } else {
+ // Put correct length for character and LOB types
+ $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
+ $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+ }
// Scale
$info->scale = $rawcolumn->scale ? $rawcolumn->scale : false;
$type = 'X';
break;
case 'IMAGE':
+ case 'VARBINARY':
case 'VARBINARY(MAX)':
$type = 'B';
break;
// id columns being auto_incremnt are PK by definition
$info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
- // Put correct length for character and LOB types
- $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
- $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+ if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
+ // This is NVARCHAR(MAX), not a normal NVARCHAR.
+ $info->max_length = -1;
+ $info->meta_type = 'X';
+ } else {
+ // Put correct length for character and LOB types
+ $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
+ $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+ }
// Scale
$info->scale = $rawcolumn->scale ? $rawcolumn->scale : false;
break;
case 'IMAGE':
+ case 'VARBINARY':
case 'VARBINARY(MAX)':
$type = 'B';
break;
}
// Check group membership.
- if ($this->is_user_access_restricted_by_group()) {
+ if ($this->is_user_access_restricted_by_group() ||
+ $this->is_user_access_restricted_by_capability()) {
$this->uservisible = false;
// Ensure activity is completely hidden from the user.
return false;
}
+ /**
+ * Checks whether mod/...:view capability restricts the current user's access.
+ *
+ * @return bool True if the user access is restricted.
+ */
+ public function is_user_access_restricted_by_capability() {
+ $capability = 'mod/' . $this->modname . ':view';
+ $capabilityinfo = get_capability_info($capability);
+ if (!$capabilityinfo) {
+ // Capability does not exist, no one is prevented from seeing the activity.
+ return false;
+ }
+
+ // You are blocked if you don't have the capability.
+ return !has_capability($capability, context_module::instance($this->id));
+ }
+
/**
* Checks whether the module's conditional access settings mean that the user cannot see the activity at all
*
$message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
//It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix for new installation.
}
- $output .= $this->box($message, 'errorbox');
+ $output .= $this->box($message, 'errorbox', array('data-rel' => 'fatalerror'));
if (debugging('', DEBUG_DEVELOPER)) {
if (!empty($debuginfo)) {
* @param string $contents The contents of the box
* @param string $classes A space-separated list of CSS classes
* @param string $id An optional ID
+ * @param array $attributes An array of other attributes to give the box.
* @return string the HTML to output.
*/
- public function box($contents, $classes = 'generalbox', $id = null) {
- return $this->box_start($classes, $id) . $contents . $this->box_end();
+ public function box($contents, $classes = 'generalbox', $id = null, $attributes = array()) {
+ return $this->box_start($classes, $id, $attributes) . $contents . $this->box_end();
}
/**
*
* @param string $classes A space-separated list of CSS classes
* @param string $id An optional ID
+ * @param array $attributes An array of other attributes to give the box.
* @return string the HTML to output.
*/
- public function box_start($classes = 'generalbox', $id = null) {
+ public function box_start($classes = 'generalbox', $id = null, $attributes = array()) {
$this->opencontainers->push('box', html_writer::end_tag('div'));
- return html_writer::start_tag('div', array('id' => $id,
- 'class' => 'box ' . renderer_base::prepare_classes($classes)));
+ $attributes['id'] = $id;
+ $attributes['class'] = 'box ' . renderer_base::prepare_classes($classes);
+ return html_writer::start_tag('div', $attributes);
}
/**
$cache = cache::make('core', 'yuimodules');
if (!isset($CFG->jsrev) || $CFG->jsrev == -1) {
$metadata = array();
+ $metadata = $this->get_moodle_metadata();
$cache->delete('metadata');
} else {
// Attempt to get the metadata from the cache.
return '';
}
- $from = $plaintext ? '' : '<ul style="text-align: left">';
+ $from = $plaintext ? '' : '<ul style="text-align: left" data-rel="backtrace">';
foreach ($callers as $caller) {
if (!isset($caller['line'])) {
$caller['line'] = '?'; // probably call_user_func()
if (!file_exists($dir)) {
if (!mkdir($dir, $CFG->directorypermissions, true)) {
- if ($exceptiononerror) {
- throw new invalid_dataroot_permissions($dir.' can not be created, check permissions.');
- } else {
- return false;
+ clearstatcache();
+ // There might be a race condition when creating directory.
+ if (!is_dir($dir)) {
+ if ($exceptiononerror) {
+ throw new invalid_dataroot_permissions($dir.' can not be created, check permissions.');
+ } else {
+ debugging('Can not create directory: '.$dir, DEBUG_DEVELOPER);
+ return false;
+ }
}
}
}
try {
// Exceptions.
- $exceptionsxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' errorbox ')]" .
- "/descendant::p[contains(concat(' ', normalize-space(@class), ' '), ' errormessage ')]";
+ $exceptionsxpath = "//div[@data-rel='fatalerror']";
// Debugging messages.
- $debuggingxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' debuggingmessage ')]";
+ $debuggingxpath = "//div[@data-rel='debugging']";
// PHP debug messages.
- $phperrorxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' phpdebugmessage ')]";
+ $phperrorxpath = "//div[@data-rel='phpdebugmessage']";
// Any other backtrace.
- $othersxpath = "(//*[contains(., ': call to ')])[1]";
+ $othersxpath = "//ul[@data-rel='backtrace']";
$xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath);
$joinedxpath = implode(' | ', $xpaths);
* Use core_text::* instead of textlib:: and also core_collator::* instead of collatorlib::*.
* Use new function moodleform::mock_submit() to simulate form submission in unit tests (backported).
* New $CFG->localcachedir setting useful for cluster nodes. Admins have to update X-Sendfile aliases if used.
+* MS SQL Server drivers are now using NVARCHAR(MAX) instead of NTEXT and VARBINARY(MAX) instead of IMAGE,
+ this change should be fully transparent and it should help significantly with add-on compatibility.
DEPRECATIONS:
Various previously deprecated functions have now been altered to throw DEBUG_DEVELOPER debugging notices
if (CLI_SCRIPT) {
echo "++ $message ++\n$from";
} else {
- echo '<div class="notifytiny debuggingmessage">' . $message . $from . '</div>';
+ echo '<div class="notifytiny debuggingmessage" data-rel="debugging">' , $message , $from , '</div>';
}
} else {
$extrafields='m.id as assignmentid, m.course, m.nosubmissions, m.submissiondrafts, m.sendnotifications, '.
'm.sendlatenotifications, m.duedate, m.allowsubmissionsfromdate, m.grade, m.timemodified, '.
'm.completionsubmit, m.cutoffdate, m.teamsubmission, m.requireallteammemberssubmit, '.
- 'm.teamsubmissiongroupingid, m.blindmarking, m.revealidentities, m.requiresubmissionstatement';
+ 'm.teamsubmissiongroupingid, m.blindmarking, m.revealidentities, m.attemptreopenmethod, '.
+ 'm.maxattempts, m.markingworkflow, m.markingallocation, m.requiresubmissionstatement';
$coursearray = array();
foreach ($courses as $id => $course) {
$assignmentarray = array();
'teamsubmissiongroupingid' => $module->teamsubmissiongroupingid,
'blindmarking' => $module->blindmarking,
'revealidentities' => $module->revealidentities,
+ 'attemptreopenmethod' => $module->attemptreopenmethod,
+ 'maxattempts' => $module->maxattempts,
+ 'markingworkflow' => $module->markingworkflow,
+ 'markingallocation' => $module->markingallocation,
'requiresubmissionstatement' => $module->requiresubmissionstatement,
'configs' => $configarray
);
return new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'assignment id'),
+ 'cmid' => new external_value(PARAM_INT, 'course module id'),
'course' => new external_value(PARAM_INT, 'course id'),
'name' => new external_value(PARAM_TEXT, 'assignment name'),
'nosubmissions' => new external_value(PARAM_INT, 'no submissions'),
'teamsubmissiongroupingid' => new external_value(PARAM_INT, 'the grouping id for the team submission groups'),
'blindmarking' => new external_value(PARAM_INT, 'if enabled, hide identities until reveal identities actioned'),
'revealidentities' => new external_value(PARAM_INT, 'show identities for a blind marking assignment'),
+ 'attemptreopenmethod' => new external_value(PARAM_TEXT, 'method used to control opening new attempts'),
+ 'maxattempts' => new external_value(PARAM_INT, 'maximum number of attempts allowed'),
+ 'markingworkflow' => new external_value(PARAM_INT, 'enable marking workflow'),
+ 'markingallocation' => new external_value(PARAM_INT, 'enable marking allocation'),
'requiresubmissionstatement' => new external_value(PARAM_INT, 'student must accept submission statement'),
'configs' => new external_multiple_structure(self::get_assignments_config_structure(), 'configuration settings')
), 'assignment information object');
$USER->id,
'/import/');
+ $keys = array_keys($files);
+ if (count($files) == 1 && $files[$keys[0]]->is_directory()) {
+ // An entire folder was zipped, rather than its contents.
+ // We need to return the contents of the folder instead, so the import can continue.
+ $files = $fs->get_directory_files($contextid,
+ 'assignfeedback_file',
+ ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
+ $USER->id,
+ $files[$keys[0]]->get_filepath());
+ }
+
return $files;
}
* @return string - The html response
*/
public function import_zip_files($assignment, $fileplugin) {
- global $USER, $CFG, $PAGE, $DB;
+ global $CFG, $PAGE, $DB;
@set_time_limit(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME);
$packer = get_file_packer('application/zip');
$contextid = $assignment->get_context()->id;
$fs = get_file_storage();
- $files = $fs->get_directory_files($contextid,
- 'assignfeedback_file',
- ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
- $USER->id,
- '/import/');
+ $files = $this->get_import_files($contextid);
$currentgroup = groups_get_activity_group($assignment->get_course_module(), true);
$allusers = $assignment->list_participants($currentgroup, false);
* Given a course_module object, this function returns any "extra" information that may be needed
* when printing this activity in a course listing. See get_array_of_activities() in course/lib.php.
*
- * @param $coursemodule object The coursemodule object (record).
+ * @param stdClass $coursemodule object The coursemodule object (record).
* @return cached_cm_info An object on information that the courses will know about (most noticeably, an icon).
*/
function assignment_get_coursemodule_info($coursemodule) {
global $CFG;
$mform =& $this->_form;
- //headline
- $mform->addElement('header', 'general', '');
$strdeleteolditmes = get_string('delete_old_items', 'feedback').
' ('.get_string('oldvalueswillbedeleted', 'feedback').')';
$strnodeleteolditmes = get_string('append_new_items', 'feedback').
' ('.get_string('oldvaluespreserved', 'feedback').')';
- $deleteolditemsarray = array();
$mform->addElement('radio', 'deleteolditems', '', $strdeleteolditmes, true);
$mform->addElement('radio', 'deleteolditems', '', $strnodeleteolditmes);
- $mform->addGroup($deleteolditemsarray, 'deleteolditemsarray', '', array(''), false);
// hidden elements
$mform->addElement('hidden', 'id');
define('FEEDBACK_MAX_PIX_LENGTH', '400'); //max. Breite des grafischen Balkens in der Auswertung
define('FEEDBACK_DEFAULT_PAGE_COUNT', 20);
+/**
+ * Returns all other caps used in module.
+ *
+ * @return array
+ */
+function feedback_get_extra_capabilities() {
+ return array('moodle/site:accessallgroups');
+}
+
/**
* @uses FEATURE_GROUPS
* @uses FEATURE_GROUPINGS
continue;
}
- if (is_null($modinfo->groups)) {
- $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
- }
-
- if (!array_key_exists($post->groupid, $modinfo->groups[0])) {
+ if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) {
continue;
}
}
foreach ($courses as $course) {
$modinfo = get_fast_modinfo($course);
- if (is_null($modinfo->groups)) {
- $modinfo->groups = groups_get_user_groups($course->id, $userid);
- }
if (empty($modinfo->instances['forum'])) {
// hmm, no forums?
/// group access
if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
- if (is_null($modinfo->groups)) {
- $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
- }
- if (isset($modinfo->groups[$cm->groupingid])) {
- $forum->onlygroups = $modinfo->groups[$cm->groupingid];
- $forum->onlygroups[] = -1;
- } else {
- $forum->onlygroups = array(-1);
- }
+
+ $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
+ $forum->onlygroups[] = -1;
}
/// hidden timed discussions
require_once($CFG->dirroot.'/course/lib.php');
$modinfo = get_fast_modinfo($course);
- if (is_null($modinfo->groups)) {
- $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
- }
- if (array_key_exists($cm->groupingid, $modinfo->groups)) {
- $mygroups = $modinfo->groups[$cm->groupingid];
- } else {
- $mygroups = false; // Will be set below
- }
+ $mygroups = $modinfo->get_groups($cm->groupingid);
// add all groups posts
- if (empty($mygroups)) {
- $mygroups = array(-1=>-1);
- } else {
- $mygroups[-1] = -1;
- }
+ $mygroups[-1] = -1;
list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
$params[] = $forum->id;
}
if ($groupid) {
- $groupselect = "AND gm.groupid = ?";
- $groupjoin = "JOIN {groups_members} gm ON gm.userid=u.id";
+ $groupselect = "AND d.groupid = ?";
$params[] = $groupid;
} else {
$groupselect = "";
- $groupjoin = "";
}
$allnames = get_all_user_name_fields(true, 'u');
JOIN {forum_discussions} d ON d.id = p.discussion
JOIN {forum} f ON f.id = d.forum
JOIN {user} u ON u.id = p.userid
- $groupjoin
WHERE p.created > ? AND f.id = ?
$userselect $groupselect
ORDER BY p.id ASC", $params)) { // order by initial posting date
$viewhiddentimed = has_capability('mod/forum:viewhiddentimedposts', $cm_context);
$accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context);
- if (is_null($modinfo->groups)) {
- $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
- }
-
$printposts = array();
foreach ($posts as $post) {
continue;
}
- if (!array_key_exists($post->groupid, $modinfo->groups[0])) {
+ if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) {
continue;
}
}
require_once($CFG->dirroot.'/course/lib.php');
$modinfo = get_fast_modinfo($course);
- if (is_null($modinfo->groups)) {
- $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
- }
- if (array_key_exists($cm->groupingid, $modinfo->groups)) {
- $mygroups = $modinfo->groups[$cm->groupingid];
- } else {
- $mygroups = false; // Will be set below
- }
+ $mygroups = $modinfo->get_groups($cm->groupingid);
// add all groups posts
- if (empty($mygroups)) {
- $mygroups = array(-1=>-1);
- } else {
- $mygroups[-1] = -1;
- }
+ $mygroups[-1] = -1;
list ($groups_sql, $groups_params) = $DB->get_in_or_equal($mygroups);
*
* @global object
* @param object $coursemodule
- * @return object|null
+ * @return cached_cm_info|null
*/
function label_get_coursemodule_info($coursemodule) {
global $DB;
$capabilities = array(
+ // Whether the user can see the link to the external tool and follow it.
'mod/lti:view' => array(
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
- 'guest' => CAP_ALLOW,
'student' => CAP_ALLOW,
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
)
),
+ // Add an External tool activity to a course.
'mod/lti:addinstance' => array(
'riskbitmask' => RISK_XSS,
'clonepermissionsfrom' => 'moodle/course:manageactivities'
),
+ // Controls access to the grade.php script, which shows all the submissions
+ // made to the external tool that have been reported back to Moodle.
'mod/lti:grade' => array(
- 'riskbitmask' => RISK_XSS,
+ 'riskbitmask' => RISK_PERSONAL,
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
)
),
+ // When the user arrives at the external tool, if they have this capability
+ // in Moodle, then they given the Instructor role in the remote system,
+ // otherwise they are given Learner. See the lti_get_ims_role function.
'mod/lti:manage' => array(
- 'riskbitmask' => RISK_XSS,
+ 'riskbitmask' => RISK_PERSONAL, // A bit of a guess, but seems likely.
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
)
),
+ // The ability to create or edit tool configurations for particular courses.
'mod/lti:addcoursetool' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
- 'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
)
),
+ // The ability to request the adminstirator to configure a particular
+ // External tool globally.
'mod/lti:requesttooladd' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
- 'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
)
It is possible that browsers will prevent the new window from opening.';
$string['launchoptions'] = 'Launch Options';
$string['lti'] = 'LTI';
-$string['lti:addinstance'] = 'Add a new LTI activity';
-$string['lti:addcoursetool'] = 'Grade LTI activities';
-$string['lti:grade'] = 'Grade LTI activities';
-$string['lti:manage'] = 'Edit LTI activities';
-$string['lti:requesttooladd'] = 'Submit a tool to admins for configuration';
-$string['lti:view'] = 'View LTI activities';
+$string['lti:addinstance'] = 'Add new external tool activities';
+$string['lti:addcoursetool'] = 'Add course-specific tool configurations';
+$string['lti:grade'] = 'View grades returned by the external tool';
+$string['lti:manage'] = 'Be an Instructor when the tool is launched';
+$string['lti:requesttooladd'] = 'Request a tool is configured site-wide';
+$string['lti:view'] = 'Launch external tool activities';
$string['lti_administration'] = 'LTI Administration';
$string['lti_errormsg'] = 'The tool returned the following error message: "{$a}"';
$string['lti_launch_error'] = 'An error occured when launching the external tool: ';
defined('MOODLE_INTERNAL') || die;
+/**
+ * Returns all other caps used in module.
+ *
+ * @return array
+ */
+function lti_get_extra_capabilities() {
+ return array('moodle/site:accessallgroups');
+}
+
/**
* List of features supported in URL module
* @param string $feature FEATURE_xx constant for requested feature
* For this module we just need to support external urls as
* activity icons
*
- * @param cm_info $coursemodule
+ * @param stdClass $coursemodule
* @return cached_cm_info info
*/
function lti_get_coursemodule_info($coursemodule) {
require_once($CFG->dirroot.'/mod/lti/locallib.php');
if (!$lti = $DB->get_record('lti', array('id' => $coursemodule->instance),
- 'icon, secureicon, intro, introformat, name')) {
+ 'icon, secureicon, intro, introformat, name, toolurl, launchcontainer')) {
return null;
}
$info->content = format_module_intro('lti', $lti, $coursemodule->id, false);
}
+ // Does the link open in a new window?
+ $tool = lti_get_tool_by_url_match($lti->toolurl);
+ if ($tool) {
+ $toolconfig = lti_get_type_config($tool->id);
+ } else {
+ $toolconfig = array();
+ }
+ $launchcontainer = lti_get_launch_container($lti, $toolconfig);
+ if ($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW) {
+ $launchurl = new moodle_url('/mod/lti/launch.php', array('id' => $coursemodule->id));
+ $info->onclick = "window.open('" . $launchurl->out(false) . "', 'lti'); return false;";
+ }
+
$info->name = $lti->name;
return $info;
FROM {lti_types_config}
WHERE typeid = :typeid1
UNION ALL
- SELECT 'toolurl' AS name, baseurl AS value
+ SELECT 'toolurl' AS name, " . $DB->sql_compare_text('baseurl', 1333) . " AS value
FROM {lti_types}
WHERE id = :typeid2";
// add standard buttons, common to all modules
$this->add_action_buttons();
- $editurl = new moodle_url("/mod/lti/instructor_edit_tool_type.php?sesskey={$USER->sesskey}&course={$COURSE->id}");
+ $editurl = new moodle_url('/mod/lti/instructor_edit_tool_type.php',
+ array('sesskey' => sesskey(), 'course' => $COURSE->id));
$ajaxurl = new moodle_url('/mod/lti/ajax.php');
$jsinfo = (object)array(
$rawbody = file_get_contents("php://input");
-foreach (getallheaders() as $name => $value) {
+foreach (lti\OAuthUtil::get_headers() as $name => $value) {
if ($name === 'Authorization') {
// TODO: Switch to core oauthlib once implemented - MDL-30149
$oauthparams = lti\OAuthUtil::split_header($value);
*
* See {@link get_array_of_activities()} in course/lib.php
*
- * @param cm_info $coursemodule
+ * @param stdClass $coursemodule
* @return cached_cm_info Info to customise main page display
*/
function page_get_coursemodule_info($coursemodule) {
*
* See {@link get_array_of_activities()} in course/lib.php
*
- * @param cm_info $coursemodule
+ * @param stdClass $coursemodule
* @return cached_cm_info info
*/
function resource_get_coursemodule_info($coursemodule) {
* See {@link get_array_of_activities()} in course/lib.php
*
* @param object $coursemodule
- * @return object info
+ * @return cached_cm_info info
*/
function url_get_coursemodule_info($coursemodule) {
global $CFG, $DB;
}
echo html_writer::label(get_string('participantslist'), 'menuuser', false, array('class' => 'accesshide'));
echo html_writer::select($users, "user", $selecteduser, false);
+ $a = new stdClass();
$a->url = "$CFG->wwwroot/report/log/index.php?chooselog=0&group=$selectedgroup&user=$selecteduser"
."&id=$course->id&date=$selecteddate&modid=$selectedactivity&showusers=1&showcourses=$showcourses";
print_string('logtoomanyusers','moodle',$a);
defined('MOODLE_INTERNAL') || die();
-$version = 2013072600.00; // YYYYMMDD = weekly release date of this DEV branch
+$version = 2013072600.01; // YYYYMMDD = weekly release date of this DEV branch
// RR = release increments - 00 in DEV branches
// .XX = incremental changes