Merge branch 'wip-MDL-40847-m26' of git://github.com/samhemelryk/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 29 Jul 2013 07:28:17 +0000 (15:28 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 29 Jul 2013 07:28:17 +0000 (15:28 +0800)
39 files changed:
backup/moodle2/restore_stepslib.php
backup/upgrade.txt
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/dbops/tests/backup_dbops_test.php
cache/stores/file/lib.php
cache/tests/cache_test.php
grade/report/user/lib.php
lib/behat/lib.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/ddl/mssql_sql_generator.php
lib/dml/mssql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/modinfolib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/setuplib.php
lib/tests/behat/behat_hooks.php
lib/upgrade.txt
lib/weblib.php
mod/assign/externallib.php
mod/assign/feedback/file/importziplib.php
mod/assignment/lib.php
mod/feedback/import_form.php
mod/feedback/lib.php
mod/forum/lib.php
mod/label/lib.php
mod/lti/db/access.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/mod_form.php
mod/lti/service.php
mod/page/lib.php
mod/resource/lib.php
mod/url/lib.php
report/log/locallib.php
version.php

index ed36b0a..2b6cdab 100644 (file)
@@ -510,11 +510,11 @@ class restore_review_pending_block_positions extends restore_execution_step {
 
         // 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
@@ -546,12 +546,12 @@ class restore_process_course_modules_availability extends restore_execution_step
 
         // 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);
@@ -3624,7 +3624,7 @@ class restore_process_file_aliases_queue extends restore_execution_step {
 
         // 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);
@@ -3657,7 +3657,7 @@ class restore_process_file_aliases_queue extends restore_execution_step {
                 $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)
index 586ee70..26e2e00 100644 (file)
@@ -7,6 +7,12 @@ information provided here is intended especially for developers.
   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
index 085fe22..668ff61 100644 (file)
@@ -154,6 +154,44 @@ abstract class backup_controller_dbops extends backup_dbops {
         $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
      */
index 12f2601..0e5fabc 100644 (file)
@@ -152,7 +152,7 @@ abstract class restore_dbops {
         $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
@@ -163,14 +163,13 @@ abstract class restore_dbops {
             // 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
@@ -673,20 +672,21 @@ abstract class restore_dbops {
         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;
@@ -700,15 +700,16 @@ abstract class restore_dbops {
         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;
     }
 
@@ -798,15 +799,15 @@ abstract class restore_dbops {
         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;
     }
 
@@ -893,7 +894,7 @@ abstract class restore_dbops {
         $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 == '.') {
@@ -1018,9 +1019,9 @@ abstract class restore_dbops {
         $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)) {
@@ -1409,9 +1410,9 @@ abstract class restore_dbops {
         }
 
         // 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) {
@@ -1496,7 +1497,7 @@ abstract class restore_dbops {
         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);
     }
@@ -1511,7 +1512,7 @@ abstract class restore_dbops {
             $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);
@@ -1520,8 +1521,9 @@ abstract class restore_dbops {
     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;
@@ -1566,18 +1568,17 @@ abstract class restore_dbops {
         // 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);
                 }
             }
index 55d57ba..e57dc7e 100644 (file)
@@ -147,6 +147,18 @@ class backup_dbops_testcase extends advanced_testcase {
         // 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);
+        }
     }
 
     /**
index 96d11b2..adc1a9b 100644 (file)
@@ -266,7 +266,7 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
         $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;
         }
@@ -314,11 +314,13 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
             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';
         }
index 5121875..1388004 100644 (file)
@@ -119,6 +119,23 @@ class cache_phpunit_tests extends advanced_testcase {
         }
     }
 
+    /**
+     * 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
      */
@@ -191,9 +208,9 @@ class cache_phpunit_tests extends advanced_testcase {
      * @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) {
@@ -850,7 +867,7 @@ class cache_phpunit_tests extends advanced_testcase {
         // 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.
@@ -858,7 +875,7 @@ class cache_phpunit_tests extends advanced_testcase {
         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);
index c5920ff..3526c58 100644 (file)
@@ -365,7 +365,8 @@ class grade_report_user extends grade_report {
                     $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;
                         }
                     }
index 559821e..7374550 100644 (file)
@@ -134,7 +134,7 @@ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
     }
 
     // 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>';
 
index a580d0a..ee19a96 100644 (file)
@@ -2306,5 +2306,12 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2013071500.02);
     }
 
+    if ($oldversion < 2013072600.01) {
+        upgrade_mssql_nvarcharmax();
+        upgrade_mssql_varbinarymax();
+
+        upgrade_main_savepoint(true, 2013072600.01);
+    }
+
     return true;
 }
index 6edc5d8..3d89eb9 100644 (file)
@@ -164,3 +164,109 @@ function upgrade_mysql_fix_unsigned_and_lob_columns() {
         $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.");
+    }
+}
index bbf769d..ccd2d81 100644 (file)
@@ -221,10 +221,10 @@ class mssql_sql_generator extends sql_generator {
                 $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';
index 862b055..2191b89 100644 (file)
@@ -464,9 +464,15 @@ class mssql_native_moodle_database extends moodle_database {
             // 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;
@@ -573,6 +579,7 @@ class mssql_native_moodle_database extends moodle_database {
                 $type = 'X';
                 break;
             case 'IMAGE':
+            case 'VARBINARY':
             case 'VARBINARY(MAX)':
                 $type = 'B';
                 break;
index 13c46e4..b5062b1 100644 (file)
@@ -528,9 +528,15 @@ class sqlsrv_native_moodle_database extends moodle_database {
             // 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;
@@ -645,6 +651,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
            break;
 
           case 'IMAGE':
+          case 'VARBINARY':
           case 'VARBINARY(MAX)':
            $type = 'B';
            break;
index 1355b08..ac79c12 100644 (file)
@@ -1203,7 +1203,8 @@ class cm_info extends stdClass {
         }
 
         // 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.
@@ -1234,6 +1235,23 @@ class cm_info extends stdClass {
         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
      *
index 3c90a27..4017783 100644 (file)
@@ -2405,7 +2405,7 @@ EOD;
             $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)) {
@@ -2552,10 +2552,11 @@ EOD;
      * @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();
     }
 
     /**
@@ -2563,12 +2564,14 @@ EOD;
      *
      * @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);
     }
 
     /**
index 49551c0..25e2de2 100644 (file)
@@ -1657,6 +1657,7 @@ class YUI_config {
         $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.
index e08d3c4..5da90ae 100644 (file)
@@ -665,7 +665,7 @@ function format_backtrace($callers, $plaintext = false) {
         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()
@@ -1260,10 +1260,15 @@ function make_writable_directory($dir, $exceptiononerror = true) {
 
     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;
+                }
             }
         }
     }
index 6c5efe4..f487c8f 100644 (file)
@@ -244,14 +244,13 @@ class behat_hooks extends behat_base {
         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);
index d0a4192..d2591e2 100644 (file)
@@ -11,6 +11,8 @@ information provided here is intended especially for developers.
 * 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
index 24f5a93..c9b6175 100644 (file)
@@ -2820,7 +2820,7 @@ function debugging($message = '', $level = DEBUG_NORMAL, $backtrace = null) {
             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 {
index b168b20..7c29a61 100644 (file)
@@ -279,7 +279,8 @@ class mod_assign_external extends external_api {
         $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();
@@ -333,6 +334,10 @@ class mod_assign_external extends external_api {
                         '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
                     );
@@ -364,6 +369,7 @@ class mod_assign_external extends external_api {
         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'),
@@ -381,6 +387,10 @@ class mod_assign_external extends external_api {
                 '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');
index a6c77a5..840f5ac 100644 (file)
@@ -194,6 +194,17 @@ class assignfeedback_file_zip_importer {
                                           $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;
     }
 
@@ -205,7 +216,7 @@ class assignfeedback_file_zip_importer {
      * @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');
@@ -216,11 +227,7 @@ class assignfeedback_file_zip_importer {
         $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);
index 814e3c9..82ea132 100644 (file)
@@ -3595,7 +3595,7 @@ function assignment_get_all_submissions($assignment, $sort="", $dir="DESC") {
  * 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) {
index 97d4ef0..8a06e4e 100644 (file)
@@ -34,18 +34,14 @@ class feedback_import_form extends moodleform {
         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');
index 1bc946d..affb79e 100644 (file)
@@ -38,6 +38,15 @@ define('FEEDBACK_RESETFORM_DROP', 'feedback_drop_feedback_');
 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
index f75d41a..245179d 100644 (file)
@@ -1521,11 +1521,7 @@ function forum_print_recent_activity($course, $viewfullnames, $timestart) {
                     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;
                 }
             }
@@ -1910,9 +1906,6 @@ function forum_get_readable_forums($userid, $courseid=0) {
     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?
@@ -1936,15 +1929,9 @@ function forum_get_readable_forums($userid, $courseid=0) {
 
          /// 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
@@ -2505,22 +2492,11 @@ function forum_count_discussions($forum, $cm, $course) {
     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;
@@ -6049,12 +6025,10 @@ function forum_get_recent_mod_activity(&$activities, &$index, $timestart, $cours
     }
 
     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');
@@ -6065,7 +6039,6 @@ function forum_get_recent_mod_activity(&$activities, &$index, $timestart, $cours
                                               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
@@ -6077,10 +6050,6 @@ function forum_get_recent_mod_activity(&$activities, &$index, $timestart, $cours
     $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) {
 
@@ -6101,7 +6070,7 @@ function forum_get_recent_mod_activity(&$activities, &$index, $timestart, $cours
                     continue;
                 }
 
-                if (!array_key_exists($post->groupid, $modinfo->groups[0])) {
+                if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) {
                     continue;
                 }
             }
@@ -6778,22 +6747,11 @@ function forum_tp_count_forum_unread_posts($cm, $course) {
     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);
 
index 224f74c..0510643 100644 (file)
@@ -118,7 +118,7 @@ function label_delete_instance($id) {
  *
  * @global object
  * @param object $coursemodule
- * @return object|null
+ * @return cached_cm_info|null
  */
 function label_get_coursemodule_info($coursemodule) {
     global $DB;
index 51c676d..647b83f 100644 (file)
@@ -31,11 +31,11 @@ defined('MOODLE_INTERNAL') || die;
 
 $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,
@@ -43,6 +43,7 @@ $capabilities = array(
         )
     ),
 
+    // Add an External tool activity to a course.
     'mod/lti:addinstance' => array(
         'riskbitmask' => RISK_XSS,
 
@@ -55,8 +56,10 @@ $capabilities = array(
         '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,
@@ -67,8 +70,11 @@ $capabilities = array(
         )
     ),
 
+    // 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,
@@ -79,21 +85,22 @@ $capabilities = array(
         )
     ),
 
+    // 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
         )
index 337a87f..2c43bb6 100644 (file)
@@ -216,12 +216,12 @@ real estate to the tool, and others provide a more integrated feel with the Mood
         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: ';
index fa74bbc..c7e1b92 100644 (file)
 
 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
@@ -169,7 +178,7 @@ function lti_delete_instance($id) {
  * 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) {
@@ -177,7 +186,7 @@ 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;
     }
 
@@ -196,6 +205,19 @@ function lti_get_coursemodule_info($coursemodule) {
         $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;
index 81c40fd..d54ab66 100644 (file)
@@ -505,7 +505,7 @@ function lti_get_type_config($typeid) {
                 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";
 
index 29a482d..aa5a728 100644 (file)
@@ -195,7 +195,8 @@ class mod_lti_mod_form extends moodleform_mod {
         // 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(
index fad61e6..b96f566 100644 (file)
@@ -33,7 +33,7 @@ use moodle\mod\lti as lti;
 
 $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);
index 0f03e2a..9216337 100644 (file)
@@ -238,7 +238,7 @@ function page_user_complete($course, $user, $mod, $page) {
  *
  * 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) {
index e102b37..f76a1f9 100644 (file)
@@ -225,7 +225,7 @@ function resource_user_complete($course, $user, $mod, $resource) {
  *
  * 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) {
index 012847d..8433058 100644 (file)
@@ -243,7 +243,7 @@ function url_user_complete($course, $user, $mod, $url) {
  * 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;
index 2b55a8a..30d1404 100644 (file)
@@ -349,6 +349,7 @@ function report_log_print_mnet_selector_form($hostid, $course, $selecteduser=0,
         }
         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);
index f289eb5..3fe6bc2 100644 (file)
@@ -29,7 +29,7 @@
 
 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