Merge branch 'install_master' of git://github.com/amosbot/moodle
authorDan Poltawski <dan@moodle.com>
Fri, 1 Jun 2012 09:01:41 +0000 (17:01 +0800)
committerDan Poltawski <dan@moodle.com>
Fri, 1 Jun 2012 09:01:41 +0000 (17:01 +0800)
141 files changed:
admin/cli/install.php
admin/cli/install_database.php
admin/cli/mysql_engine.php
admin/cli/upgrade.php
admin/index.php
admin/oauth2callback.php [moved from portfolio/googledocs/db/events.php with 52% similarity]
admin/renderer.php
admin/tool/bloglevelupgrade/index.php
auth/shibboleth/index.php
backup/moodle2/backup_stepslib.php
backup/moodle2/backup_xml_transformer.class.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_cron_helper.class.php
backup/util/plan/base_plan.class.php
backup/util/ui/backup_ui_stage.class.php
backup/util/ui/base_moodleform.class.php
calendar/lib.php
course/changenumsections.php
course/format/renderer.php
course/format/topics/format.js
course/format/topics/lib.php
course/format/weeks/format.js
course/format/weeks/lib.php
course/lib.php
course/recent.php
course/rest.php
course/view.php
course/yui/coursebase/coursebase.js
course/yui/dragdrop/dragdrop.js
files/renderer.php
grade/lib.php
lang/en/access.php
lang/en/admin.php
lang/en/moodle.php
lang/en/plugin.php
lib/completion/completion_criteria_activity.php
lib/completion/completion_criteria_course.php
lib/completion/completion_criteria_date.php
lib/completion/completion_criteria_duration.php
lib/completion/completion_criteria_grade.php
lib/conditionlib.php
lib/db/install.xml
lib/db/log.php
lib/db/upgrade.php
lib/dml/mssql_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/filebrowser/file_info_stored.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/filestorage/tests/file_storage_test.php
lib/form/dndupload.js
lib/form/filemanager.js
lib/form/filepicker.js
lib/gdlib.php
lib/googleapi.php
lib/installlib.php
lib/mathslib.php
lib/moodlelib.php
lib/oauthlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/pluginlib.php
lib/portfolio/exporter.php
lib/portfolio/forms.php
lib/setup.php
lib/tests/conditionlib_test.php
lib/upgradelib.php
lib/yui/blocks/blocks.js
lib/yui/dragdrop/dragdrop.js
mod/assign/assignmentplugin.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/file/locallib.php
mod/assign/locallib.php
mod/assign/settings.php
mod/assign/submission/comments/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/book/lang/en/book.php
mod/book/locallib.php
mod/book/mod_form.php
mod/book/settings.php
mod/book/styles.css
mod/book/tool/exportimscp/lang/en/booktool_exportimscp.php
mod/book/tool/importhtml/lang/en/booktool_importhtml.php
mod/book/version.php
mod/folder/edit.php
mod/forum/rsslib.php
mod/forum/subscribe.php
mod/quiz/module.js
mod/resource/lib.php
mod/scorm/datamodels/aicclib.php
pix/u/f3.png [new file with mode: 0644]
portfolio/add.php
portfolio/googledocs/db/upgrade.php [new file with mode: 0644]
portfolio/googledocs/lang/en/portfolio_googledocs.php
portfolio/googledocs/lib.php
portfolio/googledocs/version.php
portfolio/picasa/db/events.php [deleted file]
portfolio/picasa/db/upgrade.php [new file with mode: 0644]
portfolio/picasa/lang/en/portfolio_picasa.php
portfolio/picasa/lib.php
portfolio/picasa/version.php
question/import_form.php
question/type/essay/db/upgrade.php
question/type/essay/renderer.php
question/type/essay/version.php
report/log/index.php
repository/coursefiles/lib.php
repository/googledocs/db/upgrade.php [new file with mode: 0644]
repository/googledocs/lang/en/repository_googledocs.php
repository/googledocs/lib.php
repository/googledocs/version.php
repository/picasa/db/upgrade.php [new file with mode: 0644]
repository/picasa/lang/en/repository_picasa.php
repository/picasa/lib.php
repository/picasa/version.php
theme/afterburner/config.php
theme/anomaly/config.php
theme/arialist/config.php
theme/base/config.php
theme/boxxie/config.php
theme/boxxie/style/boilerplate.css [deleted file]
theme/canvas/config.php
theme/formal_white/config.php
theme/formal_white/style/formal_white.css
theme/formfactor/config.php
theme/leatherbound/config.php
theme/magazine/config.php
theme/serenity/layout/embedded.php [deleted file]
theme/serenity/layout/frontpage.php [deleted file]
theme/serenity/layout/general.php [deleted file]
theme/splash/config.php
theme/standardold/config.php
theme/upgrade.txt
user/files.php
user/files_form.php
version.php

index dc65e31..79ff0b7 100644 (file)
@@ -425,7 +425,8 @@ if (isset($maturity)) {
                 exit(1);
             }
         } else {
-            cli_error(get_string('maturitycorewarning', 'admin'));
+            cli_problem(get_string('maturitycorewarning', 'admin', $maturitylevel));
+            cli_error(get_string('maturityallowunstable', 'admin'));
         }
     }
 }
@@ -677,7 +678,9 @@ if (!$envstatus) {
 
 // Test plugin dependencies.
 require_once($CFG->libdir . '/pluginlib.php');
-if (!plugin_manager::instance()->all_plugins_ok($version)) {
+$failed = array();
+if (!plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+    cli_problem(get_string('pluginscheckfailed', 'admin', array('pluginslist' => implode(', ', array_unique($failed)))));
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
 
index 2524c41..313f352 100644 (file)
@@ -161,7 +161,9 @@ if (!$envstatus) {
 
 // Test plugin dependencies.
 require_once($CFG->libdir . '/pluginlib.php');
-if (!plugin_manager::instance()->all_plugins_ok($version)) {
+$failed = array();
+if (!plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+    cli_problem(get_string('pluginscheckfailed', 'admin', array('pluginslist' => implode(', ', array_unique($failed)))));
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
 
index 3b51f41..6bc3642 100644 (file)
@@ -34,8 +34,8 @@ if ($DB->get_dbfamily() !== 'mysql') {
 }
 
 // now get cli options
-list($options, $unrecognized) = cli_get_params(array('help'=>false, 'list'=>false, 'engine'=>false),
-                                               array('h'=>'help', 'l'=>'list'));
+list($options, $unrecognized) = cli_get_params(array('help'=>false, 'list'=>false, 'engine'=>false, 'available'=>false),
+                                               array('h'=>'help', 'l'=>'list', 'a'=>'available'));
 
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
@@ -52,6 +52,7 @@ and does not support transactions.
 Options:
 --engine=ENGINE       Convert MySQL tables to different engine
 -l, --list            Show table information
+-a, --available       Show list of available engines
 -h, --help            Print out this help
 
 Example:
@@ -59,7 +60,11 @@ Example:
 ";
 
 if (!empty($options['engine'])) {
+    $engines = mysql_get_engines();
     $engine = clean_param($options['engine'], PARAM_ALPHA);
+    if (!isset($engines[strtoupper($engine)])) {
+        cli_error("Error: engine '$engine' is not available on this server!");
+    }
 
     echo "Converting tables to '$engine' for $CFG->wwwroot:\n";
     $prefix = $DB->get_prefix();
@@ -68,9 +73,11 @@ if (!empty($options['engine'])) {
     $rs = $DB->get_recordset_sql($sql);
     $converted = 0;
     $skipped   = 0;
+    $errors    = 0;
     foreach ($rs as $table) {
-        if ($table->engine === $engine) {
-            echo str_pad($table->name, 40). " - NO CONVERSION NEEDED\n";
+        if (strtoupper($table->engine) === strtoupper($engine)) {
+            $newengine = mysql_get_table_engine($table->name);
+            echo str_pad($table->name, 40). " - NO CONVERSION NEEDED ($newengine)\n";
             $skipped++;
             continue;
         }
@@ -78,16 +85,22 @@ if (!empty($options['engine'])) {
 
         try {
             $DB->change_database_structure("ALTER TABLE {$table->name} ENGINE = $engine");
+            $newengine = mysql_get_table_engine($table->name);
+            if (strtoupper($newengine) !== strtoupper($engine)) {
+                echo "ERROR ($newengine)\n";
+                $errors++;
+                continue;
+            }
+            echo "DONE ($newengine)\n";
+            $converted++;
         } catch (moodle_exception $e) {
             echo $e->getMessage()."\n";
-            $skipped++;
+            $errors++;
             continue;
         }
-        echo "DONE\n";
-        $converted++;
     }
     $rs->close();
-    echo "Converted: $converted, skipped: $skipped\n";
+    echo "Converted: $converted, skipped: $skipped, errors: $errors\n";
     exit(0); // success
 
 } else if (!empty($options['list'])) {
@@ -115,7 +128,53 @@ if (!empty($options['engine'])) {
     }
     exit(0); // success
 
+} else if (!empty($options['available'])) {
+    echo "List of available MySQL engines for $CFG->wwwroot:\n";
+    $engines = mysql_get_engines();
+    foreach ($engines as $engine) {
+        echo " $engine\n";
+    }
+    die;
+
 } else {
     echo $help;
     die;
 }
+
+
+
+// ========== Some functions ==============
+
+function mysql_get_engines() {
+    global $DB;
+
+    $sql = "SHOW Engines";
+    $rs = $DB->get_recordset_sql($sql);
+    $engines = array();
+    foreach ($rs as $engine) {
+        if (strtoupper($engine->support) !== 'YES' and strtoupper($engine->support) !== 'DEFAULT') {
+            continue;
+        }
+        $engines[strtoupper($engine->engine)] = $engine->engine;
+        if (strtoupper($engine->support) === 'DEFAULT') {
+            $engines[strtoupper($engine->engine)] .= ' (default)';
+        }
+    }
+    $rs->close();
+
+    return $engines;
+}
+
+function mysql_get_table_engine($tablename) {
+    global $DB;
+
+    $engine = null;
+    $sql = "SHOW TABLE STATUS WHERE Name = '$tablename'"; // no special chars expected here
+    $rs = $DB->get_recordset_sql($sql);
+    if ($rs->valid()) {
+        $record = $rs->current();
+        $engine = $record->engine;
+    }
+    $rs->close();
+    return $engine;
+}
index 33e66cc..1fae897 100644 (file)
@@ -109,7 +109,9 @@ if (!$envstatus) {
 }
 
 // Test plugin dependencies.
-if (!plugin_manager::instance()->all_plugins_ok($version)) {
+$failed = array();
+if (!plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+    cli_problem(get_string('pluginscheckfailed', 'admin', array('pluginslist' => implode(', ', array_unique($failed)))));
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
 
@@ -132,7 +134,8 @@ if (isset($maturity)) {
             echo get_string('morehelp') . ': ' . get_docs_url('admin/versions') . PHP_EOL;
             cli_separator();
         } else {
-            cli_error(get_string('maturitycorewarning', 'admin', $maturitylevel));
+            cli_problem(get_string('maturitycorewarning', 'admin', $maturitylevel));
+            cli_error(get_string('maturityallowunstable', 'admin'));
         }
     }
 }
index 5b01be2..bf3eea5 100644 (file)
@@ -152,6 +152,20 @@ if (!core_tables_exist()) {
         die();
     }
 
+    // check plugin dependencies
+    $failed = array();
+    if (!plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+        $PAGE->navbar->add(get_string('pluginscheck', 'admin'));
+        $PAGE->set_title($strinstallation);
+        $PAGE->set_heading($strinstallation . ' - Moodle ' . $CFG->target_release);
+
+        $output = $PAGE->get_renderer('core', 'admin');
+        $url = new moodle_url('/admin/index.php', array('agreelicense' => 1, 'confirmrelease' => 1, 'lang' => $CFG->lang));
+        echo $output->unsatisfied_dependencies_page($version, $failed, $url);
+        die();
+    }
+    unset($failed);
+
     //TODO: add a page with list of non-standard plugins here
 
     $strdatabasesetup = get_string('databasesetup');
@@ -238,6 +252,15 @@ if ($version > $CFG->version) {  // upgrade
 
         $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1));
 
+        // check plugin dependencies first
+        $failed = array();
+        if (!plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+            $output = $PAGE->get_renderer('core', 'admin');
+            echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
+            die();
+        }
+        unset($failed);
+
         if ($fetchupdates) {
             // no sesskey support guaranteed here
             if (empty($CFG->disableupdatenotifications)) {
@@ -290,6 +313,16 @@ if (moodle_needs_upgrading()) {
             }
 
             $output = $PAGE->get_renderer('core', 'admin');
+
+            // check plugin dependencies first
+            $failed = array();
+            if (!plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+                echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
+                die();
+            }
+            unset($failed);
+
+            // dependencies check passed, let's rock!
             echo $output->upgrade_plugin_check_page(plugin_manager::instance(), available_update_checker::instance(),
                     $version, $showallplugins,
                     new moodle_url($PAGE->url),
similarity index 52%
rename from portfolio/googledocs/db/events.php
rename to admin/oauth2callback.php
index b0d3d0c..364c002 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Add event handlers for the googledocs portfolio.
+ * An oauth2 redirection endpoint which can be used for an application:
+ * http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-3.1.2
+ *
+ * This is used because some oauth servers will not allow a redirect urls
+ * with get params (like repository callback) and that needs to be called
+ * using the state param.
  *
- * @package    portfolio_googledocs
- * @category   event
- * @copyright  2009 Penny Leach
+ * @package    core
+ * @copyright  2012 Dan Poltawski
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$handlers = array (
-    'user_deleted' => array (
-         'handlerfile'      => '/portfolio/googledocs/lib.php',
-         'handlerfunction'  => 'portfolio_googledocs_user_deleted',
-         'schedule'         => 'cron',
-         'internal'         => 0,
-     ),
-);
+require_once(dirname(dirname(__FILE__)).'/config.php');
 
+// The authorization code generated by the authorization server.
+$code = required_param('code', PARAM_RAW);
+// The state parameter we've given (used in moodle as a redirect url).
+$state = required_param('state', PARAM_URL);
 
+redirect(new moodle_url($state, array('code' => $code)));
index b7a1bab..9b689a6 100644 (file)
@@ -107,6 +107,29 @@ class core_admin_renderer extends plugin_renderer_base {
         return $output;
     }
 
+    /**
+     * Displays the list of plugins with unsatisfied dependencies
+     *
+     * @param double|string|int $version Moodle on-disk version
+     * @param array $failed list of plugins with unsatisfied dependecies
+     * @param moodle_url $reloadurl URL of the page to recheck the dependencies
+     * @return string HTML
+     */
+    public function unsatisfied_dependencies_page($version, array $failed, moodle_url $reloadurl) {
+        $output = '';
+
+        $output .= $this->header();
+        $output .= $this->heading(get_string('pluginscheck', 'admin'));
+        $output .= $this->warning(get_string('pluginscheckfailed', 'admin', array('pluginslist' => implode(', ', array_unique($failed)))));
+        $output .= $this->plugins_check_table(plugin_manager::instance(), $version, array('xdep' => true));
+        $output .= $this->warning(get_string('pluginschecktodo', 'admin'));
+        $output .= $this->continue_button($reloadurl);
+
+        $output .= $this->footer();
+
+        return $output;
+    }
+
     /**
      * Display the 'You are about to upgrade Moodle' page. The first page
      * during upgrade.
@@ -197,19 +220,15 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->box_end();
         $output .= $this->upgrade_reload($reloadurl);
 
-        if ($pluginman->all_plugins_ok($version)) {
-            if ($pluginman->some_plugins_updatable()) {
-                $output .= $this->container_start('upgradepluginsinfo');
-                $output .= $this->help_icon('upgradepluginsinfo', 'core_admin', get_string('upgradepluginsfirst', 'core_admin'));
-                $output .= $this->container_end();
-            }
-            $button = new single_button($continueurl, get_string('upgradestart', 'admin'), 'get');
-            $button->class = 'continuebutton';
-            $output .= $this->render($button);
-        } else {
-            $output .= $this->box(get_string('pluginschecktodo', 'admin'), 'environmentbox errorbox');
+        if ($pluginman->some_plugins_updatable()) {
+            $output .= $this->container_start('upgradepluginsinfo');
+            $output .= $this->help_icon('upgradepluginsinfo', 'core_admin', get_string('upgradepluginsfirst', 'core_admin'));
+            $output .= $this->container_end();
         }
 
+        $button = new single_button($continueurl, get_string('upgradestart', 'admin'), 'get');
+        $button->class = 'continuebutton';
+        $output .= $this->render($button);
         $output .= $this->footer();
 
         return $output;
@@ -563,13 +582,14 @@ class core_admin_renderer extends plugin_renderer_base {
      * This default implementation renders all plugins into one big table. The rendering
      * options support:
      *     (bool)full = false: whether to display up-to-date plugins, too
+     *     (bool)xdep = false: display the plugins with unsatisified dependecies only
      *
      * @param plugin_manager $pluginman provides information about the plugins.
      * @param int $version the version of the Moodle code from version.php.
      * @param array $options rendering options
      * @return string HTML code
      */
-    public function plugins_check_table(plugin_manager $pluginman, $version, array $options = null) {
+    public function plugins_check_table(plugin_manager $pluginman, $version, array $options = array()) {
         global $CFG;
 
         $plugininfo = $pluginman->get_plugins();
@@ -578,11 +598,8 @@ class core_admin_renderer extends plugin_renderer_base {
             return '';
         }
 
-        if (empty($options)) {
-            $options = array(
-                'full' => false,
-            );
-        }
+        $options['full'] = isset($options['full']) ? (bool)$options['full'] : false;
+        $options['xdep'] = isset($options['xdep']) ? (bool)$options['xdep'] : false;
 
         $table = new html_table();
         $table->id = 'plugins-check';
@@ -666,16 +683,28 @@ class core_admin_renderer extends plugin_renderer_base {
 
                 $statusisboring = in_array($statuscode, array(
                         plugin_manager::PLUGIN_STATUS_NODB, plugin_manager::PLUGIN_STATUS_UPTODATE));
-                $dependenciesok = $pluginman->are_dependencies_satisfied(
-                        $plugin->get_other_required_plugins());
-                if ($isstandard and $statusisboring and $dependenciesok and empty($availableupdates)) {
+
+                $coredependency = $plugin->is_core_dependency_satisfied($version);
+                $otherpluginsdependencies = $pluginman->are_dependencies_satisfied($plugin->get_other_required_plugins());
+                $dependenciesok = $coredependency && $otherpluginsdependencies;
+
+                if ($options['xdep']) {
+                    // we want to see only plugins with failed dependencies
+                    if ($dependenciesok) {
+                        continue;
+                    }
+
+                } else if ($isstandard and $statusisboring and $dependenciesok and empty($availableupdates)) {
+                    // no change is going to happen to the plugin - display it only
+                    // if the user wants to see the full list
                     if (empty($options['full'])) {
                         continue;
                     }
-                } else {
-                    $numofhighlighted[$type]++;
                 }
 
+                // ok, the plugin should be displayed
+                $numofhighlighted[$type]++;
+
                 $row->cells = array($displayname, $rootdir, $source,
                     $versiondb, $versiondisk, $requires, $status);
                 $plugintyperows[] = $row;
@@ -691,7 +720,11 @@ class core_admin_renderer extends plugin_renderer_base {
 
         $sumofhighlighted = array_sum($numofhighlighted);
 
-        if ($sumofhighlighted == 0) {
+        if ($options['xdep']) {
+            // we do not want to display no heading and links in this mode
+            $out = '';
+
+        } else if ($sumofhighlighted == 0) {
             $out  = $this->output->container_start('nonehighlighted', 'plugins-check-info');
             $out .= $this->output->heading(get_string('nonehighlighted', 'core_plugin'));
             if (empty($options['full'])) {
index 9c87eec..bd2a531 100644 (file)
@@ -121,7 +121,7 @@ function bloglevelupgrade_entries($blogentries, $forum, $cm, $groupid=-1) {
         $discussion->groupid = $groupid;
         $message = '';
 
-        $discussionid = forum_add_discussion($discussion, null, $message);
+        $discussionid = forum_add_discussion($discussion, null, $message, $blogentry->userid);
 
         // Copy file attachment records
         $fs = get_file_storage();
index 7bc8f90..a1e4a1c 100644 (file)
@@ -30,6 +30,7 @@
 
 /// If we can find the Shibboleth attribute, save it in session and return to main login page
     if (!empty($_SERVER[$pluginconfig->user_attribute])) {    // Shibboleth auto-login
+        $frm = new stdClass();
         $frm->username = strtolower($_SERVER[$pluginconfig->user_attribute]);
         $frm->password = substr(base64_encode($_SERVER[$pluginconfig->user_attribute]),0,8);
         // The random password consists of the first 8 letters of the base 64 encoded user ID
index d1be602..a2fa948 100644 (file)
@@ -1414,7 +1414,7 @@ class backup_final_files_structure_step extends backup_structure_step {
 
         $file->set_source_sql("SELECT f.*, r.repositoryid, r.reference
                                  FROM {files} f
-                                 JOIN {files_reference} r
+                                 LEFT JOIN {files_reference} r
                                       ON r.id = f.referencefileid
                                  JOIN {backup_ids_temp} bi
                                       ON f.id = bi.itemid
index a3770d0..25c8503 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+// Cache for storing link encoders, so that we don't need to call
+// register_link_encoders each time backup_xml_transformer is constructed
+// TODO MDL-25290 replace global with MUC code.
+global $LINKS_ENCODERS_CACHE;
+
+$LINKS_ENCODERS_CACHE = array();
+
 /**
  * Class implementing the @xml_contenttrasnformed logic to be applied in moodle2 backups
  *
@@ -131,7 +138,19 @@ class backup_xml_transformer extends xml_contenttransformer {
         return $result;
     }
 
+    /**
+     * Register all available content link encoders
+     *
+     * @return array encoder
+     * @todo MDL-25290 replace LINKS_ENCODERS_CACHE global with MUC code
+     */
     private function register_link_encoders() {
+        global $LINKS_ENCODERS_CACHE;
+        // If encoder is linked, then return cached encoder.
+        if (!empty($LINKS_ENCODERS_CACHE)) {
+            return $LINKS_ENCODERS_CACHE;
+        }
+
         $encoders = array();
 
         // Add the course encoder
@@ -160,6 +179,7 @@ class backup_xml_transformer extends xml_contenttransformer {
         // Add local encodes
         // TODO: Any interest? 1.9 never had that.
 
+        $LINKS_ENCODERS_CACHE = $encoders;
         return $encoders;
     }
 }
index e8831d7..377011e 100644 (file)
@@ -135,6 +135,7 @@ class restore_final_task extends restore_task {
         $rules[] = new restore_log_rule('course', 'report outline', 'report/outline/index.php?id={course}', '{course}');
         $rules[] = new restore_log_rule('course', 'report participation', 'report/participation/index.php?id={course}', '{course}');
         $rules[] = new restore_log_rule('course', 'report stats', 'report/stats/index.php?id={course}', '{course}');
+        $rules[] = new restore_log_rule('course', 'view section', 'view.php?id={course}&section={course_sectionnumber}', '{course_section}');
 
         // module 'user' rules
         $rules[] = new restore_log_rule('user', 'view', 'view.php?id={user}&course={course}', '{user}');
index 4d610e5..d87cdb9 100644 (file)
@@ -1047,6 +1047,7 @@ class restore_section_structure_step extends restore_structure_step {
         global $CFG, $DB;
         $data = (object)$data;
         $oldid = $data->id; // We'll need this later
+        $oldsection = $data->number;
 
         $restorefiles = false;
 
@@ -1099,10 +1100,12 @@ class restore_section_structure_step extends restore_structure_step {
 
             $DB->update_record('course_sections', $section);
             $newitemid = $secrec->id;
+            $oldsection = $secrec->section;
         }
 
         // Annotate the section mapping, with restorefiles option if needed
         $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
+        $this->set_mapping('course_sectionnumber', $oldsection, $section->section, $restorefiles);
 
         // set the new course_section id in the task
         $this->task->set_sectionid($newitemid);
@@ -2541,7 +2544,7 @@ class restore_module_structure_step extends restore_structure_step {
 
         $data = (object)$data;
         $oldid = $data->id;
-
+        $oldsection = $data->sectionnumber;
         $this->task->set_old_moduleversion($data->version);
 
         $data->course = $this->task->get_courseid();
@@ -2568,6 +2571,7 @@ class restore_module_structure_step extends restore_structure_step {
                 'course' => $this->get_courseid(),
                 'section' => 1);
             $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
+            $this->set_mapping('course_sectionnumber', $oldsection, $sectionrec->section, $restorefiles);
         }
         $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
         if (!$CFG->enablegroupmembersonly) {                                         // observe groupsmemberonly
index ee98987..68c1752 100644 (file)
@@ -420,7 +420,7 @@ abstract class backup_controller_dbops extends backup_dbops {
 
         $sql = "SELECT count(r.repositoryid)
                   FROM {files} f
-                  JOIN {files_reference} r
+                  LEFT JOIN {files_reference} r
                        ON r.id = f.referencefileid
                   JOIN {backup_ids_temp} bi
                        ON f.id = bi.itemid
index e50c6e6..81dbaf6 100644 (file)
  * TODO: Finish phpdocs
  */
 abstract class restore_dbops {
+    /**
+     * Keep cache of backup records.
+     * @var array
+     * @todo MDL-25290 static should be replaced with MUC code.
+     */
+    private static $backupidscache = array();
+    /**
+     * Keep track of backup ids which are cached.
+     * @var array
+     * @todo MDL-25290 static should be replaced with MUC code.
+     */
+    private static $backupidsexist = array();
+    /**
+     * Count is expensive, so manually keeping track of
+     * backupidscache, to avoid memory issues.
+     * @var int
+     * @todo MDL-25290 static should be replaced with MUC code.
+     */
+    private static $backupidscachesize = 2048;
+    /**
+     * Count is expensive, so manually keeping track of
+     * backupidsexist, to avoid memory issues.
+     * @var int
+     * @todo MDL-25290 static should be replaced with MUC code.
+     */
+    private static $backupidsexistsize = 10240;
+    /**
+     * Slice backupids cache to add more data.
+     * @var int
+     * @todo MDL-25290 static should be replaced with MUC code.
+     */
+    private static $backupidsslice = 512;
 
     /**
      * Return one array containing all the tasks that have been included
@@ -151,6 +183,135 @@ abstract class restore_dbops {
         return $problems;
     }
 
+    /**
+     * Return cached backup id's
+     *
+     * @param int $restoreid id of backup
+     * @param string $itemname name of the item
+     * @param int $itemid id of item
+     * @return array backup id's
+     * @todo MDL-25290 replace static backupids* with MUC code
+     */
+    protected static function get_backup_ids_cached($restoreid, $itemname, $itemid) {
+        global $DB;
+
+        $key = "$itemid $itemname $restoreid";
+
+        // If record exists in cache then return.
+        if (isset(self::$backupidsexist[$key]) && isset(self::$backupidscache[$key])) {
+            // Return a copy of cached data, to avoid any alterations in cached data.
+            return clone self::$backupidscache[$key];
+        }
+
+        // Clean cache, if it's full.
+        if (self::$backupidscachesize <= 0) {
+            // Remove some records, to keep memory in limit.
+            self::$backupidscache = array_slice(self::$backupidscache, self::$backupidsslice, null, true);
+            self::$backupidscachesize = self::$backupidscachesize + self::$backupidsslice;
+        }
+        if (self::$backupidsexistsize <= 0) {
+            self::$backupidsexist = array_slice(self::$backupidsexist, self::$backupidsslice, null, true);
+            self::$backupidsexistsize = self::$backupidsexistsize + self::$backupidsslice;
+        }
+
+        // Retrive record from database.
+        $record = array(
+            'backupid' => $restoreid,
+            'itemname' => $itemname,
+            'itemid'   => $itemid
+        );
+        if ($dbrec = $DB->get_record('backup_ids_temp', $record)) {
+            self::$backupidsexist[$key] = $dbrec->id;
+            self::$backupidscache[$key] = $dbrec;
+            self::$backupidscachesize--;
+            self::$backupidsexistsize--;
+            return $dbrec;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Cache backup ids'
+     *
+     * @param int $restoreid id of backup
+     * @param string $itemname name of the item
+     * @param int $itemid id of item
+     * @param array $extrarecord extra record which needs to be updated
+     * @return void
+     * @todo MDL-25290 replace static BACKUP_IDS_* with MUC code
+     */
+    protected static function set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord) {
+        global $DB;
+
+        $key = "$itemid $itemname $restoreid";
+
+        $record = array(
+            'backupid' => $restoreid,
+            'itemname' => $itemname,
+            'itemid'   => $itemid,
+        );
+
+        // If record is not cached then add one.
+        if (!isset(self::$backupidsexist[$key])) {
+            // If we have this record in db, then just update this.
+            if ($existingrecord = $DB->get_record('backup_ids_temp', $record)) {
+                self::$backupidsexist[$key] = $existingrecord->id;
+                self::$backupidsexistsize--;
+                self::update_backup_cached_record($record, $extrarecord, $key, $existingrecord);
+            } else {
+                // Add new record to cache and db.
+                $recorddefault = array (
+                    'newitemid' => 0,
+                    'parentitemid' => null,
+                    'info' => null);
+                $record = array_merge($record, $recorddefault, $extrarecord);
+                $record['id'] = $DB->insert_record('backup_ids_temp', $record);
+                self::$backupidsexist[$key] = $record['id'];
+                self::$backupidsexistsize--;
+                if (self::$backupidscachesize > 0) {
+                    // Cache new records if we haven't got many yet.
+                    self::$backupidscache[$key] = (object) $record;
+                    self::$backupidscachesize--;
+                }
+            }
+        } else {
+            self::update_backup_cached_record($record, $extrarecord, $key);
+        }
+    }
+
+    /**
+     * Updates existing backup record
+     *
+     * @param array $record record which needs to be updated
+     * @param array $extrarecord extra record which needs to be updated
+     * @param string $key unique key which is used to identify cached record
+     * @param stdClass $existingrecord (optional) existing record
+     */
+    protected static function update_backup_cached_record($record, $extrarecord, $key, $existingrecord = null) {
+        global $DB;
+        // Update only if extrarecord is not empty.
+        if (!empty($extrarecord)) {
+            $extrarecord['id'] = self::$backupidsexist[$key];
+            $DB->update_record('backup_ids_temp', $extrarecord);
+            // Update existing cache or add new record to cache.
+            if (isset(self::$backupidscache[$key])) {
+                $record = array_merge((array)self::$backupidscache[$key], $extrarecord);
+                self::$backupidscache[$key] = (object) $record;
+            } else if (self::$backupidscachesize > 0) {
+                if ($existingrecord) {
+                    self::$backupidscache[$key] = $existingrecord;
+                } else {
+                    // Retrive record from database and cache updated records.
+                    self::$backupidscache[$key] = $DB->get_record('backup_ids_temp', $record);
+                }
+                $record = array_merge((array)self::$backupidscache[$key], $extrarecord);
+                self::$backupidscache[$key] = (object) $record;
+                self::$backupidscachesize--;
+            }
+        }
+    }
+
     /**
      * Given one role, as loaded from XML, perform the best possible matching against the assignable
      * roles, using different fallback alternatives (shortname, archetype, editingteacher => teacher, defaultcourseroleid)
@@ -1219,16 +1380,7 @@ abstract class restore_dbops {
         $DB->insert_record('backup_files_temp', $filerec);
     }
 
-
     public static function set_backup_ids_record($restoreid, $itemname, $itemid, $newitemid = 0, $parentitemid = null, $info = null) {
-        global $DB;
-
-        // Build the basic (mandatory) record info
-        $record = array(
-            'backupid' => $restoreid,
-            'itemname' => $itemname,
-            'itemid'   => $itemid
-        );
         // Build conditionally the extra record info
         $extrarecord = array();
         if ($newitemid != 0) {
@@ -1241,34 +1393,16 @@ abstract class restore_dbops {
             $extrarecord['info'] = base64_encode(serialize($info));
         }
 
-        // TODO: Analyze if some static (and limited) cache by the 3 params could save us a bunch of get_record() calls
-        // Note: Sure it will! And also will improve getter
-        if (!$dbrec = $DB->get_record('backup_ids_temp', $record)) { // Need to insert the complete record
-            $DB->insert_record('backup_ids_temp', array_merge($record, $extrarecord));
-
-        } else { // Need to update the extra record info if there is something to
-            if (!empty($extrarecord)) {
-                $extrarecord['id'] = $dbrec->id;
-                $DB->update_record('backup_ids_temp', $extrarecord);
-            }
-        }
+        self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord);
     }
 
     public static function get_backup_ids_record($restoreid, $itemname, $itemid) {
-        global $DB;
+        $dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid);
 
-        // Build the basic (mandatory) record info to look for
-        $record = array(
-            'backupid' => $restoreid,
-            'itemname' => $itemname,
-            'itemid'   => $itemid
-        );
-        // TODO: Analyze if some static (and limited) cache by the 3 params could save us a bunch of get_record() calls
-        if ($dbrec = $DB->get_record('backup_ids_temp', $record)) {
-            if ($dbrec->info != null) {
-                $dbrec->info = unserialize(base64_decode($dbrec->info));
-            }
+        if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) {
+            $dbrec->info = unserialize(base64_decode($dbrec->info));
         }
+
         return $dbrec;
     }
 
index d25c794..94a7a87 100644 (file)
@@ -315,6 +315,7 @@ abstract class backup_cron_automated_helper {
      */
     public static function launch_automated_backup($course, $starttime, $userid) {
 
+        $outcome = true;
         $config = get_config('backup');
         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_AUTOMATED, $userid);
 
@@ -347,7 +348,7 @@ abstract class backup_cron_automated_helper {
 
             $bc->set_status(backup::STATUS_AWAITING);
 
-            $outcome = $bc->execute_plan();
+            $bc->execute_plan();
             $results = $bc->get_results();
             $file = $results['backup_destination'];
             $dir = $config->backup_auto_destination;
@@ -363,16 +364,17 @@ abstract class backup_cron_automated_helper {
                 }
             }
 
-            $outcome = true;
-        } catch (backup_exception $e) {
-            $bc->log('backup_auto_failed_on_course', backup::LOG_WARNING, $course->shortname);
+        } catch (moodle_exception $e) {
+            $bc->log('backup_auto_failed_on_course', backup::LOG_ERROR, $course->shortname); // Log error header.
+            $bc->log('Exception: ' . $e->errorcode, backup::LOG_ERROR, $e->a, 1); // Log original exception problem.
+            $bc->log('Debug: ' . $e->debuginfo, backup::LOG_DEBUG, null, 1); // Log original debug information.
             $outcome = false;
         }
 
         $bc->destroy();
         unset($bc);
 
-        return true;
+        return $outcome;
     }
 
     /**
index 3ed176b..eab1869 100644 (file)
@@ -61,7 +61,12 @@ abstract class base_plan implements checksumable, executable {
         // Append task settings to plan array, if not present, for comodity
         foreach ($task->get_settings() as $key => $setting) {
             if (!in_array($setting, $this->settings)) {
-                $this->settings[] = $setting;
+                $name = $setting->get_name();
+                if (!isset($this->settings[$name])) {
+                    $this->settings[$name] = $setting;
+                } else {
+                    throw new base_plan_exception('multiple_settings_by_name_found', $name);
+                }
             }
         }
     }
@@ -84,23 +89,16 @@ abstract class base_plan implements checksumable, executable {
 
     /**
      * return one setting by name, useful to request root/course settings
-     * that are, by definition, unique by name. Throws exception if multiple
-     * are found
+     * that are, by definition, unique by name.
      *
-     * TODO: Change this to string indexed array for quicker lookup. Not critical
+     * @param string $name name of the setting
+     * @throws base_plan_exception if setting name is not found.
      */
     public function get_setting($name) {
         $result = null;
-        foreach ($this->settings as $key => $setting) {
-            if ($setting->get_name() == $name) {
-                if ($result != null) {
-                    throw new base_plan_exception('multiple_settings_by_name_found', $name);
-                } else {
-                    $result = $setting;
-                }
-            }
-        }
-        if (!$result) {
+        if (isset($this->settings[$name])) {
+            $result = $this->settings[$name];
+        } else {
             throw new base_plan_exception('setting_by_name_not_found', $name);
         }
         return $result;
index 1ce9053..7995561 100644 (file)
@@ -124,6 +124,8 @@ class backup_ui_stage_initial extends backup_ui_stage {
             // Store as a variable so we can iterate by reference
             $tasks = $this->ui->get_tasks();
             // Iterate all tasks by reference
+            $add_settings = array();
+            $dependencies = array();
             foreach ($tasks as &$task) {
                 // For the initial stage we are only interested in the root settings
                 if ($task instanceof backup_root_task) {
@@ -134,17 +136,23 @@ class backup_ui_stage_initial extends backup_ui_stage {
                         if ($setting->get_name() == 'filename') {
                             continue;
                         }
-                        $form->add_setting($setting, $task);
+                        $add_settings[] = array($setting, $task);
                     }
                     // Then add all dependencies
                     foreach ($settings as &$setting) {
                         if ($setting->get_name() == 'filename') {
                             continue;
                         }
-                        $form->add_dependencies($setting);
+                        $dependencies[] = $setting;
                     }
                 }
             }
+            // Add all settings at once.
+            $form->add_settings($add_settings);
+            // Add dependencies.
+            foreach ($dependencies as $depsetting) {
+                $form->add_dependencies($depsetting);
+            }
             $this->stageform = $form;
         }
         // Return the form
@@ -226,6 +234,8 @@ class backup_ui_stage_schema extends backup_ui_stage {
             $tasks = $this->ui->get_tasks();
             $content = '';
             $courseheading = false;
+            $add_settings = array();
+            $dependencies = array();
             foreach ($tasks as $task) {
                 if (!($task instanceof backup_root_task)) {
                     if (!$courseheading) {
@@ -235,11 +245,11 @@ class backup_ui_stage_schema extends backup_ui_stage {
                     }
                     // First add each setting
                     foreach ($task->get_settings() as $setting) {
-                        $form->add_setting($setting, $task);
+                        $add_settings[] = array($setting, $task);
                     }
                     // The add all the dependencies
                     foreach ($task->get_settings() as $setting) {
-                        $form->add_dependencies($setting);
+                        $dependencies[] = $setting;
                     }
                 } else if ($this->ui->enforce_changed_dependencies()) {
                     // Only show these settings if dependencies changed them.
@@ -254,6 +264,10 @@ class backup_ui_stage_schema extends backup_ui_stage {
                     }
                 }
             }
+            $form->add_settings($add_settings);
+            foreach ($dependencies as $depsetting) {
+                $form->add_dependencies($depsetting);
+            }
             $this->stageform = $form;
         }
         return $this->stageform;
index ad99260..e0ffc0f 100644 (file)
@@ -136,24 +136,38 @@ abstract class base_moodleform extends moodleform {
      * @return bool
      */
     function add_setting(backup_setting $setting, base_task $task=null) {
+        return $this->add_settings(array(array($setting, $task)));
+    }
+    /**
+     * Adds multiple backup_settings as elements to the form
+     * @param array $settingstasks Consists of array($setting, $task) elements
+     * @return bool
+     */
+    public function add_settings(array $settingstasks) {
         global $OUTPUT;
 
-        // If the setting cant be changed or isn't visible then add it as a fixed setting.
-        if (!$setting->get_ui()->is_changeable() || $setting->get_visibility() != backup_setting::VISIBLE) {
-            return $this->add_fixed_setting($setting, $task);
-        }
+        $defaults = array();
+        foreach ($settingstasks as $st) {
+            list($setting, $task) = $st;
+            // If the setting cant be changed or isn't visible then add it as a fixed setting.
+            if (!$setting->get_ui()->is_changeable() || $setting->get_visibility() != backup_setting::VISIBLE) {
+                $this->add_fixed_setting($setting, $task);
+                continue;
+            }
 
-        // First add the formatting for this setting
-        $this->add_html_formatting($setting);
+            // First add the formatting for this setting
+            $this->add_html_formatting($setting);
 
-        // The call the add method with the get_element_properties array
-        call_user_func_array(array($this->_form, 'addElement'), $setting->get_ui()->get_element_properties($task, $OUTPUT));
-        $this->_form->setDefault($setting->get_ui_name(), $setting->get_value());
-        if ($setting->has_help()) {
-            list($identifier, $component) = $setting->get_help();
-            $this->_form->addHelpButton($setting->get_ui_name(), $identifier, $component);
+            // Then call the add method with the get_element_properties array
+            call_user_func_array(array($this->_form, 'addElement'), $setting->get_ui()->get_element_properties($task, $OUTPUT));
+            $defaults[$setting->get_ui_name()] = $setting->get_value();
+            if ($setting->has_help()) {
+                list($identifier, $component) = $setting->get_help();
+                $this->_form->addHelpButton($setting->get_ui_name(), $identifier, $component);
+            }
+            $this->_form->addElement('html', html_writer::end_tag('div'));
         }
-        $this->_form->addElement('html', html_writer::end_tag('div'));
+        $this->_form->setDefaults($defaults);
         return true;
     }
     /**
@@ -317,4 +331,4 @@ abstract class base_moodleform extends moodleform {
             $this->definition_after_data();
         }
     }
-}
\ No newline at end of file
+}
index 184449a..8152990 100644 (file)
@@ -242,7 +242,6 @@ function calendar_get_mini($courses, $groups, $users, $cal_month = false, $cal_y
     $days_title = calendar_get_days();
 
     $summary = get_string('calendarheading', 'calendar', userdate(make_timestamp($y, $m), get_string('strftimemonthyear')));
-    $summary = get_string('tabledata', 'access', $summary);
     $content .= '<table class="minicalendar calendartable" summary="'.$summary.'">'; // Begin table
     $content .= '<tr class="weekdays">'; // Header row: day names
 
index f2af45a..3354f1c 100644 (file)
@@ -53,5 +53,7 @@ if ($course->numsections >= 0) {
     $DB->update_record('course', $course);
 }
 
+$url = course_get_url($course);
+$url->set_anchor('changenumsections');
 // Redirect to where we were..
-redirect(course_get_url($course));
+redirect($url);
index fb59c38..f0dd91f 100644 (file)
@@ -56,6 +56,21 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      */
     abstract protected function page_title();
 
+    /**
+     * Generate the section title
+     *
+     * @param stdClass $section The course_section entry from DB
+     * @param stdClass $course The course entry from DB
+     * @return string HTML to output.
+     */
+    public function section_title($section, $course) {
+        $title = get_section_name($course, $section);
+        if ($section->section != 0 && $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE) {
+            $title = html_writer::link(course_get_url($course, $section->section), $title);
+        }
+        return $title;
+    }
+
     /**
      * Generate the content to displayed on the right part of a section
      * before course modules are included
@@ -106,7 +121,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      *
      * @param stdClass $section The course_section entry from DB
      * @param stdClass $course The course entry from DB
-     * @param bool $onsectionpage true if being printed on a section page
+     * @param bool $onsectionpage true if being printed on a single-section page
      * @return string HTML to output.
      */
     protected function section_header($section, $course, $onsectionpage) {
@@ -115,7 +130,6 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o = '';
         $currenttext = '';
         $sectionstyle = '';
-        $linktitle = false;
 
         if ($section->section != 0) {
             // Only in the non-general sections.
@@ -124,7 +138,6 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             } else if ($this->is_section_current($section, $course)) {
                 $sectionstyle = ' current';
             }
-            $linktitle = ($course->coursedisplay == COURSE_DISPLAY_MULTIPAGE);
         }
 
         $o.= html_writer::start_tag('li', array('id' => 'section-'.$section->section,
@@ -138,11 +151,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o.= html_writer::start_tag('div', array('class' => 'content'));
 
         if (!$onsectionpage) {
-            $title = get_section_name($course, $section);
-            if ($linktitle) {
-                $title = html_writer::link(course_get_url($course, $section->section), $title);
-            }
-            $o.= $this->output->heading($title, 3, 'sectionname');
+            $o.= $this->output->heading($this->section_title($section, $course), 3, 'sectionname');
         }
 
         $o.= html_writer::start_tag('div', array('class' => 'summary'));
@@ -506,7 +515,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $completioninfo = new completion_info($course);
         echo $completioninfo->display_help_icon();
 
-        print_section($course, $thissection, $mods, $modnamesused, true);
+        print_section($course, $thissection, $mods, $modnamesused, true, '100%', false, true);
         if ($PAGE->user_is_editing()) {
             print_section_add_menus($course, $displaysection, $modnames);
         }
@@ -624,7 +633,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
 
             echo $this->end_section_list();
 
-            echo html_writer::start_tag('div', array('class' => 'mdl-right'));
+            echo html_writer::start_tag('div', array('id' => 'changenumsections', 'class' => 'mdl-right'));
 
             // Increase number of sections.
             $straddsection = get_string('increasesections', 'moodle');
index 218492d..71e8797 100644 (file)
@@ -1,4 +1,4 @@
-// Javascript functions for course format
+// Javascript functions for Topics course format
 
 M.course = M.course || {};
 
@@ -36,13 +36,32 @@ M.course.format.get_config = function() {
 M.course.format.swap_sections = function(Y, node1, node2) {
     var CSS = {
         COURSECONTENT : 'course-content',
-        LEFT : 'left',
         SECTIONADDMENUS : 'section_add_menus'
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap left block
-    sectionlist.item(node1).one('.'+CSS.LEFT).swap(sectionlist.item(node2).one('.'+CSS.LEFT));
     // Swap menus
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
 }
+
+/**
+ * Process sections after ajax response
+ *
+ * @param {YUI} Y YUI3 instance
+ * @param {array} response ajax response
+ * @param {string} sectionfrom first affected section
+ * @param {string} sectionto last affected section
+ * @return void
+ */
+M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
+    var CSS = {
+        SECTIONNAME : 'sectionname'
+    };
+
+    if (response.action == 'move') {
+        // update titles in all affected sections
+        for (var i = sectionfrom; i <= sectionto; i++) {
+            sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+        }
+    }
+}
index 710abaf..e8632fc 100644 (file)
@@ -80,3 +80,24 @@ function callback_topics_ajax_support() {
     $ajaxsupport->testedbrowsers = array('MSIE' => 6.0, 'Gecko' => 20061111, 'Safari' => 531, 'Chrome' => 6.0);
     return $ajaxsupport;
 }
+
+/**
+ * Callback function to do some action after section move
+ *
+ * @param stdClass $course The course entry from DB
+ * @return array This will be passed in ajax respose.
+ */
+function callback_topics_ajax_section_move($course) {
+    global $COURSE, $PAGE;
+
+    $titles = array();
+    rebuild_course_cache($course->id);
+    $modinfo = get_fast_modinfo($COURSE);
+    $renderer = $PAGE->get_renderer('format_topics');
+    if ($renderer && ($sections = $modinfo->get_section_info_all())) {
+        foreach ($sections as $number => $section) {
+            $titles[$number] = $renderer->section_title($section, $course);
+        }
+    }
+    return array('sectiontitles' => $titles, 'action' => 'move');
+}
index 159f94b..f410e07 100644 (file)
@@ -1,4 +1,4 @@
-// Javascript functions for course format
+// Javascript functions for Weeks course format
 
 M.course = M.course || {};
 
@@ -36,16 +36,32 @@ M.course.format.get_config = function() {
 M.course.format.swap_sections = function(Y, node1, node2) {
     var CSS = {
         COURSECONTENT : 'course-content',
-        LEFT : 'left',
         SECTIONADDMENUS : 'section_add_menus',
-        WEEKDATES: 'sectionname'
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap left block
-    sectionlist.item(node1).one('.'+CSS.LEFT).swap(sectionlist.item(node2).one('.'+CSS.LEFT));
     // Swap menus
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
-    // Swap week dates
-    sectionlist.item(node1).one('.'+CSS.WEEKDATES).swap(sectionlist.item(node2).one('.'+CSS.WEEKDATES));
+}
+
+/**
+ * Process sections after ajax response
+ *
+ * @param {YUI} Y YUI3 instance
+ * @param {array} response ajax response
+ * @param {string} sectionfrom first affected section
+ * @param {string} sectionto last affected section
+ * @return void
+ */
+M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
+    var CSS = {
+        SECTIONNAME : 'sectionname'
+    };
+
+    if (response.action == 'move') {
+        // update titles in all affected sections
+        for (var i = sectionfrom; i <= sectionto; i++) {
+            sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+        }
+    }
 }
index 834adda..a1e1ea9 100644 (file)
@@ -117,3 +117,24 @@ function format_weeks_get_section_dates($section, $course) {
 
     return $dates;
 }
+
+/**
+ * Callback function to do some action after section move
+ *
+ * @param stdClass $course The course entry from DB
+ * @return array This will be passed in ajax respose.
+ */
+function callback_weeks_ajax_section_move($course) {
+    global $COURSE, $PAGE;
+
+    $titles = array();
+    rebuild_course_cache($course->id);
+    $modinfo = get_fast_modinfo($COURSE);
+    $renderer = $PAGE->get_renderer('format_weeks');
+    if ($renderer && ($sections = $modinfo->get_section_info_all())) {
+        foreach ($sections as $number => $section) {
+            $titles[$number] = $renderer->section_title($section, $course);
+        }
+    }
+    return array('sectiontitles' => $titles, 'action' => 'move');
+}
index 8e07373..a5566d6 100644 (file)
@@ -1728,7 +1728,7 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false,
             // see the activity itself, or for staff)
             if (!$mod->uservisible) {
                 echo '<div class="availabilityinfo">'.$mod->availableinfo.'</div>';
-            } else if ($canviewhidden && !empty($CFG->enableavailability)) {
+            } else if ($canviewhidden && !empty($CFG->enableavailability) && $mod->visible) {
                 $ci = new condition_info($mod);
                 $fullinfo = $ci->get_full_information();
                 if($fullinfo) {
@@ -2995,6 +2995,8 @@ function move_section($course, $section, $move) {
         }
         $n++;
     }
+    // After moving section, rebuild course cache.
+    rebuild_course_cache($course->id, true);
     return true;
 }
 
index f33ef54..8b31e29 100644 (file)
@@ -221,7 +221,9 @@ if (!empty($activities)) {
                 echo $OUTPUT->spacer(array('height'=>30, 'br'=>true)); // should be done with CSS instead
             }
             echo $OUTPUT->box_start();
-            echo "<h2>$activity->name</h2>";
+            if (!empty($activity->name)) {
+                echo html_writer::tag('h2', $activity->name);
+            }
             $inbox = true;
 
         } else if ($activity->type == 'activity') {
@@ -230,16 +232,17 @@ if (!empty($activities)) {
                 $cm = $modinfo->cms[$activity->cmid];
 
                 if ($cm->visible) {
-                    $linkformat = '';
+                    $class = '';
                 } else {
-                    $linkformat = 'class="dimmed"';
+                    $class = 'dimmed';
                 }
                 $name        = format_string($cm->name);
                 $modfullname = $modnames[$cm->modname];
 
-                $image = "<img src=\"" . $OUTPUT->pix_url('icon', $cm->modname) . "\" class=\"icon\" alt=\"$modfullname\" />";
-                echo "<h3>$image $modfullname".
-                     " <a href=\"$CFG->wwwroot/mod/$cm->modname/view.php?id=$cm->id\" $linkformat>$name</a></h3>";
+                $image = $OUTPUT->pix_icon('icon', $modfullname, $cm->modname, array('class' => 'icon smallicon'));
+                $link = html_writer::link(new moodle_url("/mod/$cm->modname/view.php",
+                            array("id" => $cm->id)), $name, array('class' => $class));
+                echo html_writer::tag('h3', "$image $modfullname $link");
            }
 
         } else {
@@ -269,7 +272,7 @@ if (!empty($activities)) {
 
 } else {
 
-    echo '<h3><center>' . get_string('norecentactivity') . '</center></h3>';
+    echo html_writer::tag('h3', get_string('norecentactivity'), array('class' => 'mdl-align'));
 
 }
 
index 5152563..6efd228 100644 (file)
@@ -89,6 +89,15 @@ switch($requestmethod) {
 
                     case 'move':
                         move_section_to($course, $id, $value);
+                        // See if format wants to do something about it
+                        $libfile = $CFG->dirroot.'/course/format/'.$course->format.'/lib.php';
+                        $functionname = 'callback_'.$course->format.'_ajax_section_move';
+                        if (!function_exists($functionname) && file_exists($libfile)) {
+                            require_once $libfile;
+                        }
+                        if (function_exists($functionname)) {
+                            echo json_encode($functionname($course));
+                        }
                         break;
                 }
                 rebuild_course_cache($course->id);
index a7b1784..de37981 100644 (file)
 
     require_once($CFG->dirroot.'/calendar/lib.php');    /// This is after login because it needs $USER
 
-    //TODO: danp do we need different urls?
-    add_to_log($course->id, 'course', 'view', "view.php?id=$course->id", "$course->id");
+    $logparam = 'id='. $course->id;
+    $loglabel = 'view';
+    $infoid = $course->id;
+    if(!empty($section)) {
+        $logparam .= '&section='. $section;
+        $loglabel = 'view section';
+        $sectionparams = array('course' => $course->id, 'section' => $section);
+        if ($coursesections = $DB->get_record('course_sections', $sectionparams, 'id', MUST_EXIST)) {
+            $infoid = $coursesections->id;
+    }
+    }
+    add_to_log($course->id, 'course', $loglabel, "view.php?". $logparam, $infoid);
 
     $course->format = clean_param($course->format, PARAM_ALPHA);
     if (!file_exists($CFG->dirroot.'/course/format/'.$course->format.'/format.php')) {
index 3dbbf0b..b02c362 100644 (file)
@@ -68,6 +68,21 @@ YUI.add('moodle-course-coursebase', function(Y) {
         return null;
     }
 
+   /**
+    * Process sections after ajax response (should be defined in format.js)
+    * If some response is expected, we pass it over to format, as it knows better
+    * hot to process it.
+    *
+    * @param {YUI} Y YUI3 instance
+    * @param {NodeList} list of sections
+    * @param {array} response ajax response
+    * @param {string} sectionfrom first affected section
+    * @param {string} sectionto last affected section
+    * @return void
+    */
+    M.course.format.process_sections = M.course.format.process_sections || function(Y, sectionlist, response, sectionfrom, sectionto) {
+        return null;
+    }
 
    /**
     * Get sections config for this format, for examples see function definition
index 012355e..080cace 100644 (file)
@@ -119,6 +119,13 @@ YUI.add('moodle-course-dragdrop', function(Y) {
             drag.get('dragNode').addClass(CSS.COURSECONTENT);
         },
 
+        drag_dropmiss : function(e) {
+            // Missed the target, but we assume the user intended to drop it
+            // on the last last ghost node location, e.drag and e.drop should be
+            // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
+            this.drop_hit(e);
+        },
+
         drop_hit : function(e) {
             var drag = e.drag;
             // Get a reference to our drag node
@@ -172,9 +179,16 @@ YUI.add('moodle-course-dragdrop', function(Y) {
                         lightbox.show();
                     },
                     success: function(tid, response) {
-                        window.setTimeout(function(e) {
-                            lightbox.hide();
-                        }, 250);
+                        // Update section titles, we can't simply swap them as
+                        // they might have custom title
+                        try {
+                            var responsetext = Y.JSON.parse(response.responseText);
+                            if (responsetext.error) {
+                                new M.core.ajaxException(responsetext);
+                            }
+                            M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
+                        } catch (e) {}
+
                         // Classic bubble sort algorithm is applied to the section
                         // nodes between original drag node location and the new one.
                         do {
@@ -193,6 +207,11 @@ YUI.add('moodle-course-dragdrop', function(Y) {
                             }
                             loopend = loopend - 1;
                         } while (swapped);
+
+                        // Finally, hide the lightbox
+                        window.setTimeout(function(e) {
+                            lightbox.hide();
+                        }, 250);
                     },
                     failure: function(tid, response) {
                         this.ajax_failure(response);
@@ -309,6 +328,13 @@ YUI.add('moodle-course-dragdrop', function(Y) {
             drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
         },
 
+        drag_dropmiss : function(e) {
+            // Missed the target, but we assume the user intended to drop it
+            // on the last last ghost node location, e.drag and e.drop should be
+            // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
+            this.drop_hit(e);
+        },
+
         drop_hit : function(e) {
             var drag = e.drag;
             // Get a reference to our drag node
index ac8e582..3728a23 100644 (file)
@@ -185,7 +185,6 @@ class core_files_renderer extends plugin_renderer_base {
         $strmakedir  = get_string('makeafolder', 'moodle');
         $strdownload = get_string('downloadfolder', 'repository');
         $strloading  = get_string('loading', 'repository');
-        $strnofilesattached = get_string('nofilesattached', 'repository');
         $strdroptoupload = get_string('droptoupload', 'moodle');
         $icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).'';
         $restrictions = $this->fm_print_restrictions($fm);
@@ -220,7 +219,7 @@ class core_files_renderer extends plugin_renderer_base {
     <div class="filemanager-container" >
         <div class="fm-content-wrapper">
             <div class="fp-content"></div>
-            <div class="fm-empty-container <!--mdl-align-->">'.$strnofilesattached.'
+            <div class="fm-empty-container <!--mdl-align-->">
                 <span class="dndupload-message">'.$strdndenabledinbox.'<br/><span class="dndupload-arrow"></span></span>
             </div>
             <div class="dndupload-target">'.$strdroptoupload.'<br/><span class="dndupload-arrow"></span></div>
index e1937be..9d1bda9 100644 (file)
@@ -32,16 +32,56 @@ require_once $CFG->libdir.'/gradelib.php';
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class graded_users_iterator {
-    public $course;
-    public $grade_items;
-    public $groupid;
-    public $users_rs;
-    public $grades_rs;
-    public $gradestack;
-    public $sortfield1;
-    public $sortorder1;
-    public $sortfield2;
-    public $sortorder2;
+
+    /**
+     * The couse whose users we are interested in
+     */
+    protected $course;
+
+    /**
+     * An array of grade items or null if only user data was requested
+     */
+    protected $grade_items;
+
+    /**
+     * The group ID we are interested in. 0 means all groups.
+     */
+    protected $groupid;
+
+    /**
+     * A recordset of graded users
+     */
+    protected $users_rs;
+
+    /**
+     * A recordset of user grades (grade_grade instances)
+     */
+    protected $grades_rs;
+
+    /**
+     * Array used when moving to next user while iterating through the grades recordset
+     */
+    protected $gradestack;
+
+    /**
+     * The first field of the users table by which the array of users will be sorted
+     */
+    protected $sortfield1;
+
+    /**
+     * Should sortfield1 be ASC or DESC
+     */
+    protected $sortorder1;
+
+    /**
+     * The second field of the users table by which the array of users will be sorted
+     */
+    protected $sortfield2;
+
+    /**
+     * Should sortfield2 be ASC or DESC
+     */
+    protected $sortorder2;
 
     /**
      * Should users whose enrolment has been suspended be ignored?
@@ -59,7 +99,7 @@ class graded_users_iterator {
      * @param string $sortfield2 The second field of the users table by which the array of users will be sorted
      * @param string $sortorder2 The order in which the second sorting field will be sorted (ASC or DESC)
      */
-    public function graded_users_iterator($course, $grade_items=null, $groupid=0,
+    public function __construct($course, $grade_items=null, $groupid=0,
                                           $sortfield1='lastname', $sortorder1='ASC',
                                           $sortfield2='firstname', $sortorder2='ASC') {
         $this->course      = $course;
@@ -75,6 +115,7 @@ class graded_users_iterator {
 
     /**
      * Initialise the iterator
+     *
      * @return boolean success
      */
     public function init() {
@@ -177,7 +218,7 @@ class graded_users_iterator {
      * Returns information about the next user
      * @return mixed array of user info, all grades and feedback or null when no more users found
      */
-    function next_user() {
+    public function next_user() {
         if (!$this->users_rs) {
             return false; // no users present
         }
@@ -244,10 +285,9 @@ class graded_users_iterator {
     }
 
     /**
-     * Close the iterator, do not forget to call this function.
-     * @return void
+     * Close the iterator, do not forget to call this function
      */
-    function close() {
+    public function close() {
         if ($this->users_rs) {
             $this->users_rs->close();
             $this->users_rs = null;
@@ -273,23 +313,23 @@ class graded_users_iterator {
 
 
     /**
-     * _push
+     * Add a grade_grade instance to the grade stack
      *
      * @param grade_grade $grade Grade object
      *
      * @return void
      */
-    function _push($grade) {
+    private function _push($grade) {
         array_push($this->gradestack, $grade);
     }
 
 
     /**
-     * _pop
+     * Remove a grade_grade instance from the grade stack
      *
-     * @return object current grade object
+     * @return grade_grade current grade object
      */
-    function _pop() {
+    private function _pop() {
         global $DB;
         if (empty($this->gradestack)) {
             if (empty($this->grades_rs) || !$this->grades_rs->valid()) {
index b338d42..ee81afc 100644 (file)
@@ -39,8 +39,6 @@ $string['skipa'] = 'Skip {$a}';
 $string['skipblock'] = 'Skip block';
 $string['skipnavigation'] = 'Skip navigation';
 $string['skipto'] = 'Skip to {$a}';
-$string['tabledata'] = 'Data table, {$a}';
-$string['tablelayout'] = 'Layout table, {$a}';
 $string['tocontent'] = 'Skip to main content';
 $string['tonavigation'] = 'Go to navigation';
 $string['youarehere'] = 'You are here';
index 2ac7b1e..6dbb859 100644 (file)
@@ -636,6 +636,7 @@ $string['maturity50'] = 'Alpha';
 $string['maturity100'] = 'Beta';
 $string['maturity150'] = 'Release candidate';
 $string['maturity200'] = 'Stable version';
+$string['maturityallowunstable'] = 'Hint: You may want to run this script with --allow-unstable option';
 $string['maturitycoreinfo'] = 'Your site is currently running unstable "{$a}" development code.';
 $string['maturitycorewarning'] = 'The version of Moodle that you are about to install or upgrade to contains
 unstable "{$a}" development code that is not suitable for use on most production
@@ -753,6 +754,8 @@ $string['pleaserefreshregistration'] = 'Your site has been registered with moodl
 $string['pleaseregister'] = 'Please register your site to remove this button';
 $string['plugin'] = 'Plugin';
 $string['plugins'] = 'Plugins';
+$string['pluginscheck'] = 'Plugin dependencies check';
+$string['pluginscheckfailed'] = 'Dependencies check failed for {$a->pluginslist}';
 $string['pluginschecktodo'] = 'You must solve all the plugin requirements before proceeding to install this Moodle version!';
 $string['pluginsoverview'] = 'Plugins overview';
 $string['profilecategory'] = 'Category';
index 073d3eb..224b741 100644 (file)
@@ -466,7 +466,7 @@ $string['displayonpage'] = 'Display on page';
 $string['dndenabled'] = 'Drag and drop available';
 $string['dndenabled_help'] = 'You can drag one or more files from your desktop and drop them onto the box below to upload them.<br />Note: this may not work with other web browsers';
 $string['dndenabled_insentence'] = 'drag and drop available';
-$string['dndenabled_inbox'] = 'drag and drop files here to upload them';
+$string['dndenabled_inbox'] = 'You can drag and drop files here to add them.';
 $string['dnduploadwithoutcontent'] = 'This upload does not have any content';
 $string['dndworkingfiletextlink'] = 'Drag and drop files, text or links onto course sections to upload them';
 $string['dndworkingfilelink'] = 'Drag and drop files or links onto course sections to upload them';
index 3bc89ef..3bba405 100644 (file)
@@ -31,7 +31,7 @@ $string['checkforupdates'] = 'Check for available updates';
 $string['checkforupdateslast'] = 'Last check done on {$a}';
 $string['displayname'] = 'Plugin name';
 $string['moodleversion'] = 'Moodle {$a}';
-$string['nonehighlighted'] = 'No plugins require your attention during this upgrade';
+$string['nonehighlighted'] = 'No plugins require your attention now';
 $string['nonehighlightedinfo'] = 'Display the list of all installed plugins anyway';
 $string['noneinstalled'] = 'No plugins of this type are installed';
 $string['notes'] = 'Notes';
@@ -52,7 +52,7 @@ $string['requiredby'] = 'Required by: {$a}';
 $string['requires'] = 'Requires';
 $string['rootdir'] = 'Directory';
 $string['settings'] = 'Settings';
-$string['somehighlighted'] = 'Number of plugins requiring attention during this upgrade: {$a}';
+$string['somehighlighted'] = 'Number of plugins requiring your attention: {$a}';
 $string['somehighlightedinfo'] = 'Display the full list of installed plugins';
 $string['somehighlightedonly'] = 'Display only plugins requiring your attention';
 $string['source'] = 'Source';
index ba27546..375c966 100644 (file)
@@ -235,7 +235,7 @@ class completion_criteria_activity extends completion_criteria {
         // Loop through completions, and mark as complete
         $rs = $DB->get_recordset_sql($sql);
         foreach ($rs as $record) {
-            $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
+            $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
             $completion->mark_complete($record->timecompleted);
         }
         $rs->close();
index 04be448..a25dea2 100644 (file)
@@ -193,8 +193,8 @@ class completion_criteria_course extends completion_criteria {
         // Loop through completions, and mark as complete
         $rs = $DB->get_recordset_sql($sql);
         foreach ($rs as $record) {
-            $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
-            $completion->mark_complete($record['timecompleted']);
+            $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
+            $completion->mark_complete($record->timecompleted);
         }
         $rs->close();
     }
index bacc144..baecc20 100644 (file)
@@ -179,8 +179,8 @@ class completion_criteria_date extends completion_criteria {
         // Loop through completions, and mark as complete
         $rs = $DB->get_recordset_sql($sql, array(time()));
         foreach ($rs as $record) {
-            $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
-            $completion->mark_complete($record['timeend']);
+            $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
+            $completion->mark_complete($record->timeend);
         }
         $rs->close();
     }
index 962b052..ead1a55 100644 (file)
@@ -229,7 +229,7 @@ class completion_criteria_duration extends completion_criteria {
         $now = time();
         $rs = $DB->get_recordset_sql($sql, array($now, $now));
         foreach ($rs as $record) {
-            $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
+            $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
 
             // Use time start if not 0, otherwise use timeenrolled
             if ($record->otimestart) {
index 3e0da11..6bc32ed 100644 (file)
@@ -215,8 +215,8 @@ class completion_criteria_grade extends completion_criteria {
         // Loop through completions, and mark as complete
         $rs = $DB->get_recordset_sql($sql);
         foreach ($rs as $record) {
-            $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
-            $completion->mark_complete($record['timecompleted']);
+            $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
+            $completion->mark_complete($record->timecompleted);
         }
         $rs->close();
     }
index 9bcd4bb..0d8b3cb 100644 (file)
@@ -69,7 +69,8 @@ class condition_info extends condition_info_base {
      * @param object $cm Moodle course-module object. May have extra fields
      *   ->conditionsgrade, ->conditionscompletion which should come from
      *   get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
-     *   and ->showavailability, ->course; but the only required thing is ->id.
+     *   and ->showavailability, ->course, ->visible; but the only required
+     *   thing is ->id.
      * @param int $expectingmissing Used to control whether or not a developer
      *   debugging message (performance warning) will be displayed if some of
      *   the above data is missing and needs to be retrieved; a
@@ -426,7 +427,8 @@ abstract class condition_info_base {
      * @return array Array of field names
      */
     protected function get_main_table_fields() {
-        return array('id', 'course', 'availablefrom', 'availableuntil', 'showavailability');
+        return array('id', 'course', 'visible',
+                'availablefrom', 'availableuntil', 'showavailability');
     }
 
     /**
@@ -846,6 +848,16 @@ abstract class condition_info_base {
             }
         }
 
+        // If the item is marked as 'not visible' then we don't change the available
+        // flag (visible/available are treated distinctly), but we remove any
+        // availability info. If the item is hidden with the eye icon, it doesn't
+        // make sense to show 'Available from <date>' or similar, because even
+        // when that date arrives it will still not be available unless somebody
+        // toggles the eye icon.
+        if (!$this->item->visible) {
+            $information = '';
+        }
+
         $information = trim($information);
         return $available;
     }
index 0463430..d4b7077 100644 (file)
       </KEYS>
       <INDEXES>
         <INDEX NAME="course" UNIQUE="false" FIELDS="course" NEXT="criteriatype"/>
-        <INDEX NAME="criteriatype" UNIQUE="false" FIELDS="criteriatype" PREVIOUS="course"/>
+        <INDEX NAME="criteriatype" UNIQUE="false" FIELDS="criteriatype" PREVIOUS="course" NEXT="coursecriteriatype"/>
+        <INDEX NAME="coursecriteriatype" UNIQUE="true" FIELDS="course, criteriatype" PREVIOUS="criteriatype"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="course_completion_criteria" COMMENT="Course completion criteria" PREVIOUS="course_completion_aggr_methd" NEXT="course_completion_crit_compl">
         <INDEX NAME="userid" UNIQUE="false" FIELDS="userid" NEXT="course"/>
         <INDEX NAME="course" UNIQUE="false" FIELDS="course" PREVIOUS="userid" NEXT="criteriaid"/>
         <INDEX NAME="criteriaid" UNIQUE="false" FIELDS="criteriaid" PREVIOUS="course" NEXT="timecompleted"/>
-        <INDEX NAME="timecompleted" UNIQUE="false" FIELDS="timecompleted" PREVIOUS="criteriaid"/>
+        <INDEX NAME="timecompleted" UNIQUE="false" FIELDS="timecompleted" PREVIOUS="criteriaid" NEXT="useridcoursecriteriaid"/>
+        <INDEX NAME="useridcoursecriteriaid" UNIQUE="true" FIELDS="userid, course, criteriaid" PREVIOUS="timecompleted"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="course_completion_notify" COMMENT="Course completion notification emails" PREVIOUS="course_completion_crit_compl" NEXT="course_completions">
       <INDEXES>
         <INDEX NAME="userid" UNIQUE="false" FIELDS="userid" NEXT="course"/>
         <INDEX NAME="course" UNIQUE="false" FIELDS="course" PREVIOUS="userid" NEXT="timecompleted"/>
-        <INDEX NAME="timecompleted" UNIQUE="false" FIELDS="timecompleted" PREVIOUS="course"/>
+        <INDEX NAME="timecompleted" UNIQUE="false" FIELDS="timecompleted" PREVIOUS="course" NEXT="useridcourse"/>
+        <INDEX NAME="useridcourse" UNIQUE="true" FIELDS="userid, course" PREVIOUS="timecompleted"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="enrol" COMMENT="Instances of enrolment plugins used in courses, fields marked as custom have a plugin defined meaning, core does not touch them. Create a new linked table if you need even more custom fields." PREVIOUS="course_completions" NEXT="user_enrolments">
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index f404e01..f0475f6 100644 (file)
@@ -38,6 +38,7 @@ global $DB; // TODO: this is a hack, we should really do something with the SQL
 $logs = array(
     array('module'=>'course', 'action'=>'user report', 'mtable'=>'user', 'field'=>$DB->sql_concat('firstname', "' '" , 'lastname')),
     array('module'=>'course', 'action'=>'view', 'mtable'=>'course', 'field'=>'fullname'),
+    array('module'=>'course', 'action'=>'view section', 'mtable'=>'course_sections', 'field'=>'COALESCE(name, section)'),
     array('module'=>'course', 'action'=>'update', 'mtable'=>'course', 'field'=>'fullname'),
     array('module'=>'course', 'action'=>'enrol', 'mtable'=>'course', 'field'=>'fullname'), // there should be some way to store user id of the enrolled user!
     array('module'=>'course', 'action'=>'unenrol', 'mtable'=>'course', 'field'=>'fullname'), // there should be some way to store user id of the enrolled user!
index c74c724..ba53f99 100644 (file)
@@ -648,5 +648,126 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012052100.00);
     }
 
+    if ($oldversion < 2012052500.03) { // fix invalid course_completion_records MDL-27368
+        //first get all instances of duplicate records
+        $sql = 'SELECT userid, course FROM {course_completions} WHERE (deleted IS NULL OR deleted <> 1) GROUP BY userid, course HAVING (count(id) > 1)';
+        $duplicates = $DB->get_recordset_sql($sql, array());
+
+        foreach ($duplicates as $duplicate) {
+            $pointer = 0;
+            //now get all the records for this user/course
+            $sql = 'userid = ? AND course = ? AND (deleted IS NULL OR deleted <> 1)';
+            $completions = $DB->get_records_select('course_completions', $sql,
+                array($duplicate->userid, $duplicate->course), 'timecompleted DESC, timestarted DESC');
+            $needsupdate = false;
+            $origcompletion = null;
+            foreach ($completions as $completion) {
+                $pointer++;
+                if ($pointer === 1) { //keep 1st record but delete all others.
+                    $origcompletion = $completion;
+                } else {
+                    //we need to keep the "oldest" of all these fields as the valid completion record.
+                    $fieldstocheck = array('timecompleted', 'timestarted', 'timeenrolled');
+                    foreach ($fieldstocheck as $f) {
+                        if ($origcompletion->$f > $completion->$f) {
+                            $origcompletion->$f = $completion->$f;
+                            $needsupdate = true;
+                        }
+                    }
+                    $DB->delete_records('course_completions', array('id'=>$completion->id));
+                }
+            }
+            if ($needsupdate) {
+                $DB->update_record('course_completions', $origcompletion);
+            }
+        }
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012052500.03);
+    }
+
+    if ($oldversion < 2012052900.00) {
+        // Clean up all duplicate records in the course_completions table in preparation
+        // for adding a new index there.
+        upgrade_course_completion_remove_duplicates(
+            'course_completions',
+            array('userid', 'course'),
+            array('timecompleted', 'timestarted', 'timeenrolled')
+        );
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012052900.00);
+    }
+
+    if ($oldversion < 2012052900.01) {
+        // Add indexes to prevent new duplicates in the course_completions table.
+        // Define index useridcourse (unique) to be added to course_completions
+        $table = new xmldb_table('course_completions');
+        $index = new xmldb_index('useridcourse', XMLDB_INDEX_UNIQUE, array('userid', 'course'));
+
+        // Conditionally launch add index useridcourse
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012052900.01);
+    }
+
+    if ($oldversion < 2012052900.02) {
+        // Clean up all duplicate records in the course_completion_crit_compl table in preparation
+        // for adding a new index there.
+        upgrade_course_completion_remove_duplicates(
+            'course_completion_crit_compl',
+            array('userid', 'course', 'criteriaid'),
+            array('timecompleted')
+        );
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012052900.02);
+    }
+
+    if ($oldversion < 2012052900.03) {
+        // Add indexes to prevent new duplicates in the course_completion_crit_compl table.
+        // Define index useridcoursecriteraid (unique) to be added to course_completion_crit_compl
+        $table = new xmldb_table('course_completion_crit_compl');
+        $index = new xmldb_index('useridcoursecriteraid', XMLDB_INDEX_UNIQUE, array('userid', 'course', 'criteriaid'));
+
+        // Conditionally launch add index useridcoursecriteraid
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012052900.03);
+    }
+
+    if ($oldversion < 2012052900.04) {
+        // Clean up all duplicate records in the course_completion_aggr_methd table in preparation
+        // for adding a new index there.
+        upgrade_course_completion_remove_duplicates(
+            'course_completion_aggr_methd',
+            array('course', 'criteriatype')
+        );
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012052900.04);
+    }
+
+    if ($oldversion < 2012052900.05) {
+        // Add indexes to prevent new duplicates in the course_completion_aggr_methd table.
+        // Define index coursecriteratype (unique) to be added to course_completion_aggr_methd
+        $table = new xmldb_table('course_completion_aggr_methd');
+        $index = new xmldb_index('coursecriteriatype', XMLDB_INDEX_UNIQUE, array('course', 'criteriatype'));
+
+        // Conditionally launch add index coursecriteratype
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012052900.05);
+    }
+
     return true;
-}
+}
\ No newline at end of file
index 028538c..5ae728e 100644 (file)
@@ -826,7 +826,7 @@ class mssql_native_moodle_database extends moodle_database {
         } else {
             unset($params['id']);
             if ($returnid) {
-                $returning = "; SELECT SCOPE_IDENTITY()";
+                $returning = "OUTPUT inserted.id";
             }
         }
 
@@ -838,18 +838,29 @@ class mssql_native_moodle_database extends moodle_database {
         $qms    = array_fill(0, count($params), '?');
         $qms    = implode(',', $qms);
 
-        $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms) $returning";
+        $sql = "INSERT INTO {" . $table . "} ($fields) $returning VALUES ($qms)";
 
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
         $rawsql = $this->emulate_bound_params($sql, $params);
 
         $this->query_start($sql, $params, SQL_QUERY_INSERT);
         $result = mssql_query($rawsql, $this->mssql);
-        $this->query_end($result);
+        // Expected results are:
+        //     - true: insert ok and there isn't returned information.
+        //     - false: insert failed and there isn't returned information.
+        //     - resource: insert executed, need to look for returned (output)
+        //           values to know if the insert was ok or no. Posible values
+        //           are false = failed, integer = insert ok, id returned.
+        $end = false;
+        if (is_bool($result)) {
+            $end = $result;
+        } else if (is_resource($result)) {
+            $end = mssql_result($result, 0, 0); // Fetch 1st column from 1st row.
+        }
+        $this->query_end($end); // End the query with the calculated $end.
 
         if ($returning !== "") {
-            $row = mssql_fetch_assoc($result);
-            $params['id'] = reset($row);
+            $params['id'] = $end;
         }
         $this->free_result($result);
 
index 0ef6eca..410c7ff 100644 (file)
@@ -2078,6 +2078,34 @@ class dml_testcase extends database_driver_testcase {
         $this->assertEquals(1e-300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
         $id = $DB->insert_record($tablename, array('onetext' => 1e300));
         $this->assertEquals(1e300, $DB->get_field($tablename, 'onetext', array('id' => $id)));
+
+        // Test that inserting data violating one unique key leads to error.
+        // Empty the table completely.
+        $this->assertTrue($DB->delete_records($tablename));
+
+        // Add one unique constraint (index).
+        $key = new xmldb_key('testuk', XMLDB_KEY_UNIQUE, array('course', 'oneint'));
+        $dbman->add_key($table, $key);
+
+        // Let's insert one record violating the constraint multiple times.
+        $record = (object)array('course' => 1, 'oneint' => 1);
+        $this->assertTrue($DB->insert_record($tablename, $record, false)); // insert 1st. No problem expected.
+
+        // Re-insert same record, not returning id. dml_exception expected.
+        try {
+            $DB->insert_record($tablename, $record, false);
+            $this->fail("Expecting an exception, none occurred");
+        } catch (exception $e) {
+            $this->assertTrue($e instanceof dml_exception);
+        }
+
+        // Re-insert same record, returning id. dml_exception expected.
+        try {
+            $DB->insert_record($tablename, $record, true);
+            $this->fail("Expecting an exception, none occurred");
+        } catch (exception $e) {
+            $this->assertTrue($e instanceof dml_exception);
+        }
     }
 
     public function test_import_record() {
index a5a37a3..2f3ea57 100644 (file)
@@ -505,7 +505,7 @@ class file_info_stored extends file_info {
         if ($this->is_directory()) {
             $filepath = $this->lf->get_filepath();
             $fs = get_file_storage();
-            $storedfiles = $fs->get_area_files($this->context->id, $this->get_component(), $this->lf->get_filearea(), $this->lf->get_itemid(), "");
+            $storedfiles = $fs->get_area_files($this->context->id, $this->get_component(), $this->lf->get_filearea(), $this->lf->get_itemid());
             foreach ($storedfiles as $file) {
                 if (strpos($file->get_filepath(), $filepath) === 0) {
                     $file->delete();
index aa46e62..7aa8841 100644 (file)
@@ -2287,14 +2287,22 @@ function send_stored_file($stored_file, $lifetime=86400 , $filter=0, $forcedownl
     if (!empty($options['preview'])) {
         // replace the file with its preview
         $fs = get_file_storage();
-        $stored_file = $fs->get_file_preview($stored_file, $options['preview']);
-        if (!$stored_file) {
-            // unable to create a preview of the file
-            send_header_404();
-            die();
+        $preview_file = $fs->get_file_preview($stored_file, $options['preview']);
+        if (!$preview_file) {
+            // unable to create a preview of the file, send its default mime icon instead
+            if ($options['preview'] === 'tinyicon') {
+                $size = 24;
+            } else if ($options['preview'] === 'thumb') {
+                $size = 90;
+            } else {
+                $size = 256;
+            }
+            $fileicon = file_file_icon($stored_file, $size);
+            send_file($CFG->dirroot.'/pix/'.$fileicon.'.png', basename($fileicon).'.png');
         } else {
             // preview images have fixed cache lifetime and they ignore forced download
             // (they are generated by GD and therefore they are considered reasonably safe).
+            $stored_file = $preview_file;
             $lifetime = DAYSECS;
             $filter = 0;
             $forcedownload = false;
@@ -3612,7 +3620,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
             }
 
             // fix file name automatically
-            if ($filename !== 'f1' and $filename !== 'f2') {
+            if ($filename !== 'f1' and $filename !== 'f2' and $filename !== 'f3') {
                 $filename = 'f1';
             }
 
@@ -3625,20 +3633,28 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 redirect($theme->pix_url('u/'.$filename, 'moodle')); // intentionally not cached
             }
 
-            if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'/.png')) {
-                if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'/.jpg')) {
-                    // bad reference - try to prevent future retries as hard as possible!
-                    if ($user = $DB->get_record('user', array('id'=>$context->instanceid), 'id, picture')) {
-                        if ($user->picture == 1 or $user->picture > 10) {
-                            $DB->set_field('user', 'picture', 0, array('id'=>$user->id));
+            if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.png')) {
+                if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.jpg')) {
+                    if ($filename === 'f3') {
+                        // f3 512x512px was introduced in 2.3, there might be only the smaller version.
+                        if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.png')) {
+                            $file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.jpg');
                         }
                     }
-                    // no redirect here because it is not cached
-                    $theme = theme_config::load($themename);
-                    $imagefile = $theme->resolve_image_location('u/'.$filename, 'moodle');
-                    send_file($imagefile, basename($imagefile), 60*60*24*14);
                 }
             }
+            if (!$file) {
+                // bad reference - try to prevent future retries as hard as possible!
+                if ($user = $DB->get_record('user', array('id'=>$context->instanceid), 'id, picture')) {
+                    if ($user->picture > 0) {
+                        $DB->set_field('user', 'picture', 0, array('id'=>$user->id));
+                    }
+                }
+                // no redirect here because it is not cached
+                $theme = theme_config::load($themename);
+                $imagefile = $theme->resolve_image_location('u/'.$filename, 'moodle');
+                send_file($imagefile, basename($imagefile), 60*60*24*14);
+            }
 
             send_stored_file($file, 60*60*24*365, 0, false, array('preview' => $preview)); // enable long caching, there are many images on each page
 
index edac5a5..d5ce7c1 100644 (file)
@@ -366,7 +366,7 @@ class file_storage {
      * Returns all files belonging to given repository
      *
      * @param int $repositoryid
-     * @param string $sort
+     * @param string $sort A fragment of SQL to use for sorting
      */
     public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
         global $DB;
@@ -374,8 +374,10 @@ class file_storage {
                   FROM {files} f
              LEFT JOIN {files_reference} r
                        ON f.referencefileid = r.id
-                 WHERE r.repositoryid = ?
-              ORDER BY $sort";
+                 WHERE r.repositoryid = ?";
+        if (!empty($sort)) {
+            $sql .= " ORDER BY {$sort}";
+        }
 
         $result = array();
         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
@@ -392,11 +394,11 @@ class file_storage {
      * @param string $component component
      * @param string $filearea file area
      * @param int $itemid item ID or all files if not specified
-     * @param string $sort sort fields
+     * @param string $sort A fragment of SQL to use for sorting
      * @param bool $includedirs whether or not include directories
      * @return array of stored_files indexed by pathanmehash
      */
-    public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
+    public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "sortorder, itemid, filepath, filename", $includedirs = true) {
         global $DB;
 
         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
@@ -414,8 +416,10 @@ class file_storage {
                  WHERE f.contextid = :contextid
                        AND f.component = :component
                        AND f.filearea = :filearea
-                       $itemidsql
-              ORDER BY $sort";
+                       $itemidsql";
+        if (!empty($sort)) {
+            $sql .= " ORDER BY {$sort}";
+        }
 
         $result = array();
         $filerecords = $DB->get_records_sql($sql, $conditions);
@@ -489,7 +493,7 @@ class file_storage {
      * @param int $filepath directory path
      * @param bool $recursive include all subdirectories
      * @param bool $includedirs include files and directories
-     * @param string $sort sort fields
+     * @param string $sort A fragment of SQL to use for sorting
      * @return array of stored_files indexed by pathanmehash
      */
     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
@@ -499,6 +503,8 @@ class file_storage {
             return array();
         }
 
+        $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
+
         if ($recursive) {
 
             $dirs = $includedirs ? "" : "AND filename <> '.'";
@@ -512,7 +518,7 @@ class file_storage {
                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
                            AND f.id <> :dirid
                            $dirs
-                  ORDER BY $sort";
+                           $orderby";
             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
 
             $files = array();
@@ -542,7 +548,7 @@ class file_storage {
                                AND f.itemid = :itemid AND f.filename = '.'
                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
                                AND f.id <> :dirid
-                      ORDER BY $sort";
+                               $orderby";
                 $reqlevel = substr_count($filepath, '/') + 1;
                 $filerecords = $DB->get_records_sql($sql, $params);
                 foreach ($filerecords as $filerecord) {
@@ -559,7 +565,7 @@ class file_storage {
                            ON f.referencefileid = r.id
                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
                            AND f.filepath = :filepath AND f.filename <> '.'
-                  ORDER BY $sort";
+                           $orderby";
 
             $filerecords = $DB->get_records_sql($sql, $params);
             foreach ($filerecords as $filerecord) {
@@ -1028,7 +1034,7 @@ class file_storage {
 
         $newrecord->timecreated  = $filerecord->timecreated;
         $newrecord->timemodified = $filerecord->timemodified;
-        $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname) : $filerecord->mimetype;
+        $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
@@ -1154,7 +1160,7 @@ class file_storage {
         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
         // get mimetype by magic bytes
-        $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname) : $filerecord->mimetype;
+        $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
 
         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
 
@@ -1260,6 +1266,8 @@ class file_storage {
             $filerecord->timemodified = $now;
         }
 
+        $transaction = $DB->start_delegated_transaction();
+
         // Insert file reference record.
         try {
             $referencerecord = new stdClass;
@@ -1292,6 +1300,8 @@ class file_storage {
 
         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
 
+        $transaction->allow_commit();
+
         // Adding repositoryid and reference to file record to create stored_file instance
         $filerecord->repositoryid = $repositoryid;
         $filerecord->reference = $reference;
@@ -1801,11 +1811,15 @@ class file_storage {
      * If file has a known extension, we return the mimetype based on extension.
      * Otherwise (when possible) we try to get the mimetype from file contents.
      *
-     * @param string $pathname
+     * @param string $pathname full path to the file
+     * @param string $filename correct file name with extension, if omitted will be taken from $path
      * @return string
      */
-    public static function mimetype($pathname) {
-        $type = mimeinfo('type', $pathname);
+    public static function mimetype($pathname, $filename = null) {
+        if (empty($filename)) {
+            $filename = $pathname;
+        }
+        $type = mimeinfo('type', $filename);
         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
             $finfo = new finfo(FILEINFO_MIME_TYPE);
             $type = mimeinfo_from_type('type', $finfo->file($pathname));
index a17d45f..c936716 100644 (file)
@@ -205,6 +205,8 @@ class stored_file {
         // Remove repository info.
         $this->repository = null;
 
+        $transaction = $DB->start_delegated_transaction();
+
         // Remove reference info from DB.
         $DB->delete_records('files_reference', array('id'=>$this->file_record->referencefileid));
 
@@ -216,6 +218,8 @@ class stored_file {
         $filerecord->referencefileid = null;
         $this->update($filerecord);
 
+        $transaction->allow_commit();
+
         // unset object variable
         unset($this->file_record->repositoryid);
         unset($this->file_record->reference);
@@ -247,6 +251,9 @@ class stored_file {
      */
     public function delete() {
         global $DB;
+
+        $transaction = $DB->start_delegated_transaction();
+
         // If other files referring to this file, we need convert them
         if ($files = $this->fs->get_references_by_storedfile($this)) {
             foreach ($files as $file) {
@@ -256,6 +263,9 @@ class stored_file {
         // Now delete file records in DB
         $DB->delete_records('files', array('id'=>$this->file_record->id));
         $DB->delete_records('files_reference', array('id'=>$this->file_record->referencefileid));
+
+        $transaction->allow_commit();
+
         // moves pool file to trash if content not needed any more
         $this->fs->deleted_file_cleanup($this->file_record->contenthash);
         return true; // BC only
index d571beb..41feea1 100644 (file)
@@ -77,6 +77,30 @@ class filestoragelib_testcase extends advanced_testcase {
         $previewtinyicon = $fs->get_file_preview($file, 'thumb');
         $this->assertInstanceOf('stored_file', $previewtinyicon);
         $this->assertEquals('6b9864ae1536a8eeef54e097319175a8be12f07c', $previewtinyicon->get_filename());
+
+        $this->setExpectedException('file_exception');
+        $fs->get_file_preview($file, 'amodewhichdoesntexist');
+    }
+
+    public function test_get_file_preview_nonimage() {
+        $this->resetAfterTest(true);
+        $syscontext = context_system::instance();
+        $filerecord = array(
+            'contextid' => $syscontext->id,
+            'component' => 'core',
+            'filearea'  => 'unittest',
+            'itemid'    => 0,
+            'filepath'  => '/textfiles/',
+            'filename'  => 'testtext.txt',
+        );
+
+        $fs = get_file_storage();
+        $fs->create_file_from_string($filerecord, 'text contents');
+        $textfile = $fs->get_file($syscontext->id, $filerecord['component'], $filerecord['filearea'],
+            $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']);
+
+        $preview = $fs->get_file_preview($textfile, 'thumb');
+        $this->assertFalse($preview);
     }
 
     /**
@@ -209,20 +233,14 @@ class filestoragelib_testcase extends advanced_testcase {
         $this->assertEquals($content, $importedfile->get_content());
     }
 
-    /**
-     * TODO: the tests following this line were added to demonstrate specific Oracle problems in
-     * MDL-33172. They need to be improved to properly evalulate the results of the tests. This is
-     * tracked in MDL-33326.
-     */
     private function setup_three_private_files() {
-        global $USER, $DB;
 
         $this->resetAfterTest(true);
 
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
+        $this->setUser($user->id);
         $usercontext = context_user::instance($user->id);
-        $USER = $DB->get_record('user', array('id'=>$user->id));
         // create a user private file
         $file1 = new stdClass;
         $file1->contextid = $usercontext->id;
@@ -235,53 +253,120 @@ class filestoragelib_testcase extends advanced_testcase {
 
         $fs = get_file_storage();
         $userfile1 = $fs->create_file_from_string($file1, 'file1 content');
+        $this->assertInstanceOf('stored_file', $userfile1);
+
         $file2 = clone($file1);
         $file2->filename = '2.txt';
         $userfile2 = $fs->create_file_from_string($file2, 'file2 content');
+        $this->assertInstanceOf('stored_file', $userfile2);
 
         $file3 = clone($file1);
         $file3->filename = '3.txt';
         $userfile3 = $fs->create_file_from_storedfile($file3, $userfile2);
+        $this->assertInstanceOf('stored_file', $userfile3);
 
         $user->ctxid = $usercontext->id;
 
         return $user;
     }
 
-
     public function test_get_area_files() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
 
         // Get area files with default options.
         $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
+
         // Should be the two files we added plus the folder.
         $this->assertEquals(4, count($areafiles));
 
+        // Verify structure.
+        foreach ($areafiles as $key => $file) {
+            $this->assertInstanceOf('stored_file', $file);
+            $this->assertEquals($key, $file->get_pathnamehash());
+        }
+
         // Get area files without a folder.
-        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'sortorder', false);
+        $folderlessfiles = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'sortorder', false);
         // Should be the two files without folder.
-        $this->assertEquals(3, count($areafiles));
+        $this->assertEquals(3, count($folderlessfiles));
+
+        // Verify structure.
+        foreach ($folderlessfiles as $key => $file) {
+            $this->assertInstanceOf('stored_file', $file);
+            $this->assertEquals($key, $file->get_pathnamehash());
+        }
 
         // Get area files ordered by id (was breaking on oracle).
-        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'id', false);
+        $filesbyid  = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'id', false);
         // Should be the two files without folder.
-        $this->assertEquals(3, count($areafiles));
+        $this->assertEquals(3, count($filesbyid));
+
+        // Verify structure.
+        foreach ($filesbyid as $key => $file) {
+            $this->assertInstanceOf('stored_file', $file);
+            $this->assertEquals($key, $file->get_pathnamehash());
+        }
 
         // Test with an itemid with no files
         $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private', 666, 'sortorder', false);
-        // Should none
-        $this->assertEquals(0, count($areafiles));
+        // Should be none.
+        $this->assertEmpty($areafiles);
     }
 
     public function test_get_area_tree() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
 
+
         // Get area files with default options.
-        $areafiles = $fs->get_area_tree($user->ctxid, 'user', 'private', 0);
-        $areafiles = $fs->get_area_tree($user->ctxid, 'user', 'private', 666);
-        //TODO: verify result!! MDL-33326
+        $areatree = $fs->get_area_tree($user->ctxid, 'user', 'private', 0);
+        $this->assertEmpty($areatree['subdirs']);
+        $this->assertNotEmpty($areatree['files']);
+        $this->assertCount(3, $areatree['files']);
+
+        // Ensure an empty try with a fake itemid.
+        $emptytree = $fs->get_area_tree($user->ctxid, 'user', 'private', 666);
+        $this->assertEmpty($emptytree['subdirs']);
+        $this->assertEmpty($emptytree['files']);
+
+        // Create a subdir.
+        $dir = $fs->create_directory($user->ctxid, 'user', 'private', 0, '/testsubdir/');
+        $this->assertInstanceOf('stored_file', $dir);
+
+        // Add a file to the subdir.
+        $filerecord = array(
+            'contextid' => $user->ctxid,
+            'component' => 'user',
+            'filearea'  => 'private',
+            'itemid'    => 0,
+            'filepath'  => '/testsubdir/',
+            'filename'  => 'test-get-area-tree.txt',
+        );
+
+        $directoryfile = $fs->create_file_from_string($filerecord, 'Test content');
+        $this->assertInstanceOf('stored_file', $directoryfile);
+
+        $areatree = $fs->get_area_tree($user->ctxid, 'user', 'private', 0);
+
+        // At the top level there should still be 3 files.
+        $this->assertCount(3, $areatree['files']);
+
+        // There should now be a subdirectory.
+        $this->assertCount(1, $areatree['subdirs']);
+
+        // The test subdir is named testsubdir.
+        $subdir = $areatree['subdirs']['testsubdir'];
+        $this->assertNotEmpty($subdir);
+        // It should have one file we added.
+        $this->assertCount(1, $subdir['files']);
+        // And no subdirs itself.
+        $this->assertCount(0, $subdir['subdirs']);
+
+        // Verify the file is the one we added.
+        $subdirfile = reset($subdir['files']);
+        $this->assertInstanceOf('stored_file', $subdirfile);
+        $this->assertEquals($filerecord['filename'], $subdirfile->get_filename());
     }
 
     public function test_get_file_by_id() {
@@ -294,6 +379,10 @@ class filestoragelib_testcase extends advanced_testcase {
         $filebyid = reset($areafiles);
         $shouldbesame = $fs->get_file_by_id($filebyid->get_id());
         $this->assertEquals($filebyid->get_contenthash(), $shouldbesame->get_contenthash());
+
+        // Test an id which doens't exist.
+        $doesntexist = $fs->get_file_by_id(99999);
+        $this->assertFalse($doesntexist);
     }
 
     public function test_get_file_by_hash() {
@@ -305,6 +394,10 @@ class filestoragelib_testcase extends advanced_testcase {
         $filebyhash = reset($areafiles);
         $shouldbesame = $fs->get_file_by_hash($filebyhash->get_pathnamehash());
         $this->assertEquals($filebyhash->get_id(), $shouldbesame->get_id());
+
+        // Test an hash which doens't exist.
+        $doesntexist = $fs->get_file_by_hash('DOESNTEXIST');
+        $this->assertFalse($doesntexist);
     }
 
     public function test_get_references_by_storedfile() {
@@ -316,7 +409,7 @@ class filestoragelib_testcase extends advanced_testcase {
 
         $testfile = reset($areafiles);
         $references = $fs->get_references_by_storedfile($testfile);
-        //TODO: verify result!! MDL-33326
+        // TODO MDL-33368 Verify result!!
     }
 
     public function test_get_external_files() {
@@ -327,45 +420,132 @@ class filestoragelib_testcase extends advanced_testcase {
         $userrepository = reset($repos);
         $this->assertInstanceOf('repository', $userrepository);
 
-        // this should break on oracle
+        // This should break on oracle.
         $fs->get_external_files($userrepository->id, 'id');
-        //TODO: verify result!! MDL-33326
-     }
+        // TODO MDL-33368 Verify result!!
+    }
+
+    public function test_create_directory_contextid_negative() {
+        $fs = get_file_storage();
+
+        $this->setExpectedException('file_exception');
+        $fs->create_directory(-1, 'core', 'unittest', 0, '/');
+    }
+
+    public function test_create_directory_contextid_invalid() {
+        $fs = get_file_storage();
+
+        $this->setExpectedException('file_exception');
+        $fs->create_directory('not an int', 'core', 'unittest', 0, '/');
+    }
+
+    public function test_create_directory_component_invalid() {
+        $fs = get_file_storage();
+        $syscontext = context_system::instance();
+
+        $this->setExpectedException('file_exception');
+        $fs->create_directory($syscontext->id, 'bad/component', 'unittest', 0, '/');
+    }
+
+    public function test_create_directory_filearea_invalid() {
+        $fs = get_file_storage();
+        $syscontext = context_system::instance();
+
+        $this->setExpectedException('file_exception');
+        $fs->create_directory($syscontext->id, 'core', 'bad-filearea', 0, '/');
+    }
+
+    public function test_create_directory_itemid_negative() {
+        $fs = get_file_storage();
+        $syscontext = context_system::instance();
+
+        $this->setExpectedException('file_exception');
+        $fs->create_directory($syscontext->id, 'core', 'unittest', -1, '/');
+    }
+
+    public function test_create_directory_itemid_invalid() {
+        $fs = get_file_storage();
+        $syscontext = context_system::instance();
+
+        $this->setExpectedException('file_exception');
+        $fs->create_directory($syscontext->id, 'core', 'unittest', 'notanint', '/');
+    }
+
+    public function test_create_directory_filepath_invalid() {
+        $fs = get_file_storage();
+        $syscontext = context_system::instance();
+
+        $this->setExpectedException('file_exception');
+        $fs->create_directory($syscontext->id, 'core', 'unittest', 0, '/not-with-trailing/or-leading-slash');
+    }
 
     public function test_get_directory_files() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
 
-        // This should also break on oracle.
-        $fs->create_directory($user->ctxid, 'user', 'private', 0, '/');
-        //TODO: verify result!! MDL-33326
+        $dir = $fs->create_directory($user->ctxid, 'user', 'private', 0, '/testsubdir/');
+        $this->assertInstanceOf('stored_file', $dir);
 
-        // Don't recurse with dirs
-        $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', false, true, 'id');
-        //TODO: verify result!! MDL-33326
+        // Add a file to the subdir.
+        $filerecord = array(
+            'contextid' => $user->ctxid,
+            'component' => 'user',
+            'filearea'  => 'private',
+            'itemid'    => 0,
+            'filepath'  => '/testsubdir/',
+            'filename'  => 'test-get-area-tree.txt',
+        );
 
-        // Don't recurse without dirs
-        $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', false, false, 'id');
-        //TODO: verify result!! MDL-33326
+        $directoryfile = $fs->create_file_from_string($filerecord, 'Test content');
+        $this->assertInstanceOf('stored_file', $directoryfile);
 
-        // Recurse with dirs
-        $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', true, true, 'id');
-        //TODO: verify result!! MDL-33326
-        // Recurse without dirs
-        $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', true, false, 'id');
-        //TODO: verify result!! MDL-33326
+        // Don't recurse without dirs
+        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', false, false, 'id');
+        // 3 files only.
+        $this->assertCount(3, $files);
+        foreach ($files as $key => $file) {
+            $this->assertInstanceOf('stored_file', $file);
+            $this->assertEquals($key, $file->get_pathnamehash());
+        }
+
+        // Don't recurse with dirs.
+        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', false, true, 'id');
+        // 3 files + 1 directory.
+        $this->assertCount(4, $files);
+        foreach ($files as $key => $file) {
+            $this->assertInstanceOf('stored_file', $file);
+            $this->assertEquals($key, $file->get_pathnamehash());
+        }
+
+        // Recurse with dirs.
+        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', true, true, 'id');
+        // 3 files + 1 directory +  1 subdir file.
+        $this->assertCount(5, $files);
+        foreach ($files as $key => $file) {
+            $this->assertInstanceOf('stored_file', $file);
+            $this->assertEquals($key, $file->get_pathnamehash());
+        }
+
+        // Recurse without dirs.
+        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', true, false, 'id');
+        // 3 files +  1 subdir file.
+        $this->assertCount(4, $files);
+        foreach ($files as $key => $file) {
+            $this->assertInstanceOf('stored_file', $file);
+            $this->assertEquals($key, $file->get_pathnamehash());
+        }
     }
 
     public function test_search_references() {
         $fs = get_file_storage();
         $references = $fs->search_references('testsearch');
-        //TODO: verify result!! MDL-33326
+        // TODO MDL-33368 Verify result!!
     }
 
     public function test_search_references_count() {
         $fs = get_file_storage();
         $references = $fs->search_references_count('testsearch');
-        //TODO: verify result!! MDL-33326
+        // TODO MDL-33368 Verify result!!
     }
 
     public function test_delete_area_files() {
@@ -383,6 +563,20 @@ class filestoragelib_testcase extends advanced_testcase {
         $this->assertEquals(0, count($areafiles));
     }
 
+    public function test_delete_area_files_itemid() {
+        $user = $this->setup_three_private_files();
+        $fs = get_file_storage();
+
+        // Get area files with default options.
+        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
+        // Should be the two files we added plus the folder.
+        $this->assertEquals(4, count($areafiles));
+        $fs->delete_area_files($user->ctxid, 'user', 'private', 9999);
+
+        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
+        $this->assertEquals(4, count($areafiles));
+    }
+
     public function test_delete_area_files_select() {
         $user = $this->setup_three_private_files();
         $fs = get_file_storage();
@@ -397,4 +591,579 @@ class filestoragelib_testcase extends advanced_testcase {
         // Should be the two files we added plus the folder.
         $this->assertEquals(0, count($areafiles));
     }
+
+    public function test_create_file_from_url() {
+        $this->resetAfterTest(true);
+
+        $syscontext = context_system::instance();
+        $filerecord = array(
+            'contextid' => $syscontext->id,
+            'component' => 'core',
+            'filearea'  => 'unittest',
+            'itemid'    => 0,
+            'filepath'  => '/downloadtest/',
+        );
+        $url = 'http://download.moodle.org/unittest/test.html';
+
+        $fs = get_file_storage();
+
+        // Test creating file without filename.
+        $file1 = $fs->create_file_from_url($filerecord, $url);
+        $this->assertInstanceOf('stored_file', $file1);
+
+        // Set filename.
+        $filerecord['filename'] = 'unit-test-filename.html';
+        $file2 = $fs->create_file_from_url($filerecord, $url);
+        $this->assertInstanceOf('stored_file', $file2);
+
+        // Use temporary file.
+        $filerecord['filename'] = 'unit-test-with-temp-file.html';
+        $file3 = $fs->create_file_from_url($filerecord, $url, null, true);
+        $file3 = $this->assertInstanceOf('stored_file', $file3);
+    }
+
+    public function test_cron() {
+        $this->resetAfterTest(true);
+
+        // Note: this is only testing DB compatibility atm, rather than
+        // that work is done.
+        $fs = get_file_storage();
+
+        $this->expectOutputRegex('/Cleaning up/');
+        $fs->cron();
+    }
+
+    public function test_is_area_empty() {
+        $user = $this->setup_three_private_files();
+        $fs = get_file_storage();
+
+        $this->assertFalse($fs->is_area_empty($user->ctxid, 'user', 'private'));
+
+        // File area with madeup itemid should be empty.
+        $this->assertTrue($fs->is_area_empty($user->ctxid, 'user', 'private', 9999));
+        // Still empty with dirs included.
+        $this->assertTrue($fs->is_area_empty($user->ctxid, 'user', 'private', 9999, false));
+    }
+
+    public function test_move_area_files_to_new_context() {
+        $this->resetAfterTest(true);
+
+        // Create a course with a page resource.
+        $course = $this->getDataGenerator()->create_course();
+        $page1 = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
+        $page1context = context_module::instance($page1->cmid);
+
+        // Add a file to the page.
+        $fs = get_file_storage();
+        $filerecord = array(
+            'contextid' => $page1context->id,
+            'component' => 'mod_page',
+            'filearea'  => 'content',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => 'unit-test-file.txt',
+        );
+
+        $originalfile = $fs->create_file_from_string($filerecord, 'Test content');
+        $this->assertInstanceOf('stored_file', $originalfile);
+
+        $pagefiles = $fs->get_area_files($page1context->id, 'mod_page', 'content', 0, 'sortorder', false);
+        // Should be one file in filearea.
+        $this->assertFalse($fs->is_area_empty($page1context->id, 'mod_page', 'content'));
+
+        // Create a new page.
+        $page2 = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
+        $page2context = context_module::instance($page2->cmid);
+
+        // Newly created page area is empty.
+        $this->assertTrue($fs->is_area_empty($page2context->id, 'mod_page', 'content'));
+
+        // Move the files.
+        $fs->move_area_files_to_new_context($page1context->id, $page2context->id, 'mod_page', 'content');
+
+        // Page2 filearea should no longer be empty.
+        $this->assertFalse($fs->is_area_empty($page2context->id, 'mod_page', 'content'));
+
+        // Page1 filearea should now be empty.
+        $this->assertTrue($fs->is_area_empty($page1context->id, 'mod_page', 'content'));
+
+        $page2files = $fs->get_area_files($page2context->id, 'mod_page', 'content', 0, 'sortorder', false);
+        $movedfile = reset($page2files);
+
+        // The two files should have the same content hash.
+        $this->assertEquals($movedfile->get_contenthash(), $originalfile->get_contenthash());
+    }
+
+    public function test_convert_image() {
+        global $CFG;
+
+        $this->resetAfterTest(false);
+
+        $filepath = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+        $syscontext = context_system::instance();
+        $filerecord = array(
+            'contextid' => $syscontext->id,
+            'component' => 'core',
+            'filearea'  => 'unittest',
+            'itemid'    => 0,
+            'filepath'  => '/images/',
+            'filename'  => 'testimage.jpg',
+        );
+
+        $fs = get_file_storage();
+        $original = $fs->create_file_from_pathname($filerecord, $filepath);
+
+        $filerecord['filename'] = 'testimage-converted-10x10.jpg';
+        $converted = $fs->convert_image($filerecord, $original, 10, 10, true, 100);
+        $this->assertInstanceOf('stored_file', $converted);
+
+        $filerecord['filename'] = 'testimage-convereted-nosize.jpg';
+        $converted = $fs->convert_image($filerecord, $original);
+        $this->assertInstanceOf('stored_file', $converted);
+    }
+
+    private function generate_file_record() {
+        $syscontext = context_system::instance();
+        $filerecord = new stdClass();
+        $filerecord->contextid = $syscontext->id;
+        $filerecord->component = 'core';
+        $filerecord->filearea = 'phpunit';
+        $filerecord->filepath = '/';
+        $filerecord->filename = 'testfile.txt';
+        $filerecord->itemid = 0;
+
+        return $filerecord;
+    }
+
+    public function test_create_file_from_storedfile_file_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $this->setExpectedException('file_exception');
+        // Create a file from a file id which doesn't exist.
+        $fs->create_file_from_storedfile($filerecord,  9999);
+    }
+
+    public function test_create_file_from_storedfile_contextid_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'invalid.txt';
+        $filerecord->contextid = 'invalid';
+
+        $this->setExpectedException('file_exception', 'Invalid contextid');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_component_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'invalid.txt';
+        $filerecord->component = 'bad/component';
+
+        $this->setExpectedException('file_exception', 'Invalid component');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_filearea_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'invalid.txt';
+        $filerecord->filearea = 'bad-filearea';
+
+        $this->setExpectedException('file_exception', 'Invalid filearea');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_itemid_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'invalid.txt';
+        $filerecord->itemid = 'bad-itemid';
+
+        $this->setExpectedException('file_exception', 'Invalid itemid');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_filepath_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'invalid.txt';
+        $filerecord->filepath = 'a-/bad/-filepath';
+
+        $this->setExpectedException('file_exception', 'Invalid file path');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_filename_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = '';
+
+        $this->setExpectedException('file_exception', 'Invalid file name');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_timecreated_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'invalid.txt';
+        $filerecord->timecreated = 'today';
+
+        $this->setExpectedException('file_exception', 'Invalid file timecreated');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_timemodified_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'invalid.txt';
+        $filerecord->timemodified  = 'today';
+
+        $this->setExpectedException('file_exception', 'Invalid file timemodified');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile_duplicate() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+
+        $fs = get_file_storage();
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        // Creating a file validating unique constraint.
+        $this->setExpectedException('stored_file_creation_exception');
+        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+    }
+
+    public function test_create_file_from_storedfile() {
+        $this->resetAfterTest(true);
+
+        $syscontext = context_system::instance();
+
+        $filerecord = new stdClass();
+        $filerecord->contextid = $syscontext->id;
+        $filerecord->component = 'core';
+        $filerecord->filearea = 'phpunit';
+        $filerecord->filepath = '/';
+        $filerecord->filename = 'testfile.txt';
+        $filerecord->itemid = 0;
+
+        $fs = get_file_storage();
+
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+        $this->assertInstanceOf('stored_file', $file1);
+
+        $filerecord->filename = 'test-create-file-from-storedfile.txt';
+        $file2 = $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+        $this->assertInstanceOf('stored_file', $file2);
+
+        // These will be normalised to current time..
+        $filerecord->timecreated = -100;
+        $filerecord->timemodified= -100;
+        $filerecord->filename = 'test-create-file-from-storedfile-bad-dates.txt';
+
+        $file3 = $fs->create_file_from_storedfile($filerecord, $file1->get_id());
+        $this->assertInstanceOf('stored_file', $file3);
+
+        $this->assertNotEquals($file3->get_timemodified(), $filerecord->timemodified);
+        $this->assertNotEquals($file3->get_timecreated(), $filerecord->timecreated);
+    }
+
+    public function test_create_file_from_string_contextid_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->contextid = 'invalid';
+
+        $this->setExpectedException('file_exception', 'Invalid contextid');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_component_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->component = 'bad/component';
+
+        $this->setExpectedException('file_exception', 'Invalid component');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_filearea_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->filearea = 'bad-filearea';
+
+        $this->setExpectedException('file_exception', 'Invalid filearea');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_itemid_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->itemid = 'bad-itemid';
+
+        $this->setExpectedException('file_exception', 'Invalid itemid');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_filepath_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->filepath = 'a-/bad/-filepath';
+
+        $this->setExpectedException('file_exception', 'Invalid file path');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_filename_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->filename = '';
+
+        $this->setExpectedException('file_exception', 'Invalid file name');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_timecreated_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->timecreated = 'today';
+
+        $this->setExpectedException('file_exception', 'Invalid file timecreated');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_timemodified_invalid() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->timemodified  = 'today';
+
+        $this->setExpectedException('file_exception', 'Invalid file timemodified');
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_string_duplicate() {
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
+
+        // Creating a file validating unique constraint.
+        $this->setExpectedException('stored_file_creation_exception');
+        $file2 = $fs->create_file_from_string($filerecord, 'text contents');
+    }
+
+    public function test_create_file_from_pathname_contextid_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->contextid = 'invalid';
+
+        $this->setExpectedException('file_exception', 'Invalid contextid');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_component_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->component = 'bad/component';
+
+        $this->setExpectedException('file_exception', 'Invalid component');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_filearea_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->filearea = 'bad-filearea';
+
+        $this->setExpectedException('file_exception', 'Invalid filearea');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_itemid_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->itemid = 'bad-itemid';
+
+        $this->setExpectedException('file_exception', 'Invalid itemid');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_filepath_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->filepath = 'a-/bad/-filepath';
+
+        $this->setExpectedException('file_exception', 'Invalid file path');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_filename_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->filename = '';
+
+        $this->setExpectedException('file_exception', 'Invalid file name');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_timecreated_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->timecreated = 'today';
+
+        $this->setExpectedException('file_exception', 'Invalid file timecreated');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_timemodified_invalid() {
+        global $CFG;
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $this->resetAfterTest(true);
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $filerecord->timemodified  = 'today';
+
+        $this->setExpectedException('file_exception', 'Invalid file timemodified');
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+    }
+
+    public function test_create_file_from_pathname_duplicate_file() {
+        global $CFG;
+        $this->resetAfterTest(true);
+
+        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
+
+        $filerecord = $this->generate_file_record();
+        $fs = get_file_storage();
+
+        $file1 = $fs->create_file_from_pathname($filerecord, $path);
+        $this->assertInstanceOf('stored_file', $file1);
+
+        // Creating a file validating unique constraint.
+        $this->setExpectedException('stored_file_creation_exception');
+        $file2 = $fs->create_file_from_pathname($filerecord, $path);
+    }
 }
index bf7316a..2e1514c 100644 (file)
@@ -91,6 +91,7 @@ M.form_dndupload.init = function(Y, options) {
             this.maxfiles = options.maxfiles;
             this.maxbytes = options.maxbytes;
             this.itemid = options.itemid;
+            this.author = options.author;
             this.container = this.Y.one('#'+options.containerid);
 
             if (options.filemanager) {
@@ -452,6 +453,9 @@ M.form_dndupload.init = function(Y, options) {
             formdata.append('sesskey', M.cfg.sesskey);
             formdata.append('repo_id', this.repositoryid);
             formdata.append('itemid', this.itemid);
+            if (this.author) {
+                formdata.append('author', this.author);
+            }
             if (this.filemanager) { // Filepickers do not have folders
                 formdata.append('savepath', this.filemanager.currentpath);
             }
index 592dc1c..6627c2a 100644 (file)
@@ -963,6 +963,7 @@ M.form_filemanager.init = function(Y, options) {
         filemanager: manager,
         acceptedtypes: options.accepted_types,
         clientid: options.client_id,
+        author: options.author,
         maxfiles: options.maxfiles,
         maxbytes: options.maxbytes,
         itemid: options.itemid,
index 4b5de33..e372a23 100644 (file)
@@ -47,6 +47,7 @@ M.form_filepicker.init = function(Y, options) {
     var dndoptions = {
         clientid: options.client_id,
         acceptedtypes: options.accepted_types,
+        author: options.author,
         maxfiles: -1,
         maxbytes: options.maxbytes,
         itemid: options.itemid,
index 68d891a..b4e4918 100644 (file)
@@ -119,6 +119,7 @@ function process_new_icon($context, $component, $filearea, $itemid, $originalfil
     $image->height = $imageinfo[1];
     $image->type   = $imageinfo[2];
 
+    $t = null;
     switch ($image->type) {
         case IMAGETYPE_GIF:
             if (function_exists('imagecreatefromgif')) {
@@ -127,6 +128,11 @@ function process_new_icon($context, $component, $filearea, $itemid, $originalfil
                 debugging('GIF not supported on this server');
                 return false;
             }
+            // Guess transparent colour from GIF.
+            $transparent = imagecolortransparent($im);
+            if ($transparent != -1) {
+                $t = imagecolorsforindex($im, $transparent);
+            }
             break;
         case IMAGETYPE_JPEG:
             if (function_exists('imagecreatefromjpeg')) {
@@ -166,19 +172,37 @@ function process_new_icon($context, $component, $filearea, $itemid, $originalfil
     if (function_exists('imagecreatetruecolor') and $CFG->gdversion >= 2) {
         $im1 = imagecreatetruecolor(100, 100);
         $im2 = imagecreatetruecolor(35, 35);
-        if ($image->type == IMAGETYPE_PNG and $imagefnc === 'imagepng') {
+        $im3 = imagecreatetruecolor(512, 512);
+        if ($image->type != IMAGETYPE_JPEG and $imagefnc === 'imagepng') {
+            if ($t) {
+                // Transparent GIF hacking...
+                $transparentcolour = imagecolorallocate($im1 , $t['red'] , $t['green'] , $t['blue']);
+                imagecolortransparent($im1 , $transparentcolour);
+                $transparentcolour = imagecolorallocate($im2 , $t['red'] , $t['green'] , $t['blue']);
+                imagecolortransparent($im2 , $transparentcolour);
+                $transparentcolour = imagecolorallocate($im3 , $t['red'] , $t['green'] , $t['blue']);
+                imagecolortransparent($im3 , $transparentcolour);
+            }
+
             imagealphablending($im1, false);
             $color = imagecolorallocatealpha($im1, 0, 0,  0, 127);
             imagefill($im1, 0, 0,  $color);
             imagesavealpha($im1, true);
+
             imagealphablending($im2, false);
             $color = imagecolorallocatealpha($im2, 0, 0,  0, 127);
             imagefill($im2, 0, 0,  $color);
             imagesavealpha($im2, true);
+
+            imagealphablending($im3, false);
+            $color = imagecolorallocatealpha($im3, 0, 0,  0, 127);
+            imagefill($im3, 0, 0,  $color);
+            imagesavealpha($im3, true);
         }
     } else {
         $im1 = imagecreate(100, 100);
         $im2 = imagecreate(35, 35);
+        $im3 = imagecreate(512, 512);
     }
 
     $cx = $image->width / 2;
@@ -192,6 +216,7 @@ function process_new_icon($context, $component, $filearea, $itemid, $originalfil
 
     imagecopybicubic($im1, $im, 0, 0, $cx - $half, $cy - $half, 100, 100, $half * 2, $half * 2);
     imagecopybicubic($im2, $im, 0, 0, $cx - $half, $cy - $half, 35, 35, $half * 2, $half * 2);
+    imagecopybicubic($im3, $im, 0, 0, $cx - $half, $cy - $half, 512, 512, $half * 2, $half * 2);
 
     $fs = get_file_storage();
 
@@ -220,6 +245,17 @@ function process_new_icon($context, $component, $filearea, $itemid, $originalfil
     $icon['filename'] = 'f2'.$imageext;
     $fs->create_file_from_string($icon, $data);
 
+    ob_start();
+    if (!$imagefnc($im3, NULL, $quality, $filters)) {
+        ob_end_clean();
+        $fs->delete_area_files($context->id, $component, $filearea, $itemid);
+        return false;
+    }
+    $data = ob_get_clean();
+    imagedestroy($im3);
+    $icon['filename'] = 'f3'.$imageext;
+    $fs->create_file_from_string($icon, $data);
+
     return $file1->get_id();
 }
 
index a079ffb..5994dd3 100644 (file)
 <?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
 /**
- * Moodle - Modular Object-Oriented Dynamic Learning Environment
- *          http://moodle.org
- * Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * Simple implementation of some Google API functions for Moodle.
  *
- * @package    core
- * @subpackage lib
+ * @package   core
  * @copyright Dan Poltawski <talktodan@gmail.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- *
- * Simple implementation of some Google API functions for Moodle.
  */
 
 defined('MOODLE_INTERNAL') || die();
 
- /** Include essential file */
 require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->libdir.'/oauthlib.php');
 
 /**
- * Base class for google authenticated http requests
- *
- * Most Google API Calls required that requests are sent with an
- * Authorization header + token. This class extends the curl class
- * to aid this
+ * Class for manipulating google documents through the google data api.
  *
- * @package    moodlecore
- * @subpackage lib
- * @copyright Dan Poltawski <talktodan@gmail.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-abstract class google_auth_request extends curl {
-    protected $token = '';
-    private $persistantheaders = array();
-
-    // Must be overridden with the authorization header name
-    public static function get_auth_header_name() {
-        throw new coding_exception('get_auth_header_name() method needs to be overridden in each subclass of google_auth_request');
-    }
-
-    protected function request($url, $options = array()){
-        if($this->token){
-            // Adds authorisation head to a request so that it can be authentcated
-            $this->setHeader('Authorization: '. $this->get_auth_header_name().'"'.$this->token.'"');
-        }
-
-        foreach($this->persistantheaders as $h){
-            $this->setHeader($h);
-        }
-
-        $ret = parent::request($url, $options);
-        // reset headers for next request
-        $this->header = array();
-        return $ret;
-    }
-
-    protected function multi($requests, $options = array()) {
-        if($this->token){
-            // Adds authorisation head to a request so that it can be authentcated
-            $this->setHeader('Authorization: '. $this->get_auth_header_name().'"'.$this->token.'"');
-        }
-
-        foreach($this->persistantheaders as $h){
-            $this->setHeader($h);
-        }
-
-        $ret = parent::multi($requests, $options);
-        // reset headers for next request
-        $this->header = array();
-        return $ret;
-    }
-
-    public function get_sessiontoken(){
-        return $this->token;
-    }
-
-    public function add_persistant_header($header){
-        $this->persistantheaders[] = $header;
-    }
-}
-
-/*******
- * The following two classes are usd to implement AuthSub google
- * authtentication, as documented here:
- * http://code.google.com/apis/accounts/docs/AuthSub.html
- *******/
-
-/**
- * Used to uprade a google AuthSubRequest one-time token into
- * a session token which can be used long term.
- *
- * @package    moodlecore
- * @subpackage lib
- * @copyright Dan Poltawski <talktodan@gmail.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class google_authsub_request extends google_auth_request {
-    const AUTHSESSION_URL = 'https://www.google.com/accounts/AuthSubSessionToken';
-
-    /**
-     * Constructor. Calls constructor of its parents
-     *
-     * @param string $authtoken The token to upgrade to a session token
-     */
-    public function __construct($authtoken){
-        parent::__construct();
-        $this->token = $authtoken;
-    }
-
-    /**
-     * Requests a long-term session token from google based on the
-     *
-     * @return string Sub-Auth token
-     */
-    public function get_session_token(){
-        $content = $this->get(google_authsub_request::AUTHSESSION_URL);
-
-        if( preg_match('/token=(.*)/i', $content, $matches) ){
-            return $matches[1];
-        }else{
-            throw new moodle_exception('could not upgrade google authtoken to session token');
-        }
-    }
-
-    public static function get_auth_header_name(){
-        return 'AuthSub token=';
-    }
-}
-
-/**
- * Allows http calls using google subauth authorisation
- *
- * @package    moodlecore
- * @subpackage lib
- * @copyright Dan Poltawski <talktodan@gmail.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class google_authsub extends google_auth_request {
-    const LOGINAUTH_URL    = 'https://www.google.com/accounts/AuthSubRequest';
-    const VERIFY_TOKEN_URL = 'https://www.google.com/accounts/AuthSubTokenInfo';
-    const REVOKE_TOKEN_URL = 'https://www.google.com/accounts/AuthSubRevokeToken';
-
-    /**
-     * Constructor, allows subauth requests using the response from an initial
-     * AuthSubRequest or with the subauth long-term token. Note that constructing
-     * this object without a valid token will cause an exception to be thrown.
-     *
-     * @param string $sessiontoken A long-term subauth session token
-     * @param string $authtoken A one-time auth token wich is used to upgrade to session token
-     * @param mixed  @options Options to pass to the base curl object
-     */
-    public function __construct($sessiontoken = '', $authtoken = '', $options = array()){
-        parent::__construct($options);
-
-        if( $authtoken ){
-            $gauth = new google_authsub_request($authtoken);
-            $sessiontoken = $gauth->get_session_token();
-        }
-
-        $this->token = $sessiontoken;
-        if(! $this->valid_token() ){
-            throw new moodle_exception('Invalid subauth token');
-        }
-    }
-
-    /**
-     * Tests if a subauth token used is valid
-     *
-     * @return boolean true if token valid
-     */
-    public function valid_token(){
-        $this->get(google_authsub::VERIFY_TOKEN_URL);
-
-        if($this->info['http_code'] === 200){
-            return true;
-        }else{
-            return false;
-        }
-    }
-
-    /**
-     * Calls googles api to revoke the subauth token
-     *
-     * @return boolean Returns true if token succesfully revoked
-     */
-    public function revoke_session_token(){
-        $this->get(google_authsub::REVOKE_TOKEN_URL);
-
-        if($this->info['http_code'] === 200){
-            $this->token = '';
-            return true;
-        }else{
-            return false;
-        }
-    }
-
-    /**
-     * Creates a login url for subauth request
-     *
-     * @param string $returnaddr The address which the user should be redirected to recieve the token
-     * @param string $realm The google realm which is access is being requested
-     * @return string URL to bounce the user to
-     */
-    public static function login_url($returnaddr, $realm){
-        $uri = google_authsub::LOGINAUTH_URL.'?next='
-            .urlencode($returnaddr)
-            .'&scope='
-            .urlencode($realm)
-            .'&session=1&secure=0';
-
-        return $uri;
-    }
-
-    public static function get_auth_header_name(){
-        return 'AuthSub token=';
-    }
-}
-
-/**
- * Class for manipulating google documents through the google data api
  * Docs for this can be found here:
  * {@link http://code.google.com/apis/documents/docs/2.0/developers_guide_protocol.html}
  *
- * @package    moodlecore
+ * @package    core
  * @subpackage lib
  * @copyright Dan Poltawski <talktodan@gmail.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class google_docs {
-    // need both docs and the spreadsheets realm
+    /** @var string Realm for authentication, need both docs and spreadsheet realm */
     const REALM            = 'https://docs.google.com/feeds/ https://spreadsheets.google.com/feeds/ https://docs.googleusercontent.com/';
+    /** @var string Document list url */
     const DOCUMENTFEED_URL = 'https://docs.google.com/feeds/default/private/full';
-    const USER_PREF_NAME   = 'google_authsub_sesskey';
+    /** @var string Upload url */
+    const UPLOAD_URL       = 'https://docs.google.com/feeds/upload/create-session/default/private/full?convert=false';
 
-    private $google_curl = null;
+    /** @var google_oauth oauth curl class for making authenticated requests */
+    private $googleoauth = null;
 
     /**
      * Constructor.
      *
-     * @param object A google_auth_request object which can be used to do http requests
+     * @param google_oauth $googleoauth oauth curl class for making authenticated requests
      */
-    public function __construct($google_curl){
-        if(is_a($google_curl, 'google_auth_request')){
-            $this->google_curl = $google_curl;
-            $this->google_curl->add_persistant_header('GData-Version: 3.0');
-        }else{
-            throw new moodle_exception('Google Curl Request object not given');
-        }
-    }
-
-    public static function get_sesskey($userid){
-        return get_user_preferences(google_docs::USER_PREF_NAME, false, $userid);
-    }
-
-    public static function set_sesskey($value, $userid){
-        return set_user_preference(google_docs::USER_PREF_NAME, $value, $userid);
-    }
-
-    public static function delete_sesskey($userid){
-        return unset_user_preference(google_docs::USER_PREF_NAME, $userid);
+    public function __construct(google_oauth $googleoauth) {
+        $this->googleoauth = $googleoauth;
+        $this->googleoauth->setHeader('GData-Version: 3.0');
     }
 
     /**
@@ -279,21 +65,19 @@ class google_docs {
      * @param string $search A search string to do full text search on the documents
      * @return mixed Array of files formated for fileapoi
      */
-    #FIXME
-    public function get_file_list($search = ''){
+    public function get_file_list($search = '') {
         global $CFG, $OUTPUT;
-        $url = google_docs::DOCUMENTFEED_URL;
+        $url = self::DOCUMENTFEED_URL;
 
-        if($search){
+        if ($search) {
             $url.='?q='.urlencode($search);
         }
-        $content = $this->google_curl->get($url);
+        $content = $this->googleoauth->get($url);
 
         $xml = new SimpleXMLElement($content);
 
-
         $files = array();
-        foreach($xml->entry as $gdoc){
+        foreach ($xml->entry as $gdoc) {
             $docid  = (string) $gdoc->children('http://schemas.google.com/g/2005')->resourceId;
             list($type, $docid) = explode(':', $docid);
 
@@ -319,9 +103,13 @@ class google_docs {
                     $title  = (string)$gdoc->title;
                     $source = (string)$gdoc->content[0]->attributes()->src;
                     break;
+                case 'file':
+                    $title = (string)$gdoc->title;
+                    $source = (string)$gdoc->content[0]->attributes()->src;
+                    break;
             }
 
-            if(!empty($source)){
+            if (!empty($source)) {
                 $files[] =  array( 'title' => $title,
                     'url' => "{$gdoc->link[0]->attributes()->href}",
                     'source' => $source,
@@ -340,71 +128,98 @@ class google_docs {
      * @param object $file File object
      * @return boolean True on success
      */
-    public function send_file($file){
-        $this->google_curl->setHeader("Content-Length: ". $file->get_filesize());
-        $this->google_curl->setHeader("Content-Type: ". $file->get_mimetype());
-        $this->google_curl->setHeader("Slug: ". $file->get_filename());
+    public function send_file($file) {
+        // First we create the 'resumable upload request'.
+        $this->googleoauth->setHeader("Content-Length: 0");
+        $this->googleoauth->setHeader("X-Upload-Content-Length: ". $file->get_filesize());
+        $this->googleoauth->setHeader("X-Upload-Content-Type: ". $file->get_mimetype());
+        $this->googleoauth->setHeader("Slug: ". $file->get_filename());
+        $this->googleoauth->post(self::UPLOAD_URL);
+
+        if ($this->googleoauth->info['http_code'] !== 200) {
+            throw new moodle_exception('Cantpostupload');
+        }
+
+        // Now we http PUT the file in the location returned.
+        $location = $this->googleoauth->response['Location'];
+        if (empty($location)) {
+            throw new moodle_exception('Nouploadlocation');
+        }
+
+        // Reset the curl object for actually sending the file.
+        $this->googleoauth->clear_headers();
+        $this->googleoauth->setHeader("Content-Length: ". $file->get_filesize());
+        $this->googleoauth->setHeader("Content-Type: ". $file->get_mimetype());
+
+        // We can't get a filepointer, so have to copy the file..
+        $tmproot = make_temp_directory('googledocsuploads');
+        $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
+        $file->copy_content_to($tmpfilepath);
 
-        $this->google_curl->post(google_docs::DOCUMENTFEED_URL, $file->get_content());
+        // HTTP PUT the file.
+        $this->googleoauth->put($location, array('file'=>$tmpfilepath));
 
-        if($this->google_curl->info['http_code'] === 201){
+        // Remove the temporary file we created..
+        unlink($tmpfilepath);
+
+        if ($this->googleoauth->info['http_code'] === 201) {
             return true;
-        }else{
+        } else {
             return false;
         }
     }
 
-    public function download_file($url, $fp){
-        return $this->google_curl->download(array( array('url'=>$url, 'file' => $fp) ));
+    /**
+     * Downloads a file using authentication
+     *
+     * @param string $url url of file
+     * @param string $path path to save file to
+     * @return array stucture for repository download_file
+     */
+    public function download_file($url, $path) {
+        $content = $this->googleoauth->get($url);
+        file_put_contents($path, $content);
+        return array('path'=>$path, 'url'=>$url);
     }
 }
 
 /**
- * Class for manipulating picasa through the google data api
+ * Class for manipulating picasa through the google data api.
+ *
  * Docs for this can be found here:
  * {@link http://code.google.com/apis/picasaweb/developers_guide_protocol.html}
  *
- * @package    moodlecore
- * @subpackage lib
+ * @package   core
  * @copyright Dan Poltawski <talktodan@gmail.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class google_picasa {
+    /** @var string Realm for authentication */
     const REALM             = 'http://picasaweb.google.com/data/';
-    const USER_PREF_NAME    = 'google_authsub_sesskey_picasa';
+    /** @var string Upload url */
     const UPLOAD_LOCATION   = 'https://picasaweb.google.com/data/feed/api/user/default/albumid/default';
+    /** @var string photo list url */
     const ALBUM_PHOTO_LIST  = 'https://picasaweb.google.com/data/feed/api/user/default/albumid/';
+    /** @var string search url */
     const PHOTO_SEARCH_URL  = 'https://picasaweb.google.com/data/feed/api/user/default?kind=photo&q=';
+    /** @var string album list url */
     const LIST_ALBUMS_URL   = 'https://picasaweb.google.com/data/feed/api/user/default';
+    /** @var string manage files url */
     const MANAGE_URL        = 'http://picasaweb.google.com/';
 
-    private $google_curl = null;
+    /** @var google_oauth oauth curl class for making authenticated requests */
+    private $googleoauth = null;
+    /** @var string Last album name retrievied */
     private $lastalbumname = null;
 
     /**
      * Constructor.
      *
-     * @param object A google_auth_request object which can be used to do http requests
+     * @param google_oauth $googleoauth oauth curl class for making authenticated requests
      */
-    public function __construct($google_curl){
-        if(is_a($google_curl, 'google_auth_request')){
-            $this->google_curl = $google_curl;
-            $this->google_curl->add_persistant_header('GData-Version: 2');
-        }else{
-            throw new moodle_exception('Google Curl Request object not given');
-        }
-    }
-
-    public static function get_sesskey($userid){
-        return get_user_preferences(google_picasa::USER_PREF_NAME, false, $userid);
-    }
-
-    public static function set_sesskey($value, $userid){
-        return set_user_preference(google_picasa::USER_PREF_NAME, $value, $userid);
-    }
-
-    public static function delete_sesskey($userid){
-        return unset_user_preference(google_picasa::USER_PREF_NAME, $userid);
+    public function __construct(google_oauth $googleoauth) {
+        $this->googleoauth = $googleoauth;
+        $this->googleoauth->setHeader('GData-Version: 2');
     }
 
     /**
@@ -413,16 +228,16 @@ class google_picasa {
      * @param object $file File object
      * @return boolean True on success
      */
-    public function send_file($file){
-        $this->google_curl->setHeader("Content-Length: ". $file->get_filesize());
-        $this->google_curl->setHeader("Content-Type: ". $file->get_mimetype());
-        $this->google_curl->setHeader("Slug: ". $file->get_filename());
+    public function send_file($file) {
+        $this->googleoauth->setHeader("Content-Length: ". $file->get_filesize());
+        $this->googleoauth->setHeader("Content-Type: ". $file->get_mimetype());
+        $this->googleoauth->setHeader("Slug: ". $file->get_filename());
 
-        $this->google_curl->post(google_picasa::UPLOAD_LOCATION, $file->get_content());
+        $this->googleoauth->post(self::UPLOAD_LOCATION, $file->get_content());
 
-        if($this->google_curl->info['http_code'] === 201){
+        if ($this->googleoauth->info['http_code'] === 201) {
             return true;
-        }else{
+        } else {
             return false;
         }
     }
@@ -435,10 +250,10 @@ class google_picasa {
      * @param string $path The path to files (assumed to be albumid)
      * @return mixed $files A list of files for the file picker
      */
-    public function get_file_list($path = ''){
-        if(!$path){
+    public function get_file_list($path = '') {
+        if (!$path) {
             return $this->get_albums();
-        }else{
+        } else {
             return $this->get_album_photos($path);
         }
     }
@@ -449,8 +264,8 @@ class google_picasa {
      * @param int $albumid Photo album to list photos from
      * @return mixed $files A list of files for the file picker
      */
-    public function get_album_photos($albumid){
-        $albumcontent = $this->google_curl->get(google_picasa::ALBUM_PHOTO_LIST.$albumid);
+    public function get_album_photos($albumid) {
+        $albumcontent = $this->googleoauth->get(self::ALBUM_PHOTO_LIST.$albumid);
 
         return $this->get_photo_details($albumcontent);
     }
@@ -471,8 +286,8 @@ class google_picasa {
      * @param string $query Search terms
      * @return mixed $files A list of files for the file picker
      */
-    public function do_photo_search($query){
-        $content = $this->google_curl->get(google_picasa::PHOTO_SEARCH_URL.htmlentities($query));
+    public function do_photo_search($query) {
+        $content = $this->googleoauth->get(self::PHOTO_SEARCH_URL.htmlentities($query));
 
         return $this->get_photo_details($content);
     }
@@ -483,17 +298,17 @@ class google_picasa {
      *
      * @return mixes $files Array in the format get_listing uses for folders
      */
-    public function get_albums(){
-        $content = $this->google_curl->get(google_picasa::LIST_ALBUMS_URL);
+    public function get_albums() {
+        $content = $this->googleoauth->get(self::LIST_ALBUMS_URL);
         $xml = new SimpleXMLElement($content);
 
         $files = array();
 
-        foreach($xml->entry as $album){
+        foreach ($xml->entry as $album) {
             $gphoto = $album->children('http://schemas.google.com/photos/2007');
 
             $mediainfo = $album->children('http://search.yahoo.com/mrss/');
-            //hacky...
+            // Hacky...
             $thumbnailinfo = $mediainfo->group->thumbnail[0]->attributes();
 
             $files[] = array( 'title' => (string) $album->title,
@@ -501,7 +316,7 @@ class google_picasa {
                 'size'  => (int) $gphoto->bytesUsed,
                 'path'  => (string) $gphoto->id,
                 'thumbnail' => (string) $thumbnailinfo['url'],
-                'thumbnail_width' => 160,  // 160 is the native maximum dimension
+                'thumbnail_width' => 160,  // 160 is the native maximum dimension.
                 'thumbnail_height' => 160,
                 'children' => array(),
             );
@@ -518,22 +333,22 @@ class google_picasa {
      * @param string $rawxml XML from picasa api
      * @return mixed $files A list of files for the file picker
      */
-    public function get_photo_details($rawxml){
+    public function get_photo_details($rawxml) {
 
         $xml = new SimpleXMLElement($rawxml);
         $this->lastalbumname = (string)$xml->title;
 
         $files = array();
 
-        foreach($xml->entry as $photo){
+        foreach ($xml->entry as $photo) {
             $gphoto = $photo->children('http://schemas.google.com/photos/2007');
 
             $mediainfo = $photo->children('http://search.yahoo.com/mrss/');
             $fullinfo = $mediainfo->group->content->attributes();
-            //hacky...
+            // Hacky...
             $thumbnailinfo = $mediainfo->group->thumbnail[0]->attributes();
 
-            // Derive the nicest file name we can
+            // Derive the nicest file name we can.
             if (!empty($mediainfo->group->description)) {
                 $title = shorten_text((string)$mediainfo->group->description, 20, false, '');
                 $title = clean_filename($title).'.jpg';
@@ -547,7 +362,7 @@ class google_picasa {
                 'size' => (int) $gphoto->size,
                 'path' => $gphoto->albumid.'/'.$gphoto->id,
                 'thumbnail' => (string) $thumbnailinfo['url'],
-                'thumbnail_width' => 72,  // 72 is the native maximum dimension
+                'thumbnail_width' => 72,  // 72 is the native maximum dimension.
                 'thumbnail_height' => 72,
                 'source' => (string) $fullinfo['url'],
                 'url' => (string) $fullinfo['url']
@@ -556,54 +371,36 @@ class google_picasa {
 
         return $files;
     }
-
 }
 
 /**
- * Beginings of an implementation of Clientogin authenticaton for google
- * accounts as documented here:
- * {@link http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html#ClientLogin}
+ * OAuth 2.0 client for Google Services
  *
- * With this authentication we have to accept a username and password and to post
- * it to google. Retrieving a token for use afterwards.
- *
- * @package    moodlecore
- * @subpackage lib
- * @copyright Dan Poltawski <talktodan@gmail.com>
+ * @package   core
+ * @copyright 2012 Dan Poltawski
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class google_authclient extends google_auth_request {
-    const LOGIN_URL = 'https://www.google.com/accounts/ClientLogin';
-
-    public function __construct($sessiontoken = '', $username = '', $password = '', $options = array() ){
-        parent::__construct($options);
-
-        if($username and $password){
-            $param =  array(
-                'accountType'=>'GOOGLE',
-                'Email'=>$username,
-                'Passwd'=>$password,
-                'service'=>'writely'
-            );
-
-            $content = $this->post(google_authclient::LOGIN_URL, $param);
-
-            if( preg_match('/auth=(.*)/i', $content, $matches) ){
-                $sessiontoken = $matches[1];
-            }else{
-                throw new moodle_exception('could not upgrade authtoken');
-            }
-
-        }
+class google_oauth extends oauth2_client {
+    /**
+     * Returns the auth url for OAuth 2.0 request
+     * @return string the auth url
+     */
+    protected function auth_url() {
+        return 'https://accounts.google.com/o/oauth2/auth';
+    }
 
-        if($sessiontoken){
-            $this->token = $sessiontoken;
-        }else{
-            throw new moodle_exception('no session token specified');
-        }
+    /**
+     * Returns the token url for OAuth 2.0 request
+     * @return string the auth url
+     */
+    protected function token_url() {
+        return 'https://accounts.google.com/o/oauth2/token';
     }
 
-    public static function get_auth_header_name(){
-        return 'GoogleLogin auth=';
+    /**
+     * Clear any headers in the curl object
+     */
+    public function clear_headers() {
+        $this->header = array();
     }
 }
index e652a8c..7da6ed6 100644 (file)
@@ -455,6 +455,11 @@ function install_cli_database(array $options, $interactive) {
     set_config('release', $release);
     set_config('branch', $branch);
 
+    if (PHPUNIT_TEST) {
+        // mark as test database as soon as possible
+        set_config('phpunittest', 'na');
+    }
+
     // install all plugins types, local, etc.
     upgrade_noncore(true);
 
index 82bf053..1640f14 100644 (file)
@@ -115,7 +115,7 @@ class calc_formula {
      * @param string $formula
      * @return string localised formula
      */
-    function localize($formula) {
+    public static function localize($formula) {
         $formula = str_replace('.', '$', $formula); // temp placeholder
         $formula = str_replace(',', get_string('listsep', 'langconfig'), $formula);
         $formula = str_replace('$', get_string('decsep', 'langconfig'), $formula);
@@ -127,7 +127,7 @@ class calc_formula {
      * @param string $formula localised formula
      * @return string
      */
-    function unlocalize($formula) {
+    public static function unlocalize($formula) {
         $formula = str_replace(get_string('decsep', 'langconfig'), '$', $formula);
         $formula = str_replace(get_string('listsep', 'langconfig'), ',', $formula);
         $formula = str_replace('$', '.', $formula); // temp placeholder
index 8b0379b..23ee2f5 100644 (file)
@@ -9642,7 +9642,7 @@ function address_in_subnet($addr, $subnetstr) {
  */
 function mtrace($string, $eol="\n", $sleep=0) {
 
-    if (defined('STDOUT')) {
+    if (defined('STDOUT') and !PHPUNIT_TEST) {
         fwrite(STDOUT, $string.$eol);
     } else {
         echo $string . $eol;
index 36ee14e..b4aa15b 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/filelib.php');
+
 /**
  * OAuth helper class
  *
@@ -349,3 +353,259 @@ class oauth_helper {
         return md5($mt . $rand);
     }
 }
+
+/**
+ * OAuth 2.0 Client for using web access tokens.
+ *
+ * http://tools.ietf.org/html/draft-ietf-oauth-v2-22
+ *
+ * @package   core
+ * @copyright Dan Poltawski <talktodan@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class oauth2_client extends curl {
+    /** var string client identifier issued to the client */
+    private $clientid = '';
+    /** var string The client secret. */
+    private $clientsecret = '';
+    /** var string URL to return to after authenticating */
+    private $returnurl = '';
+    /** var string scope of the authentication request */
+    private $scope = '';
+    /** var stdClass access token object */
+    private $accesstoken = null;
+
+    /**
+     * Returns the auth url for OAuth 2.0 request
+     * @return string the auth url
+     */
+    abstract protected function auth_url();
+
+    /**
+     * Returns the token url for OAuth 2.0 request
+     * @return string the auth url
+     */
+    abstract protected function token_url();
+
+    /**
+     * Constructor.
+     *
+     * @param string $clientid
+     * @param string $clientsecret
+     * @param string $returnurl
+     * @param string $scope
+     */
+    public function __construct($clientid, $clientsecret, $returnurl, $scope) {
+        parent::__construct();
+        $this->clientid = $clientid;
+        $this->clientsecret = $clientsecret;
+        $this->returnurl = $returnurl;
+        $this->scope = $scope;
+        $this->accesstoken = $this->get_stored_token();
+    }
+
+    /**
+     * Is the user logged in? Note that if this is called
+     * after the first part of the authorisation flow the token
+     * is upgraded to an accesstoken.
+     *
+     * @return boolean true if logged in
+     */
+    public function is_logged_in() {
+        // Has the token expired?
+        if (isset($this->accesstoken->expires) && time() >= $this->accesstoken->expires) {
+            $this->log_out();
+            return false;
+        }
+
+        // We have a token so we are logged in.
+        if (isset($this->accesstoken->token)) {
+            return true;
+        }
+
+        // If we've been passed then authorization code generated by the
+        // authorization server try and upgrade the token to an access token.
+        $code = optional_param('code', null, PARAM_RAW);
+        if ($code && $this->upgrade_token($code)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Callback url where the request is returned to.
+     *
+     * @return moodle_url url of callback
+     */
+    public static function callback_url() {
+        global $CFG;
+
+        return new moodle_url('/admin/oauth2callback.php');
+    }
+
+    /**
+     * Returns the login link for this oauth request
+     *
+     * @return moodle_url login url
+     */
+    public function get_login_url() {
+
+        $callbackurl = self::callback_url();
+        $url = new moodle_url($this->auth_url(),
+                        array('client_id' => $this->clientid,
+                              'response_type' => 'code',
+                              'redirect_uri' => $callbackurl->out(false),
+                              'state' => $this->returnurl,
+                              'scope' => $this->scope,
+                          ));
+
+        return $url;
+    }
+
+    /**
+     * Upgrade a authorization token from oauth 2.0 to an access token
+     *
+     * @param string $code the code returned from the oauth authenticaiton
+     * @return boolean true if token is upgraded succesfully
+     */
+    public function upgrade_token($code) {
+        $callbackurl = self::callback_url();
+        $params = array('client_id' => $this->clientid,
+            'client_secret' => $this->clientsecret,
+            'grant_type' => 'authorization_code',
+            'code' => $code,
+            'redirect_uri' => $callbackurl->out(false),
+        );
+
+        // Requests can either use http GET or POST.
+        if ($this->use_http_get()) {
+            $response = $this->get($this->token_url(), $params);
+        } else {
+            $response = $this->post($this->token_url(), $params);
+        }
+
+        if (!$this->info['http_code'] === 200) {
+            throw new moodle_exception('Could not upgrade oauth token');
+        }
+
+        $r = json_decode($response);
+
+        if (!isset($r->access_token)) {
+            return false;
+        }
+
+        // Store the token an expiry time.
+        $accesstoken = new stdClass;
+        $accesstoken->token = $r->access_token;
+        $accesstoken->expires = (time() + ($r->expires_in - 10)); // Expires 10 seconds before actual expiry.
+        $this->store_token($accesstoken);
+
+        return true;
+    }
+
+    /**
+     * Logs out of a oauth request, clearing any stored tokens
+     */
+    public function log_out() {
+        $this->store_token(null);
+    }
+
+    /**
+     * Make a HTTP request, adding the access token we have
+     *
+     * @param string $url The URL to request
+     * @param array $options
+     * @return bool
+     */
+    protected function request($url, $options = array()) {
+        $murl = new moodle_url($url);
+
+        if ($this->accesstoken) {
+            if ($this->use_http_get()) {
+                // If using HTTP GET add as a parameter.
+                $murl->param('access_token', $this->accesstoken->token);
+            } else {
+                $this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
+            }
+        }
+
+        return parent::request($murl->out(false), $options);
+    }
+
+    /**
+     * Multiple HTTP Requests
+     * This function could run multi-requests in parallel.
+     *
+     * @param array $requests An array of files to request
+     * @param array $options An array of options to set
+     * @return array An array of results
+     */
+    protected function multi($requests, $options = array()) {
+        if ($this->accesstoken) {
+            $this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
+        }
+        return parent::multi($requests, $options);
+    }
+
+    /**
+     * Returns the tokenname for the access_token to be stored
+     * through multiple requests.
+     *
+     * The default implentation is to use the classname combiend
+     * with the scope.
+     *
+     * @return string tokenname for prefernce storage
+     */
+    protected function get_tokenname() {
+        // This is unusual but should work for most purposes.
+        return get_class($this).'-'.md5($this->scope);
+    }
+
+    /**
+     * Store a token between requests. Currently uses
+     * session named by get_tokenname
+     *
+     * @param stdClass|null $token token object to store or null to clear
+     */
+    protected function store_token($token) {
+        global $SESSION;
+
+        $this->accesstoken = $token;
+        $name = $this->get_tokenname();
+
+        if ($token !== null) {
+            $SESSION->{$name} = $token;
+        } else {
+            unset($SESSION->{$name});
+        }
+    }
+
+    /**
+     * Retrieve a token stored.
+     *
+     * @return stdClass|null token object
+     */
+    protected function get_stored_token() {
+        global $SESSION;
+
+        $name = $this->get_tokenname();
+
+        if (isset($SESSION->{$name})) {
+            return $SESSION->{$name};
+        }
+
+        return null;
+    }
+
+    /**
+     * Should HTTP GET be used instead of POST?
+     * Some APIs do not support POST and want oauth to use
+     * GET instead (with the auth_token passed as a GET param).
+     *
+     * @return bool true if GET should be used
+     */
+    protected function use_http_get() {
+        return false;
+    }
+}
index 514eac9..4bd0673 100644 (file)
@@ -321,6 +321,9 @@ class user_picture implements renderable {
         } else if ($this->size === true or $this->size == 1) {
             $filename = 'f1';
             $size = 100;
+        } else if ($this->size > 100) {
+            $filename = 'f3';
+            $size = (int)$this->size;
         } else if ($this->size >= 50) {
             $filename = 'f1';
             $size = (int)$this->size;
index 80f20c6..697ca6f 100644 (file)
@@ -2025,7 +2025,9 @@ class core_renderer extends renderer_base {
 
         $currentfile = $options->currentfile;
         if (empty($currentfile)) {
-            $currentfile = get_string('nofilesattached', 'repository');
+            $currentfile = '';
+        } else {
+            $currentfile .= ' - ';
         }
         if ($options->maxbytes) {
             $size = $options->maxbytes;
@@ -2056,7 +2058,7 @@ EOD;
             $html .= <<<EOD
     <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
     <div class="filepicker-filename">
-        <div class="filepicker-container">$currentfile<span class="dndupload-message"> - $strdndenabled <br/><span class="dndupload-arrow"></span></span></div>
+        <div class="filepicker-container">$currentfile<span class="dndupload-message">$strdndenabled <br/><span class="dndupload-arrow"></span></span></div>
     </div>
     <div><div class="dndupload-target">{$strdroptoupload}<br/><span class="dndupload-arrow"></span></div></div>
     </div>
index df5239c..20f8a34 100644 (file)
@@ -280,25 +280,35 @@ class plugin_manager {
     }
 
     /**
-     * Checks all dependencies for all installed plugins. Used by install and upgrade.
+     * Checks all dependencies for all installed plugins
+     *
+     * This is used by install and upgrade. The array passed by reference as the second
+     * argument is populated with the list of plugins that have failed dependencies (note that
+     * a single plugin can appear multiple times in the $failedplugins).
+     *
      * @param int $moodleversion the version from version.php.
+     * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
      * @return bool true if all the dependencies are satisfied for all plugins.
      */
-    public function all_plugins_ok($moodleversion) {
+    public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
+
+        $return = true;
         foreach ($this->get_plugins() as $type => $plugins) {
             foreach ($plugins as $plugin) {
 
-                if (!empty($plugin->versionrequires) && $plugin->versionrequires > $moodleversion) {
-                    return false;
+                if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
+                    $return = false;
+                    $failedplugins[] = $plugin->component;
                 }
 
                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
-                    return false;
+                    $return = false;
+                    $failedplugins[] = $plugin->component;
                 }
             }
         }
 
-        return true;
+        return $return;
     }
 
     /**
@@ -1610,6 +1620,22 @@ abstract class plugininfo_base {
         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
     }
 
+    /**
+     * Returns true if the the given Moodle version is enough to run this plugin
+     *
+     * @param string|int|double $moodleversion
+     * @return bool
+     */
+    public function is_core_dependency_satisfied($moodleversion) {
+
+        if (empty($this->versionrequires)) {
+            return true;
+
+        } else {
+            return (double)$this->versionrequires <= (double)$moodleversion;
+        }
+    }
+
     /**
      * Returns the status of the plugin
      *
index 81727f5..3baf0b5 100644 (file)
@@ -809,7 +809,7 @@ class portfolio_exporter {
      */
     public function get_tempfiles($skipfile='portfolio-export.zip') {
         $fs = get_file_storage();
-        $files = $fs->get_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id, '', false);
+        $files = $fs->get_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id, 'sortorder, itemid, filepath, filename', false);
         if (empty($files)) {
             return array();
         }
index ba408f7..7b7c669 100644 (file)
@@ -107,7 +107,7 @@ final class portfolio_export_form extends moodleform {
      * @param stdClass $data portfolio information from form data
      * @return array
      */
-    public function validation($data) {
+    public function validation($data, $files) {
 
         $errors = array();
 
@@ -228,7 +228,7 @@ final class portfolio_admin_form extends moodleform {
      * @param stdObject $data form data
      * @return array
      */
-    public function validation($data) {
+    public function validation($data, $files) {
         global $DB;
 
         $errors = array();
@@ -296,7 +296,7 @@ final class portfolio_user_form extends moodleform {
      *
      * @param stdClass $data form data
      */
-    public function validation($data) {
+    public function validation($data, $files) {
 
         $errors = $this->instance->user_config_validation($data);
 
index 1a635f3..23cf9e1 100644 (file)
@@ -478,8 +478,21 @@ setup_DB();
 if (PHPUNIT_TEST and !PHPUNIT_UTIL) {
     // make sure tests do not run in parallel
     phpunit_util::acquire_test_lock();
-    // reset DB tables
-    phpunit_util::reset_database();
+    $dbhash = null;
+    try {
+        if ($dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest'))) {
+            // reset DB tables
+            phpunit_util::reset_database();
+        }
+    } catch (Exception $e) {
+        if ($dbhash) {
+            // we ned to reinit if reset fails
+            $DB->set_field('config', 'value', 'na', array('name'=>'phpunittest'));
+        } else {
+            throw $e;
+        }
+    }
+    unset($dbhash);
 }
 
 // Disable errors for now - needed for installation when debug enabled in config.php
index f35c916..2315916 100644 (file)
@@ -70,7 +70,8 @@ class conditionlib_testcase extends advanced_testcase {
         $this->assertEquals(
             (object)array('id'=>$id,'showavailability'=>1,
                 'availablefrom'=>17,'availableuntil'=>398,'course'=>64,
-                'conditionsgrade'=>array(), 'conditionscompletion'=>array()),
+                'conditionsgrade'=>array(), 'conditionscompletion'=>array(),
+                'visible' => 1),
             $test->get_full_course_module());
 
         // just the course_modules stuff; check it doesn't request that from db
@@ -78,11 +79,13 @@ class conditionlib_testcase extends advanced_testcase {
         $cm->availablefrom=2;
         $cm->availableuntil=74;
         $cm->course=38;
+        $cm->visible = 1;
         $test=new condition_info($cm,CONDITION_MISSING_EXTRATABLE);
         $this->assertEquals(
             (object)array('id'=>$id,'showavailability'=>0,
                 'availablefrom'=>2,'availableuntil'=>74,'course'=>38,
-                'conditionsgrade'=>array(), 'conditionscompletion'=>array()),
+                'conditionsgrade' => array(), 'conditionscompletion' => array(),
+                'visible' => 1),
             $test->get_full_course_module());
 
         // Now let's add some actual grade/completion conditions
@@ -136,7 +139,8 @@ class conditionlib_testcase extends advanced_testcase {
         $this->assertEquals(
                 (object)array('id' => $id, 'showavailability' => 1, 'groupingid' => 13,
                     'availablefrom' => 17, 'availableuntil' => 398, 'course' => 64,
-                    'conditionsgrade' => array(), 'conditionscompletion' => array()),
+                    'conditionsgrade' => array(), 'conditionscompletion' => array(),
+                    'visible' => 1),
                 $test->get_full_section());
 
         // Just the course_sections stuff; check it doesn't request that from db
@@ -146,11 +150,13 @@ class conditionlib_testcase extends advanced_testcase {
         $section->availableuntil = 74;
         $section->course = 38;
         $section->groupingid = 99;
+        $section->visible = 1;
         $test = new condition_info_section($section, CONDITION_MISSING_EXTRATABLE);
         $this->assertEquals(
                 (object)array('id' => $id, 'showavailability' => 0, 'groupingid' => 99,
                     'availablefrom' => 2, 'availableuntil' => 74, 'course' => 38,
-                    'conditionsgrade' => array(), 'conditionscompletion' => array()),
+                    'conditionsgrade' => array(), 'conditionscompletion' => array(),
+                    'visible' => 1),
                 $test->get_full_section());
 
         // Now let's add some actual grade/completion conditions
index 6e6e029..aa6ee8f 100644 (file)
@@ -1764,3 +1764,78 @@ function admin_mnet_method_profile(Zend_Server_Reflection_Function_Abstract $fun
     }
     return $profile;
 }
+
+
+/**
+ * This function finds duplicate records (based on combinations of fields that should be unique)
+ * and then progamatically generated a "most correct" version of the data, update and removing
+ * records as appropriate
+ *
+ * Thanks to Dan Marsden for help
+ *
+ * @param   string  $table      Table name
+ * @param   array   $uniques    Array of field names that should be unique
+ * @param   array   $fieldstocheck  Array of fields to generate "correct" data from (optional)
+ * @return  void
+ */
+function upgrade_course_completion_remove_duplicates($table, $uniques, $fieldstocheck = array()) {
+    global $DB;
+
+    // Find duplicates
+    $sql_cols = implode(', ', $uniques);
+
+    $sql = "SELECT {$sql_cols} FROM {{$table}} GROUP BY {$sql_cols} HAVING (count(id) > 1)";
+    $duplicates = $DB->get_recordset_sql($sql, array());
+
+    // Loop through duplicates
+    foreach ($duplicates as $duplicate) {
+        $pointer = 0;
+
+        // Generate SQL for finding records with these duplicate uniques
+        $sql_select = implode(' = ? AND ', $uniques).' = ?'; // builds "fieldname = ? AND fieldname = ?"
+        $uniq_values = array();
+        foreach ($uniques as $u) {
+            $uniq_values[] = $duplicate->$u;
+        }
+
+        $sql_order = implode(' DESC, ', $uniques).' DESC'; // builds "fieldname DESC, fieldname DESC"
+
+        // Get records with these duplicate uniques
+        $records = $DB->get_records_select(
+            $table,
+            $sql_select,
+            $uniq_values,
+            $sql_order
+        );
+
+        // Loop through and build a "correct" record, deleting the others
+        $needsupdate = false;
+        $origrecord = null;
+        foreach ($records as $record) {
+            $pointer++;
+            if ($pointer === 1) { // keep 1st record but delete all others.
+                $origrecord = $record;
+            } else {
+                // If we have fields to check, update original record
+                if ($fieldstocheck) {
+                    // we need to keep the "oldest" of all these fields as the valid completion record.
+                    // but we want to ignore null values
+                    foreach ($fieldstocheck as $f) {
+                        if ($record->$f && (($origrecord->$f > $record->$f) || !$origrecord->$f)) {
+                            $origrecord->$f = $record->$f;
+                            $needsupdate = true;
+                        }
+                    }
+                }
+                $DB->delete_records($table, array('id' => $record->id));
+            }
+        }
+        if ($needsupdate || isset($origrecord->reaggregate)) {
+            // If this table has a reaggregate field, update to force recheck on next cron run
+            if (isset($origrecord->reaggregate)) {
+                $origrecord->reaggregate = time();
+            }
+            $DB->update_record($table, $origrecord);
+        }
+    }
+}
index 35fc12e..8555f32 100644 (file)
@@ -167,6 +167,13 @@ YUI.add('moodle-core-blocks', function(Y) {
             this.dragsourceregion = null;
         },
 
+        drag_dropmiss : function(e) {
+            // Missed the target, but we assume the user intended to drop it
+            // on the last last ghost node location, e.drag and e.drop should be
+            // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
+            this.drop_hit(e);
+        },
+
         drop_hit : function(e) {
             var drag = e.drag;
             // Get a reference to our drag node
index 930f600..27d0f1d 100644 (file)
@@ -15,6 +15,7 @@ YUI.add('moodle-core-dragdrop', function(Y) {
         samenodeclass : null,
         parentnodeclass : null,
         groups : [],
+        lastdroptarget : null,
         initializer : function(params) {
             // Listen for all drag:start events
             Y.DD.DDM.on('drag:start', this.global_drag_start, this);
@@ -26,6 +27,8 @@ YUI.add('moodle-core-dragdrop', function(Y) {
             Y.DD.DDM.on('drop:over', this.global_drop_over, this);
             // Listen for all drop:hit events
             Y.DD.DDM.on('drop:hit', this.global_drop_hit, this);
+            // Listen for all drop:miss events
+            Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this);
         },
 
         get_drag_handle: function(title, classname, iconclass) {
@@ -34,8 +37,7 @@ YUI.add('moodle-core-dragdrop', function(Y) {
                 .setAttrs({
                     'src' : M.util.image_url(MOVEICON.pix, MOVEICON.component),
                     'alt' : title,
-                    'title' : M.str.moodle.move,
-                    'hspace' : '3'
+                    'title' : M.str.moodle.move
                 });
             if (iconclass) {
                 dragicon.addClass(iconclass);
@@ -66,14 +68,23 @@ YUI.add('moodle-core-dragdrop', function(Y) {
             return new M.core.exception(e);
         },
 
+        in_group: function(target) {
+            var ret = false;
+            Y.each(this.groups, function(v, k) {
+                if (target._groups[v]) {
+                    ret = true;
+                }
+            }, this);
+            return ret;
+        },
         /*
          * Drag-dropping related functions
          */
         global_drag_start : function(e) {
             // Get our drag object
             var drag = e.target;
-            // Check that drop object belong to correct group
-            if (!drag.target.inGroup(this.groups)) {
+            // Check that drag object belongs to correct group
+            if (!this.in_group(drag)) {
                 return;
             }
             // Set some general styles here
@@ -89,8 +100,8 @@ YUI.add('moodle-core-dragdrop', function(Y) {
 
         global_drag_end : function(e) {
             var drag = e.target;
-            // Check that drop object belong to correct group
-            if (!drag.target.inGroup(this.groups)) {
+            // Check that drag object belongs to correct group
+            if (!this.in_group(drag)) {
                 return;
             }
             //Put our general styles back
@@ -103,8 +114,8 @@ YUI.add('moodle-core-dragdrop', function(Y) {
 
         global_drag_drag : function(e) {
             var drag = e.target;
-            // Check that drop object belong to correct group
-            if (!drag.target.inGroup(this.groups)) {
+            // Check that drag object belongs to correct group
+            if (!this.in_group(drag)) {
                 return;
             }
             //Get the last y point
@@ -130,6 +141,8 @@ YUI.add('moodle-core-dragdrop', function(Y) {
             //Get a reference to our drag and drop nodes
             var drag = e.drag.get('node');
             var drop = e.drop.get('node');
+            // Save last drop target for the case of missed target processing
+            this.lastdroptarget = e.drop;
             //Are we dropping on the same node?
             if (drop.hasClass(this.samenodeclass)) {
                 //Are we not going up?
@@ -149,6 +162,19 @@ YUI.add('moodle-core-dragdrop', function(Y) {
             this.drop_over(e);
         },
 
+        global_drag_dropmiss : function(e) {
+            // drag:dropmiss does not have e.drag and e.drop properties
+            // we substitute them for the ease of use. For e.drop we use,
+            // this.lastdroptarget (ghost node we use for indicating where to drop)
+            e.drag = e.target;
+            // Check that drop object belong to correct group
+            if (!e.drag.target.inGroup(this.groups)) {
+                return;
+            }
+            e.drop = this.lastdroptarget;
+            this.drag_dropmiss(e);
+        },
+
         global_drop_hit : function(e) {
             // Check that drop object belong to correct group
             if (!e.drop.inGroup(this.groups)) {
@@ -163,6 +189,7 @@ YUI.add('moodle-core-dragdrop', function(Y) {
         drag_start : function(e) {},
         drag_end : function(e) {},
         drag_drag : function(e) {},
+        drag_dropmiss : function(e) {},
         drop_over : function(e) {},
         drop_hit : function(e) {}
     }, {
index 69b6ec4..dcb31e9 100644 (file)
@@ -344,7 +344,7 @@ abstract class assign_plugin {
      * @param bool $showviewlink Modifed to return whether or not to show a link to the full submission/feedback
      * @return string - return a string representation of the submission in full
      */
-    public function view_summary(stdClass $submissionorgrade, $showviewlink) {
+    public function view_summary(stdClass $submissionorgrade, $showviewlink) {
         return '';
     }
 
index 7f97175..dadd2a8 100644 (file)
@@ -105,7 +105,7 @@ class assign_feedback_comments extends assign_feedback_plugin {
      * @param bool $showviewlink Set to true to show a link to view the full feedback
      * @return string
      */
-    public function view_summary(stdClass $grade, $showviewlink) {
+    public function view_summary(stdClass $grade, $showviewlink) {
         $feedbackcomments = $this->get_feedback_comments($grade->id);
         if ($feedbackcomments) {
             $text = format_text($feedbackcomments->commenttext, $feedbackcomments->commentformat, array('context' => $this->assignment->get_context()));
index 20c4be7..9989cf3 100644 (file)
@@ -146,7 +146,7 @@ class assign_feedback_file extends assign_feedback_plugin {
      * @param bool $showviewlink - Set to true to show a link to see the full list of files
      * @return string
      */
-    public function view_summary(stdClass $grade, $showviewlink) {
+    public function view_summary(stdClass $grade, $showviewlink) {
         $count = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA);
         // show a view all link if the number of files is over this limit
         $showviewlink = $count > ASSIGNFEEDBACK_FILE_MAXSUMMARYFILES;
index ff37319..2211bb8 100644 (file)
@@ -118,10 +118,10 @@ class assign {
     private $returnparams = array();
 
     /** @var string modulename prevents excessive calls to get_string */
-    private static $modulename = '';
+    private static $modulename = null;
 
     /** @var string modulenameplural prevents excessive calls to get_string */
-    private static $modulenameplural = '';
+    private static $modulenameplural = null;
 
     /**
      * Constructor for the base assign class
@@ -2368,8 +2368,8 @@ class assign {
         global $CFG;
         $posthtml  = '<p><font face="sans-serif">'.
                      '<a href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.format_string($course->shortname, true, array('context' => $context->get_course_context())).'</a> ->'.
-                     '<a href="'.$CFG->wwwroot.'/mod/assignment/index.php?id='.$course->id.'">'.$modulename.'</a> ->'.
-                     '<a href="'.$CFG->wwwroot.'/mod/assignment/view.php?id='.$coursemodule->id.'">'.format_string($assignmentname, true, array('context' => $context)).'</a></font></p>';
+                     '<a href="'.$CFG->wwwroot.'/mod/assign/index.php?id='.$course->id.'">'.$modulename.'</a> ->'.
+                     '<a href="'.$CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id.'">'.format_string($assignmentname, true, array('context' => $context)).'</a></font></p>';
         $posthtml .= '<hr /><font face="sans-serif">';
         $posthtml .= '<p>'.get_string($messagetype . 'html', 'assign', $info).'</p>';
         $posthtml .= '</font><hr />';
index 7860b64..187bc9a 100644 (file)
@@ -36,6 +36,9 @@ $ADMIN->add('assignmentplugins', new admin_category('assignfeedbackplugins',
 $ADMIN->add('assignfeedbackplugins', new assign_admin_page_manage_assign_plugins('assignfeedback'));
 
 
+assign_plugin_manager::add_admin_assign_plugin_settings('assignsubmission', $ADMIN, $settings, $module);
+assign_plugin_manager::add_admin_assign_plugin_settings('assignfeedback', $ADMIN, $settings, $module);
+
 if ($ADMIN->fulltree) {
     $menu = array();
     foreach (get_plugin_list('assignfeedback') as $type => $notused) {
@@ -54,4 +57,4 @@ if ($ADMIN->fulltree) {
                    new lang_string('configshowrecentsubmissions', 'assign'), 0));
     $settings->add(new admin_setting_configcheckbox('assign/submissionreceipts',
                    get_string('sendsubmissionreceipts', 'mod_assign'), get_string('sendsubmissionreceipts_help', 'mod_assign'), 1));
-}
\ No newline at end of file
+}
index 43ee80d..eb87938 100644 (file)
@@ -53,7 +53,7 @@ class assign_submission_comments extends assign_submission_plugin {
     * @param bool $showviewlink - If the comments are long this is set to true so they can be shown in a separate page
     * @return string
     */
-   public function view_summary(stdClass $submission, $showviewlink) {
+   public function view_summary(stdClass $submission, $showviewlink) {
 
         // never show a link to view full submission
         $showviewlink = false;
index b097c39..3f95f34 100644 (file)
@@ -244,7 +244,7 @@ class assign_submission_file extends assign_submission_plugin {
      * @param bool $showviewlink Set this to true if the list of files is long
      * @return string
      */
-    public function view_summary(stdClass $submission, $showviewlink) {
+    public function view_summary(stdClass $submission, $showviewlink) {
         $count = $this->count_files($submission->id, ASSIGNSUBMISSION_FILE_FILEAREA);
 
         // show we show a link to view all files for this plugin?
index dcaf0c5..0ab3777 100644 (file)
@@ -192,7 +192,7 @@ class assign_submission_onlinetext extends assign_submission_plugin {
       * @param bool $showviewlink - If the summary has been truncated set this to true
       * @return string
       */
-    public function view_summary(stdClass $submission, $showviewlink) {
+    public function view_summary(stdClass $submission, $showviewlink) {
 
         $onlinetextsubmission = $this->get_onlinetext_submission($submission->id);
         // always show the view link
index f737bda..5d29b71 100644 (file)
@@ -38,74 +38,39 @@ $string['pluginname'] = 'Book';
 $string['pluginadministration'] = 'Book administration';
 
 $string['toc'] = 'Table of contents';
-$string['faq'] = 'Book FAQ';
-$string['faq_help'] = '
-*Why only two levels?*
-
-Two levels are generally enough for all books, three levels would lead to poorly structured documents. Book module is designed for
-creation of short multipage study materials. It is usually better to use PDF format for longer documents. The easiest way to create PDFs are
-virtual printers (see
-<a  href="http://sector7g.wurzel6.de/pdfcreator/index_en.htm"  target="_blank">PDFCreator</a>,
-<a  href="http://fineprint.com/products/pdffactory/index.html"  target="_blank">PDFFactory</a>,
-<a  href="http://www.adobe.com/products/acrobatstd/main.html"  target="_blank">Adobe Acrobat</a>,
-etc.).
-
-*Can students edit books?*
-
-Only teachers can create and edit books. There are no plans to implement student editing for books, but somebody may create something
-similar for students (Portfolio?). The main reason is to keep Book module as simple as possible.
-
-*How do I search the books?*
-
-At present there is only one way, use browser\'s search capability in print page. Global searching is now possible only in Moodle forums.
-It would be nice to have global searching for all resources including books, any volunteers?
-
-*My titles do not fit on one line.*
-
-Either rephrase your titles or ask your site admin to change TOC
-width. It is defined globally for all books in module configuration
-page.';
-
 $string['customtitles'] = 'Custom titles';
-$string['customtitles_help'] = 'Chapter titles are displayed automatically only in TOC.';
+$string['customtitles_help'] = 'Normally the chapter title is displayed in the table of contents (TOC) AND as a heading above the content.
 
+If the custom titles checkbox is ticked, the chapter title is NOT displayed as a heading above the content. A different title (perhaps longer than the chapter title) may be entered as part of the content.';
 $string['chapters'] = 'Chapters';
 $string['editingchapter'] = 'Editing chapter';
 $string['chaptertitle'] = 'Chapter title';
 $string['content'] = 'Content';
 $string['subchapter'] = 'Subchapter';
-
-$string['numbering'] = 'Chapter numbering';
-$string['numbering_help'] = '* None - chapter and subchapter titles are not formatted at all, use if you want to define special numbering styles. For example letters: in chapter title type "A First Chapter", "A.1 Some Subchapter",...
-* Numbers - chapters and subchapters are numbered (1, 1.1, 1.2, 2, ...)
-* Bullets - subchapters are indented and displayed with bullets
-* Indented - subchapters are indented';
-
+$string['numbering'] = 'Chapter formatting';
+$string['numbering_help'] = '* None - Chapter and subchapter titles have no formatting
+* Numbers - Chapters and subchapter titles are numbered 1, 1.1, 1.2, 2, ...
+* Bullets - Subchapters are indented and displayed with bullets in the table of contents
+* Indented - Subchapters are indented in the table of contents';
 $string['numbering0'] = 'None';
 $string['numbering1'] = 'Numbers';
 $string['numbering2'] = 'Bullets';
 $string['numbering3'] = 'Indented';
-$string['numberingoptions'] = 'Available numbering options';
-$string['numberingoptions_help'] = 'Select numbering options that should be available when creating new books.';
-
+$string['numberingoptions'] = 'Available options for chapter formatting';
+$string['numberingoptions_desc'] = 'Options for displaying chapters and subchapters in the table of contents';
 $string['chapterscount'] = 'Chapters';
-
 $string['addafter'] = 'Add new chapter';
 $string['confchapterdelete'] = 'Do you really want to delete this chapter?';
 $string['confchapterdeleteall'] = 'Do you really want to delete this chapter and all its subchapters?';
-
 $string['top'] = 'top';
-
 $string['navprev'] = 'Previous';
 $string['navnext'] = 'Next';
 $string['navexit'] = 'Exit book';
-
 $string['book:addinstance'] = 'Add a new book';
 $string['book:read'] = 'Read book';
 $string['book:edit'] = 'Edit book chapters';
 $string['book:viewhiddenchapters'] = 'View hidden book chapters';
-
-$string['errorchapter'] = 'Error reading book chapter.';
+$string['errorchapter'] = 'Error reading chapter of book.';
 
 $string['page-mod-book-x'] = 'Any book module page';
 
index 96f9f45..238913d 100644 (file)
@@ -194,12 +194,6 @@ function book_add_fake_block($chapters, $chapter, $book, $cm, $edit) {
 
     $toc = book_get_toc($chapters, $chapter, $book, $cm, $edit, 0);
 
-    if ($edit) {
-        $toc .= '<div class="book_faq">';
-        $toc .=  $OUTPUT->help_icon('faq', 'mod_book', get_string('faq', 'mod_book'));
-        $toc .=  '</div>';
-    }
-
     $bc = new block_contents();
     $bc->title = get_string('toc', 'mod_book');
     $bc->attributes['class'] = 'block';
index d58cce3..b795875 100644 (file)
@@ -45,7 +45,7 @@ class mod_book_mod_form extends moodleform_mod {
             $mform->setType('name', PARAM_CLEANHTML);
         }
         $mform->addRule('name', null, 'required', null, 'client');
-        $this->add_intro_editor($config->requiremodintro, get_string('summary'));
+        $this->add_intro_editor($config->requiremodintro, get_string('moduleintro'));
 
         $alloptions = book_get_numbering_types();
         $allowed = explode(',', $config->numberingoptions);
index 810ad3c..38f87bb 100644 (file)
@@ -35,7 +35,7 @@ if ($ADMIN->fulltree) {
     $options = book_get_numbering_types();
 
     $settings->add(new admin_setting_configmultiselect('book/numberingoptions',
-        get_string('numberingoptions', 'mod_book'), get_string('numberingoptions_help', 'mod_book'),
+        get_string('numberingoptions', 'mod_book'), get_string('numberingoptions_desc', 'mod_book'),
         array_keys($options), $options));
 
 
@@ -46,4 +46,4 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configselect('book/numbering',
         get_string('numbering', 'mod_book'), '', BOOK_NUM_NUMBERS, $options));
 
-}
\ No newline at end of file
+}
index d09189d..6ddaf09 100644 (file)
 
 /* == Fake toc block == */
 
-.mod_book .book_faq {
-  font-size: 0.7em;
-}
-
 /* toc style NONE */
 .mod_book .book_toc_none {
   font-size: 0.8em;
index 2273e55..5b6a505 100644 (file)
@@ -26,5 +26,5 @@ defined('MOODLE_INTERNAL') || die;
 
 $string['exportimscp:export'] = 'Export book as IMS content package';
 $string['generateimscp'] = 'Generate IMS CP';
-$string['nochapters'] = 'No book chapters found, can not export to IMS CP.';
+$string['nochapters'] = 'No book chapters found, so unable to export to IMS CP.';
 $string['pluginname'] = 'Book IMS CP export';
index c564009..bbd811d 100644 (file)
 defined('MOODLE_INTERNAL') || die;
 
 $string['doimport'] = 'Import';
-$string['errornochapters'] = 'Can not find chapters in selected file';
-$string['import'] = 'Import from HTML';
+$string['errornochapters'] = 'Cannot find chapters in selected file';
+$string['import'] = 'Import chapter';
 $string['importhtml:import'] = 'Import chapters';
 $string['importing'] = 'Importing';
 $string['importingchapters'] = 'Importing chapters into book';
-$string['pluginname'] = 'Book HTML import';
+$string['pluginname'] = 'Book chapter import';
 $string['relinking'] = 'Relinking';
 $string['type'] = 'Type';
 $string['typeonefile'] = 'One HTML file with headings as chapters';
 $string['typezipfiles'] = 'Each HTML file represents one chapter';
-$string['typezipdirs'] = 'Each directory represents one chapter';
-$string['ziparchive'] = 'Zip archive';
-$string['ziparchive_help'] = 'Select a ZIP archive that contains HTML files and other media. File or directory names ending with "_sub" indicate subchapters. You can use copy and paste in text editor for simple HML files without embedded media.';
\ No newline at end of file
+$string['typezipdirs'] = 'Each folder represents one chapter';
+$string['ziparchive'] = 'Zip file';
+$string['ziparchive_help'] = 'Select a zip file containing HTML files and optional multimedia files and folders. To upload subchapters, add "_sub" to the end of HTML file or folder names.';
index fe07130..fbce66e 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die;
 
 $module->component = 'mod_book'; // Full name of the plugin (used for diagnostics)
-$module->version   = 2012052100; // The current module version (Date: YYYYMMDDXX)
+$module->version   = 2012052700; // The current module version (Date: YYYYMMDDXX)
 $module->requires  = 2012051900; // Requires this Moodle version
 $module->cron      = 0;          // Period for cron to check this module (secs)
index 9185685..0b74e47 100644 (file)
@@ -48,7 +48,7 @@ $PAGE->set_activity_record($folder);
 
 $data = new stdClass();
 $data->id = $cm->id;
-$options = array('mainfile'=>true, 'subdirs'=>1, 'maxbytes'=>$CFG->maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*');
+$options = array('subdirs'=>1, 'maxbytes'=>$CFG->maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*');
 file_prepare_standard_filemanager($data, 'files', $options, $context, 'mod_folder', 'content', 0);
 
 $mform = new mod_folder_edit_form(null, array('data'=>$data, 'options'=>$options));
index f1444af..86bf31b 100644 (file)
@@ -31,7 +31,7 @@
  * @return string the full path to the cached RSS feed directory. Null if there is a problem.
  */
 function forum_rss_get_feed($context, $args) {
-    global $CFG, $DB;
+    global $CFG, $DB, $USER;
 
     $status = true;
 
@@ -43,7 +43,7 @@ function forum_rss_get_feed($context, $args) {
 
     $forumid  = clean_param($args[3], PARAM_INT);
     $cm = get_coursemodule_from_instance('forum', $forumid, 0, false, MUST_EXIST);
-    $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
+    $modcontext = context_module::instance($cm->id);
 
     //context id from db should match the submitted one
     if ($context->id != $modcontext->id || !has_capability('mod/forum:viewdiscussion', $modcontext)) {
@@ -58,8 +58,14 @@ function forum_rss_get_feed($context, $args) {
     //the sql that will retreive the data for the feed and be hashed to get the cache filename
     $sql = forum_rss_get_sql($forum, $cm);
 
-    //hash the sql to get the cache file name
-    $filename = rss_get_file_name($forum, $sql);
+    // Hash the sql to get the cache file name.
+    // If the forum is Q and A then we need to cache the files per user. This can
+    // have a large impact on performance, so we want to only do it on this type of forum.
+    if ($forum->type == 'qanda') {
+        $filename = rss_get_file_name($forum, $sql . $USER->id);
+    } else {
+        $filename = rss_get_file_name($forum, $sql);
+    }
     $cachedfilepath = rss_get_file_full_name('mod_forum', $filename);
 
     //Is the cache out of date?
@@ -151,7 +157,7 @@ function forum_rss_feed_discussions_sql($forum, $cm, $newsince=0) {
     $now = round(time(), -2);
     $params = array($cm->instance);
 
-    $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
+    $modcontext = context_module::instance($cm->id);
 
     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
@@ -205,7 +211,7 @@ function forum_rss_feed_discussions_sql($forum, $cm, $newsince=0) {
  * @return string the SQL query to be used to get the Post details from the forum table of the database
  */
 function forum_rss_feed_posts_sql($forum, $cm, $newsince=0) {
-    $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
+    $modcontext = context_module::instance($cm->id);
 
     //get group enforcement SQL
     $groupmode    = groups_get_activity_groupmode($cm);
@@ -290,8 +296,10 @@ function forum_rss_get_group_sql($cm, $groupmode, $currentgroup, $modcontext=nul
  *
  * @Todo MDL-31129 implement post attachment handling
  */
-function forum_rss_feed_contents($forum, $sql, $context) {
-    global $CFG, $DB;
+
+function forum_rss_feed_contents($forum, $sql) {
+    global $CFG, $DB, $USER;
+
 
     $status = true;
 
@@ -305,23 +313,45 @@ function forum_rss_feed_contents($forum, $sql, $context) {
         $isdiscussion = false;
     }
 
+    if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
+        print_error('invalidcoursemodule');
+    }
+    $context = context_module::instance($cm->id);
+
     $formatoptions = new stdClass();
     $items = array();
     foreach ($recs as $rec) {
             $item = new stdClass();
             $user = new stdClass();
-            if ($isdiscussion && !empty($rec->discussionname)) {
-                $item->title = format_string($rec->discussionname);
-            } else if (!empty($rec->postsubject)) {
-                $item->title = format_string($rec->postsubject);
+
+            if ($isdiscussion && !forum_user_can_see_discussion($forum, $rec->discussionid, $context)) {
+                // This is a discussion which the user has no permission to view
+                $item->title = get_string('forumsubjecthidden', 'forum');
+                $message = get_string('forumbodyhidden', 'forum');
+                $item->author = get_string('forumauthorhidden', 'forum');
+            } else if (!$isdiscussion && !forum_user_can_see_post($forum, $rec->discussionid, $rec->postid, $USER, $cm)) {
+                // This is a post which the user has no permission to view
+                $item->title = get_string('forumsubjecthidden', 'forum');
+                $message = get_string('forumbodyhidden', 'forum');
+                $item->author = get_string('forumauthorhidden', 'forum');
             } else {
-                //we should have an item title by now but if we dont somehow then substitute something somewhat meaningful
-                $item->title = format_string($forum->name.' '.userdate($rec->postcreated,get_string('strftimedatetimeshort', 'langconfig')));
+                // The user must have permission to view
+                if ($isdiscussion && !empty($rec->discussionname)) {
+                    $item->title = format_string($rec->discussionname);
+                } else if (!empty($rec->postsubject)) {
+                    $item->title = format_string($rec->postsubject);
+                } else {
+                    //we should have an item title by now but if we dont somehow then substitute something somewhat meaningful
+                    $item->title = format_string($forum->name.' '.userdate($rec->postcreated,get_string('strftimedatetimeshort', 'langconfig')));
+                }
+                $user->firstname = $rec->userfirstname;
+                $user->lastname = $rec->userlastname;
+                $item->author = fullname($user);
+                $message = file_rewrite_pluginfile_urls($rec->postmessage, 'pluginfile.php', $context->id,
+                        'mod_forum', 'post', $rec->postid);
+                $formatoptions->trusted = $rec->posttrust;
             }
-            $user->firstname = $rec->userfirstname;
-            $user->lastname = $rec->userlastname;
-            $item->author = fullname($user);
-            $item->pubdate = $rec->postcreated;
+
             if ($isdiscussion) {
                 $item->link = $CFG->wwwroot."/mod/forum/discuss.php?d=".$rec->discussionid;
             } else {
@@ -329,8 +359,6 @@ function forum_rss_feed_contents($forum, $sql, $context) {
             }
 
             $formatoptions->trusted = $rec->posttrust;
-            $message = file_rewrite_pluginfile_urls($rec->postmessage, 'pluginfile.php', $context->id,
-                'mod_forum', 'post', $rec->postid);
             $item->description = format_text($message, $rec->postformat, $formatoptions, $forum->course);
 
             //TODO: MDL-31129 implement post attachment handling
@@ -342,6 +370,7 @@ function forum_rss_feed_contents($forum, $sql, $context) {
                     $item->attachments = array();
                 }
             }*/
+            $item->pubdate = $rec->postcreated;
 
             $items[] = $item;
         }
index 9066c8e..58a5fd5 100644 (file)
@@ -62,7 +62,7 @@ if ($user) {
     if (!has_capability('mod/forum:managesubscriptions', $context)) {
         print_error('nopermissiontosubscribe', 'forum');
     }
-    $user = $DB->get_record('user', array('id' => $user), MUST_EXIST);
+    $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST);
 } else {
     $user = $USER;
 }
index fa45821..c1bec7f 100644 (file)
@@ -212,10 +212,13 @@ M.mod_quiz.secure_window = {
             window.location = 'about:blank';
         }
         Y.delegate('contextmenu', M.mod_quiz.secure_window.prevent, document, '*');
-        Y.delegate('mousedown', M.mod_quiz.secure_window.prevent_mouse, document, '*');
-        Y.delegate('mouseup', M.mod_quiz.secure_window.prevent_mouse, document, '*');
-        Y.delegate('dragstart', M.mod_quiz.secure_window.prevent, document, '*');
+        Y.delegate('mousedown',   M.mod_quiz.secure_window.prevent_mouse, document, '*');
+        Y.delegate('mouseup',     M.mod_quiz.secure_window.prevent_mouse, document, '*');
+        Y.delegate('dragstart',   M.mod_quiz.secure_window.prevent, document, '*');
         Y.delegate('selectstart', M.mod_quiz.secure_window.prevent, document, '*');
+        Y.delegate('cut',         M.mod_quiz.secure_window.prevent, document, '*');
+        Y.delegate('copy',        M.mod_quiz.secure_window.prevent, document, '*');
+        Y.delegate('paste',       M.mod_quiz.secure_window.prevent, document, '*');
         M.mod_quiz.secure_window.clear_status;
         Y.on('beforeprint', function() {
             Y.one(document.body).setStyle('display', 'none');
index 179c507..d5f2081 100644 (file)
@@ -500,7 +500,7 @@ function mod_resource_dndupload_handle($uploadinfo) {
     $data = new stdClass();
     $data->course = $uploadinfo->course->id;
     $data->name = $uploadinfo->displayname;
-    $data->intro = '<p>'.$uploadinfo->displayname.'</p>';
+    $data->intro = '';
     $data->introformat = FORMAT_HTML;
     $data->coursemodule = $uploadinfo->coursemodule;
     $data->files = $uploadinfo->draftitemid;
@@ -512,6 +512,8 @@ function mod_resource_dndupload_handle($uploadinfo) {
     $data->popupwidth = $config->popupwidth;
     $data->printheading = $config->printheading;
     $data->printintro = $config->printintro;
+    $data->showsize = $config->showsize;
+    $data->showtype = $config->showtype;
 
     return resource_add_instance($data, null);
 }
index c926c12..43369fe 100644 (file)
@@ -121,7 +121,7 @@ function scorm_parse_aicc($scorm) {
 
     $fs = get_file_storage();
 
-    $files = $fs->get_area_files($context->id, 'mod_scorm', 'content', 0, '', false);
+    $files = $fs->get_area_files($context->id, 'mod_scorm', 'content', 0, 'sortorder, itemid, filepath, filename', false);
 
     $version = 'AICC';
     $ids = array();
diff --git a/pix/u/f3.png b/pix/u/f3.png
new file mode 100644 (file)
index 0000000..27e3516
Binary files /dev/null and b/pix/u/f3.png differ
index b3c3982..b5a1c4a 100644 (file)
@@ -221,7 +221,8 @@ if (!$exporter->get('instance')) {
         throw new portfolio_export_exception($exporter, 'noavailableplugins', 'portfolio');
     } else if (count($options) == 1) {
         // no point displaying a form, just redirect.
-        $instance = array_shift(array_keys($options));
+        $optionskeys = array_keys($options);
+        $instance = array_shift($optionskeys);
         redirect($CFG->wwwroot . '/portfolio/add.php?id= ' . $exporter->get('id') . '&instance=' . $instance . '&sesskey=' . sesskey());
     }
     // be very selective about not including this unless we really need to
diff --git a/portfolio/googledocs/db/upgrade.php b/portfolio/googledocs/db/upgrade.php
new file mode 100644 (file)
index 0000000..dfdc1dc
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @param int $oldversion the version we are upgrading from
+ * @return bool result
+ */
+function xmldb_portfolio_googledocs_upgrade($oldversion) {
+    global $CFG, $DB;
+
+    $dbman = $DB->get_manager();
+
+    if ($oldversion < 2012053000) {
+        // Delete old user preferences containing authsub tokens.
+        $DB->delete_records('user_preferences', array('name' => 'google_authsub_sesskey'));
+        upgrade_plugin_savepoint(true, 2012053000, 'portfolio', 'googledocs');
+    }
+
+    if ($oldversion < 2012053001) {
+        $existing = $DB->get_record('portfolio_instance', array('plugin' => 'googledocs'), '*', IGNORE_MULTIPLE);
+
+        if ($existing) {
+            portfolio_googledocs_admin_upgrade_notification();
+        }
+
+        upgrade_plugin_savepoint(true, 2012053001, 'portfolio', 'googledocs');
+    }
+
+    return true;
+}
+
+function portfolio_googledocs_admin_upgrade_notification() {
+    $admins = get_admins();
+
+    if (empty($admins)) {
+        return;
+    }
+    $mainadmin = reset($admins);
+
+    foreach ($admins as $admin) {
+        $message = new stdClass();
+        $message->component         = 'moodle';
+        $message->name              = 'notices';
+        $message->userfrom          = $mainadmin;
+        $message->userto            = $admin;
+        $message->smallmessage      = get_string('oauth2upgrade_message_small', 'portfolio_googledocs');
+        $message->subject           = get_string('oauth2upgrade_message_subject', 'portfolio_googledocs');
+        $message->fullmessage       = get_string('oauth2upgrade_message_content', 'portfolio_googledocs');
+        $message->fullmessagehtml   = get_string('oauth2upgrade_message_content', 'portfolio_googledocs');
+        $message->fullmessageformat = FORMAT_PLAIN;
+        $message->notification      = 1;
+        message_send($message);
+    }
+}
index f1c9262..c52bba1 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['clientid'] = 'Client ID';
 $string['noauthtoken'] = 'An authentication token has not been recieved from google. Please ensure you are allowing moodle to access your google account';
+$string['nooauthcredentials'] = 'OAuth credentials required.';
+$string['nooauthcredentials_help'] = 'To use the Google Docs portfolio plugin you must configure OAuth credentials in the portfolio settings.';
 $string['nosessiontoken'] = 'A session token does not exist preventing export to google.';
+$string['oauthinfo'] = '<p>To use the google docs portfolio you must be registered with Google. Instructions for registing your installation with Google are described in <a href="{$a->docsurl}">Moodle Docs</a>. The redirect url should be set to:</p><p>{$a->callbackurl}</p>';
+$string['oauth2upgrade_message_subject'] = 'Important information regarding Google Docs portfolio plugin';
+$string['oauth2upgrade_message_content'] = 'As part of the upgrade to Moodle 2.3, the Google Docs portfolio plugin has been disabled due to changes in Googles API. To re-enable your plugin, you must configure oauth credentials in this plugin.';
+$string['oauth2upgrade_message_small'] = 'The Google Docs portfolio plugin has been disabled until configured with OAuth2';
 $string['pluginname'] = 'Google Docs';
 $string['sendfailed'] = 'The file {$a} failed to transfer to google';
+$string['secret'] = 'Secret';
index 2223f9d..9820246 100644 (file)
@@ -1,4 +1,19 @@
 <?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
 /**
  * Google Documents Portfolio Plugin
  *
@@ -9,18 +24,10 @@ require_once($CFG->libdir.'/portfolio/plugin.php');
 require_once($CFG->libdir.'/googleapi.php');
 
 class portfolio_plugin_googledocs extends portfolio_plugin_push_base {
-    private $sessiontoken;
+    private $googleoauth = null;
 
     public function supported_formats() {
-        return array(
-            PORTFOLIO_FORMAT_PLAINHTML,
-            PORTFOLIO_FORMAT_IMAGE,
-            PORTFOLIO_FORMAT_TEXT,
-            PORTFOLIO_FORMAT_PDF,
-            PORTFOLIO_FORMAT_DOCUMENT,
-            PORTFOLIO_FORMAT_PRESENTATION,
-            PORTFOLIO_FORMAT_SPREADSHEET
-        );
+        return array(PORTFOLIO_FORMAT_FILE);
     }
 
     public static function get_name() {
@@ -28,29 +35,27 @@ class portfolio_plugin_googledocs extends portfolio_plugin_push_base {
     }
 
     public function prepare_package() {
-        // we send the files as they are, no prep required
+        // We send the files as they are, no prep required.
         return true;
     }
 
-    public function get_interactive_continue_url(){
+    public function get_interactive_continue_url() {
         return 'http://docs.google.com/';
     }
 
     public function expected_time($callertime) {
-        // we trust what the portfolio says
+        // We trust what the portfolio says.
         return $callertime;
     }
 
     public function send_package() {
-
-        if(!$this->sessiontoken){
-            throw new portfolio_plugin_exception('nosessiontoken', 'portfolio_googledocs');
+        if (!$this->googleoauth) {
+            throw new portfolio_plugin_exception('noauthtoken', 'portfolio_googledocs');
         }
 
-        $gdocs = new google_docs(new google_authsub($this->sessiontoken));
-
+        $gdocs = new google_docs($this->googleoauth);
         foreach ($this->exporter->get_tempfiles() as $file) {
-            if(!$gdocs->send_file($file)){
+            if (!$gdocs->send_file($file)) {
                 throw new portfolio_plugin_exception('sendfailed', 'portfolio_gdocs', $file->get_filename());
             }
         }
@@ -62,20 +67,12 @@ class portfolio_plugin_googledocs extends portfolio_plugin_push_base {
             return false;
         }
 
-        $sesskey = google_docs::get_sesskey($this->get('user')->id);
-
-        if($sesskey){
-            try{
-                $gauth = new google_authsub($sesskey);
-                $this->sessiontoken = $sesskey;
-                return false;
-            }catch(Exception $e){
-                // sesskey is not valid, delete store and re-auth
-                google_docs::delete_sesskey($this->get('user')->id);
-            }
+        $this->initialize_oauth();
+        if ($this->googleoauth->is_logged_in()) {
+            return false;
+        } else {
+            return $this->googleoauth->get_login_url();
         }
-
-        return google_authsub::login_url($CFG->wwwroot.'/portfolio/add.php?postcontrol=1&id=' . $this->exporter->get('id') . '&sesskey=' . sesskey(), google_docs::REALM);
     }
 
     public function post_control($stage, $params) {
@@ -83,43 +80,62 @@ class portfolio_plugin_googledocs extends portfolio_plugin_push_base {
             return;
         }
 
-        if(!array_key_exists('token', $params)){
-            throw new portfolio_plugin_exception('noauthtoken', 'portfolio_googledocs');
+        $this->initialize_oauth();
+        if ($this->googleoauth->is_logged_in()) {
+            return false;
+        } else {
+            return $this->googleoauth->get_login_url();
         }
-
-        // we now have our auth token, get a session token..
-        $gauth = new google_authsub(false, $params['token']);
-        $this->sessiontoken = $gauth->get_sessiontoken();
-
-        google_docs::set_sesskey($this->sessiontoken, $this->get('user')->id);
     }
 
     public static function allows_multiple_instances() {
         return false;
     }
-}
 
-/**
- * Registers to the user_deleted event to revoke any
- * subauth tokens we have from them
- *
- * @param $user user object
- * @return boolean true in all cases as its only minor cleanup
- */
-function portfolio_googledocs_user_deleted($user){
-    // it is only by luck that the user prefstill exists now?
-    // We probably need a pre-delete event?
-    if($sesskey = google_docs::get_sesskey($user->id)){
-        try{
-            $gauth = new google_authsub($sesskey);
-
-            $gauth->revoke_session_token();
-        }catch(Exception $e){
-            // we don't care that much about success- just being good
-            // google api citzens
-            return true;
-        }
+    public static function has_admin_config() {
+        return true;
+    }
+
+    public static function get_allowed_config() {
+        return array('clientid', 'secret');
+    }
+
+    public function admin_config_form(&$mform) {
+        $a = new stdClass;
+        $a->docsurl = get_docs_url('Google_OAuth2_Setup');
+        $a->callbackurl = google_oauth::callback_url()->out(false);
+
+        $mform->addElement('static', null, '', get_string('oauthinfo', 'portfolio_googledocs', $a));
+
+        $mform->addElement('text', 'clientid', get_string('clientid', 'portfolio_googledocs'));
+        $mform->addElement('text', 'secret', get_string('secret', 'portfolio_googledocs'));
+
+        $strrequired = get_string('required');
+        $mform->addRule('clientid', $strrequired, 'required', null, 'client');
+        $mform->addRule('secret', $strrequired, 'required', null, 'client');
+    }
+
+    private function initialize_oauth() {
+        $returnurl = new moodle_url('/portfolio/add.php');
+        $returnurl->param('postcontrol', 1);
+        $returnurl->param('id', $this->exporter->get('id'));
+        $returnurl->param('sesskey', sesskey());
+
+        $clientid = $this->get_config('clientid');
+        $secret = $this->get_config('secret');
+
+        $this->googleoauth = new google_oauth($clientid, $secret, $returnurl->out(false), google_docs::REALM);
     }
 
-    return true;
+    public function instance_sanity_check() {
+        $clientid = $this->get_config('clientid');
+        $secret = $this->get_config('secret');
+
+        // If there is no oauth config (e.g. plugins upgraded from < 2.3 then
+        // there will be no config and this plugin should be disabled.
+        if (empty($clientid) or empty($secret)) {
+            return 'nooauthcredentials';
+        }
+        return 0;
+    }
 }
index 2044666..5cee5db 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2011112900;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2011112900;        // Requires this Moodle version
-$plugin->component = 'portfolio_googledocs'; // Full name of the plugin (used for diagnostics)
+$plugin->version   = 2012053001;        // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2012051100;        // Requires this Moodle version.
+$plugin->component = 'portfolio_googledocs'; // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 0;
diff --git a/portfolio/picasa/db/events.php b/portfolio/picasa/db/events.php
deleted file mode 100644 (file)
index 09ef7cc..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Add event handlers for the picasa portfolio.
- *
- * @package    portfolio_picasa
- * @category   event
- * @copyright  2009 Penny Leach
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-$handlers = array (
-    'user_deleted' => array (
-         'handlerfile'      => '/portfolio/picasa/lib.php',
-         'handlerfunction'  => 'portfolio_picasa_user_deleted',
-         'schedule'         => 'cron',
-         'internal'         => 0,
-     ),
-);
\ No newline at end of file
diff --git a/portfolio/picasa/db/upgrade.php b/portfolio/picasa/db/upgrade.php
new file mode 100644 (file)
index 0000000..e5d1cca
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @param int $oldversion the version we are upgrading from
+ * @return bool result
+ */
+function xmldb_portfolio_picasa_upgrade($oldversion) {
+    global $CFG, $DB;
+
+    $dbman = $DB->get_manager();
+
+    if ($oldversion < 2012053000) {
+        // Delete old user preferences containing authsub tokens.
+        $DB->delete_records('user_preferences', array('name' => 'google_authsub_sesskey_picasa'));
+        upgrade_plugin_savepoint(true, 2012053000, 'portfolio', 'picasa');
+    }
+
+    if ($oldversion < 2012053001) {
+        $existing = $DB->get_record('portfolio_instance', array('plugin' => 'picasa'), '*', IGNORE_MISSING);
+
+        if ($existing) {
+            portfolio_picasa_admin_upgrade_notification();
+        }
+
+        upgrade_plugin_savepoint(true, 2012053001, 'portfolio', 'picasa');
+    }
+
+    return true;
+}
+
+function portfolio_picasa_admin_upgrade_notification() {
+    $admins = get_admins();
+
+    if (empty($admins)) {
+        return;
+    }
+    $mainadmin = reset($admins);
+
+    foreach ($admins as $admin) {
+        $message = new stdClass();
+        $message->component         = 'moodle';
+        $message->name              = 'notices';
+        $message->userfrom          = $mainadmin;
+        $message->userto            = $admin;
+        $message->smallmessage      = get_string('oauth2upgrade_message_small', 'portfolio_picasa');
+        $message->subject           = get_string('oauth2upgrade_message_subject', 'portfolio_picasa');
+        $message->fullmessage       = get_string('oauth2upgrade_message_content', 'portfolio_picasa');
+        $message->fullmessagehtml   = get_string('oauth2upgrade_message_content', 'portfolio_picasa');
+        $message->fullmessageformat = FORMAT_PLAIN;
+        $message->notification      = 1;
+        message_send($message);
+    }
+}