Merge branch 'MDL-37953_master' of https://github.com/markn86/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 12 Feb 2013 09:12:01 +0000 (10:12 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 12 Feb 2013 09:12:01 +0000 (10:12 +0100)
134 files changed:
admin/tool/behat/cli/util.php
admin/tool/behat/tests/behat/basic_actions.feature [new file with mode: 0644]
admin/tool/behat/tests/behat/manipulate_forms.feature [new file with mode: 0644]
admin/tool/uploaduser/index.php
admin/tool/xmldb/actions/XMLDBAction.class.php
admin/tool/xmldb/actions/view_structure_php/view_structure_php.class.php
admin/tool/xmldb/actions/view_table_php/view_table_php.class.php
auth/db/auth.php
auth/email/auth.php
auth/ldap/auth.php
auth/manual/auth.php
auth/none/auth.php
auth/webservice/auth.php
backup/converter/moodle1/lib.php
backup/converter/moodle1/tests/lib_test.php
backup/util/dbops/restore_dbops.class.php
backup/util/ui/base_moodleform.class.php
backup/util/xml/parser/processors/grouped_parser_processor.class.php
backup/util/xml/parser/tests/fixtures/test6.xml [new file with mode: 0644]
backup/util/xml/parser/tests/parser_test.php
blocks/community/communitycourse.php
blocks/community/forms.php
blocks/community/lang/en/block_community.php
blocks/community/styles.css
blocks/dock.js
blocks/glossary_random/block_glossary_random.php
blocks/glossary_random/edit_form.php
blocks/glossary_random/version.php
config-dist.php
course/dndupload.js
course/dnduploadlib.php
course/format/formatlegacy.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/tests/behat/add_activities.feature
course/tests/behat/behat_course.php
enrol/manual/locallib.php
grade/edit/outcome/index.php
grade/report/grader/ajax_callbacks.php
grade/report/grader/lib.php
grade/tests/reportgrader_test.php [new file with mode: 0644]
help.php
lang/en/block.php
lang/en/form.php
lang/en/moodle.php
lib/accesslib.php
lib/behat/behat_base.php
lib/behat/classes/behat_config_manager.php
lib/cronlib.php
lib/datalib.php
lib/db/install.xml
lib/db/upgrade.php
lib/filelib.php
lib/form/form.js
lib/form/listing.php [new file with mode: 0644]
lib/form/yui/listing/listing.js [new file with mode: 0644]
lib/form/yui/shortforms/shortforms.js [new file with mode: 0644]
lib/form/yui/showadvanced/showadvanced.js [new file with mode: 0644]
lib/formslib.php
lib/grade/tests/fixtures/lib.php
lib/installlib.php
lib/javascript-static.js
lib/moodlelib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/password_compat/lib/password.php [new file with mode: 0644]
lib/password_compat/readme_moodle.txt [new file with mode: 0644]
lib/password_compat/tests/PasswordGetInfoTest.php [new file with mode: 0644]
lib/password_compat/tests/PasswordHashTest.php [new file with mode: 0644]
lib/password_compat/tests/PasswordNeedsRehashTest.php [new file with mode: 0644]
lib/password_compat/tests/PasswordVerifyTest.php [new file with mode: 0644]
lib/phpunit/bootstrap.php
lib/setuplib.php
lib/testing/classes/util.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/yui/popuphelp/popuphelp.js [new file with mode: 0644]
lib/yui/tooltip/tooltip.js [new file with mode: 0644]
message/index.php
message/lib.php
message/tests/externallib_test.php
mod/assign/feedback/offline/importgradesform.php
mod/assign/feedback/offline/locallib.php
mod/assign/gradingtable.php
mod/assign/module.js
mod/assign/submission/file/locallib.php
mod/assignment/type/upload/assignment.class.php
mod/assignment/type/uploadsingle/assignment.class.php
mod/choice/mod_form.php
mod/folder/backup/moodle1/lib.php
mod/folder/mod_form.php
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/db/access.php
mod/forum/db/install.xml [changed mode: 0644->0755]
mod/forum/db/upgrade.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/styles.css
mod/forum/upgrade.txt [new file with mode: 0644]
mod/forum/version.php
mod/label/lang/en/label.php
mod/label/lib.php
mod/label/settings.php [new file with mode: 0644]
mod/quiz/settings.php
mod/resource/mod_form.php
mod/workshop/mod_form.php
mod/workshop/settings.php
phpunit.xml.dist
question/engine/questionattempt.php
question/type/essay/question.php
report/security/lang/en/report_security.php
report/security/locallib.php
repository/filepicker.js
tag/index.php
theme/anomaly/style/general.css
theme/base/style/core.css
theme/canvas/style/core.css
theme/formal_white/style/formal_white.css
theme/splash/style/core.css
theme/standard/style/modules.css
user/filters/user_filter_forms.php
version.php
webservice/lib.php

index 4fd147f..cfd3581 100644 (file)
@@ -150,6 +150,7 @@ require_once($CFG->libdir.'/upgradelib.php');
 require_once($CFG->libdir.'/clilib.php');
 require_once($CFG->libdir.'/pluginlib.php');
 require_once($CFG->libdir.'/installlib.php');
+require_once($CFG->libdir.'/testing/classes/test_lock.php');
 
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
@@ -165,6 +166,8 @@ if ($options['install']) {
     behat_util::install_site();
     mtrace("Acceptance tests site installed");
 } else if ($options['drop']) {
+    // Ensure no tests are running.
+    test_lock::acquire('behat');
     behat_util::drop_site();
     mtrace("Acceptance tests site dropped");
 } else if ($options['enable']) {
diff --git a/admin/tool/behat/tests/behat/basic_actions.feature b/admin/tool/behat/tests/behat/basic_actions.feature
new file mode 100644 (file)
index 0000000..8adc991
--- /dev/null
@@ -0,0 +1,29 @@
+@tool_behat
+Feature: Page contents assertions
+  In order to write good tests
+  As a tests writer
+  I need to check the page contents
+
+  @javascript
+  Scenario: Basic contents assertions
+    Given I log in as "admin"
+    And I am on homepage
+    And I expand "Users" node
+    And I follow "Groups"
+    And I press "Create group"
+    And I fill the moodle form with:
+      | Group name | I'm the name |
+      | Group description | I'm the description |
+    And I press "Save changes"
+    When I follow "Overview"
+    And I wait until the page is ready
+    And I wait "2" seconds
+    And I hover ".region-content .generaltable td span"
+    Then I should see "I'm the description"
+    And I should see "Filter groups by"
+    And I should not see "Filter groupssss by"
+    And I should see "Group members" in the ".region-content table th.c1" element
+    And I should not see "Group membersssss" in the ".region-content table th.c1" element
+    And I follow "Groups"
+    And the element "#groupeditform #showcreateorphangroupform" should be enabled
+    And the element "#groupeditform #showeditgroupsettingsform" should be disabled
diff --git a/admin/tool/behat/tests/behat/manipulate_forms.feature b/admin/tool/behat/tests/behat/manipulate_forms.feature
new file mode 100644 (file)
index 0000000..9db0740
--- /dev/null
@@ -0,0 +1,20 @@
+@tool_behat @core_form
+Feature: Forms manipulation
+  In order to interact with Moodle
+  As a user
+  I need to set forms values
+
+  @javascript
+  Scenario: Basic forms manipulation
+    Given I log in as "admin"
+    And I follow "Admin User"
+    And I follow "Edit profile"
+    When I fill in "First name" with "Field value"
+    And I select "Use standard web forms" from "When editing text"
+    And I check "Unmask"
+    Then the "First name" field should match "Field value" value
+    And the "When editing text" select box should contain "Use standard web forms"
+    And the "Unmask" checkbox should be checked
+    And I uncheck "Unmask"
+    And the "Unmask" checkbox should not be checked
+    And I press "Update profile"
index f249f1d..88aaeda 100644 (file)
@@ -535,6 +535,7 @@ if ($formdata = $mform2->is_cancelled()) {
                     }
                     if (!property_exists($user, $column) or !property_exists($existinguser, $column)) {
                         // this should never happen
+                        debugging("Could not find $column on the user objects", DEBUG_DEVELOPER);
                         continue;
                     }
                     if ($updatetype == UU_UPDATE_MISSING) {
@@ -618,7 +619,7 @@ if ($formdata = $mform2->is_cancelled()) {
                 // Do not mess with passwords of remote users.
 
             } else if (!$isinternalauth) {
-                $existinguser->password = 'not cached';
+                $existinguser->password = AUTH_PASSWORD_NOT_CACHED;
                 $upt->track('password', '-', 'normal', false);
                 // clean up prefs
                 unset_user_preference('create_password', $existinguser);
@@ -626,6 +627,8 @@ if ($formdata = $mform2->is_cancelled()) {
 
             } else if (!empty($user->password)) {
                 if ($updatepasswords) {
+                    // Check for passwords that we want to force users to reset next
+                    // time they log in.
                     $errmsg = null;
                     $weak = !check_password_policy($user->password, $errmsg);
                     if ($resetpasswords == UU_PWRESET_ALL or ($resetpasswords == UU_PWRESET_WEAK and $weak)) {
@@ -638,7 +641,12 @@ if ($formdata = $mform2->is_cancelled()) {
                         unset_user_preference('auth_forcepasswordchange', $existinguser);
                     }
                     unset_user_preference('create_password', $existinguser); // no need to create password any more
-                    $existinguser->password = hash_internal_user_password($user->password);
+
+                    // Use a low cost factor when generating bcrypt hash otherwise
+                    // hashing would be slow when uploading lots of users. Hashes
+                    // will be automatically updated to a higher cost factor the first
+                    // time the user logs in.
+                    $existinguser->password = hash_internal_user_password($user->password, true);
                     $upt->track('password', $user->password, 'normal', false);
                 } else {
                     // do not print password when not changed
@@ -771,10 +779,14 @@ if ($formdata = $mform2->is_cancelled()) {
                         }
                         $forcechangepassword = true;
                     }
-                    $user->password = hash_internal_user_password($user->password);
+                    // Use a low cost factor when generating bcrypt hash otherwise
+                    // hashing would be slow when uploading lots of users. Hashes
+                    // will be automatically updated to a higher cost factor the first
+                    // time the user logs in.
+                    $user->password = hash_internal_user_password($user->password, true);
                 }
             } else {
-                $user->password = 'not cached';
+                $user->password = AUTH_PASSWORD_NOT_CACHED;
                 $upt->track('password', '-', 'normal', false);
             }
 
index 3c7ca7a..d93fba0 100644 (file)
@@ -239,22 +239,22 @@ class XMLDBAction {
         switch ($plugintype ) {
             case 'lib': // has own savepoint function
                 $result = XMLDB_LINEFEED .
-                          '        // Main savepoint reached' . XMLDB_LINEFEED .
+                          '        // Main savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_main_savepoint(true, XXXXXXXXXX);' . XMLDB_LINEFEED;
                 break;
             case 'mod': // has own savepoint function
                 $result = XMLDB_LINEFEED .
-                          '        // ' . $pluginname . ' savepoint reached' . XMLDB_LINEFEED .
+                          '        // ' . ucfirst($pluginname) . ' savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_mod_savepoint(true, XXXXXXXXXX, ' . "'$pluginname'" . ');' . XMLDB_LINEFEED;
                 break;
             case 'block': // has own savepoint function
                 $result = XMLDB_LINEFEED .
-                          '        // ' . $pluginname . ' savepoint reached' . XMLDB_LINEFEED .
+                          '        // ' . ucfirst($pluginname) . ' savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_block_savepoint(true, XXXXXXXXXX, ' . "'$pluginname'" . ');' . XMLDB_LINEFEED;
                 break;
             default: // rest of plugins
                 $result = XMLDB_LINEFEED .
-                          '        // ' . $pluginname . ' savepoint reached' . XMLDB_LINEFEED .
+                          '        // ' . ucfirst($pluginname) . ' savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_plugin_savepoint(true, XXXXXXXXXX, ' . "'$plugintype'" . ', ' . "'$pluginname'" . ');' . XMLDB_LINEFEED;
         }
         return $result;
index 0ddf559..99d4524 100644 (file)
@@ -170,10 +170,10 @@ class view_structure_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define table ' . $table->getName() . ' to be created' . XMLDB_LINEFEED;
+        $result .= '        // Define table ' . $table->getName() . ' to be created.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Adding fields to table ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Adding fields to table ' . $table->getName() . '.' . XMLDB_LINEFEED;
         // Iterate over each field
         foreach ($table->getFields() as $field) {
             // The field header, with name
@@ -186,7 +186,7 @@ class view_structure_php extends XMLDBAction {
         // Iterate over each key
         if ($keys = $table->getKeys()) {
             $result .= XMLDB_LINEFEED;
-            $result .= '        // Adding keys to table ' . $table->getName() . XMLDB_LINEFEED;
+            $result .= '        // Adding keys to table ' . $table->getName() . '.' . XMLDB_LINEFEED;
             foreach ($keys as $key) {
                 // The key header, with name
                 $result .= '        $table->add_key(' . "'" . $key->getName() . "', ";
@@ -199,7 +199,7 @@ class view_structure_php extends XMLDBAction {
         // Iterate over each index
         if ($indexes = $table->getIndexes()) {
             $result .= XMLDB_LINEFEED;
-            $result .= '        // Adding indexes to table ' . $table->getName() . XMLDB_LINEFEED;
+            $result .= '        // Adding indexes to table ' . $table->getName() . '.' . XMLDB_LINEFEED;
             foreach ($indexes as $index) {
                 // The index header, with name
                 $result .= '        $table->add_index(' . "'" . $index->getName() . "', ";
@@ -212,7 +212,7 @@ class view_structure_php extends XMLDBAction {
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch create table for ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch create table for ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if (!$dbman->table_exists($table)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->create_table($table);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -250,12 +250,12 @@ class view_structure_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define table ' . $table->getName() . ' to be dropped' . XMLDB_LINEFEED;
+        $result .= '        // Define table ' . $table->getName() . ' to be dropped.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch drop table for ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch drop table for ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if ($dbman->table_exists($table)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->drop_table($table);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -293,12 +293,12 @@ class view_structure_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define table ' . $table->getName() . ' to be renamed to NEWNAMEGOESHERE' . XMLDB_LINEFEED;
+        $result .= '        // Define table ' . $table->getName() . ' to be renamed to NEWNAMEGOESHERE.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename table for ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename table for ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_table($table, ' . "'NEWNAMEGOESHERE'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
index 235056f..e975201 100644 (file)
@@ -314,13 +314,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define field ' . $field->getName() . ' to be added to ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define field ' . $field->getName() . ' to be added to ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch add field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch add field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if (!$dbman->field_exists($table, $field)) {'. XMLDB_LINEFEED;
         $result .= '            $dbman->add_field($table, $field);' . XMLDB_LINEFEED;
         $result .= '        }'. XMLDB_LINEFEED;
@@ -362,13 +362,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define field ' . $field->getName() . ' to be dropped from ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define field ' . $field->getName() . ' to be dropped from ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "'" . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch drop field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch drop field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if ($dbman->field_exists($table, $field)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->drop_field($table, $field);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -410,13 +410,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Rename field ' . $field->getName() . ' on table ' . $table->getName() . ' to NEWNAMEGOESHERE'. XMLDB_LINEFEED;
+        $result .= '        // Rename field ' . $field->getName() . ' on table ' . $table->getName() . ' to NEWNAMEGOESHERE.'. XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_field($table, $field, ' . "'" . 'NEWNAMEGOESHERE' . "'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -465,13 +465,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing type of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $type . XMLDB_LINEFEED;
+        $result .= '        // Changing type of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $type . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of type for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of type for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_type($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -517,13 +517,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing precision of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $precision . XMLDB_LINEFEED;
+        $result .= '        // Changing precision of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $precision . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
-        $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " .$field->getPHP(true) . ');' . XMLDB_LINEFEED;
+        $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of precision for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of precision for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_precision($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -565,13 +565,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing nullability of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $notnull . XMLDB_LINEFEED;
+        $result .= '        // Changing nullability of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $notnull . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of nullability for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of nullability for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_notnull($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -613,13 +613,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing the default of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $default . XMLDB_LINEFEED;
+        $result .= '        // Changing the default of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $default . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of default for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of default for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_default($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -659,13 +659,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be added to ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be added to ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $key = new xmldb_key(' . "'" . $key->getName() . "', " . $key->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch add key ' . $key->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch add key ' . $key->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->add_key($table, $key);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -705,13 +705,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be dropped form ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be dropped form ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $key = new xmldb_key(' . "'" . $key->getName() . "', " . $key->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch drop key ' . $key->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch drop key ' . $key->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->drop_key($table, $key);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -754,13 +754,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be renamed to NEWNAMEGOESHERE' . XMLDB_LINEFEED;
+        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be renamed to NEWNAMEGOESHERE.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $key = new xmldb_key(' . "'" . $key->getName() . "', " . $key->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename key ' . $key->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename key ' . $key->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_key($table, $key, ' . "'" . 'NEWNAMEGOESHERE' . "'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -800,13 +800,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be added to ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be added to ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $index = new xmldb_index(' . "'" . $index->getName() . "', " . $index->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch add index ' . $index->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch add index ' . $index->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if (!$dbman->index_exists($table, $index)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->add_index($table, $index);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -848,13 +848,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be dropped form ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be dropped form ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $index = new xmldb_index(' . "'" . $index->getName() . "', " . $index->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch drop index ' . $index->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch drop index ' . $index->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if ($dbman->index_exists($table, $index)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->drop_index($table, $index);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -899,13 +899,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be renamed to NEWNAMEGOESHERE' . XMLDB_LINEFEED;
+        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be renamed to NEWNAMEGOESHERE.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $index = new xmldb_index(' . "'" . $index->getName() . "', " . $index->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename index ' . $index->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename index ' . $index->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_index($table, $index, ' . "'" . 'NEWNAMEGOESHERE' . "'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
index 57c46e4..f245d60 100644 (file)
@@ -221,6 +221,9 @@ class auth_plugin_db extends auth_plugin_base {
 
         if ($this->is_internal()) {
             $puser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
+            // This will also update the stored hash to the latest algorithm
+            // if the existing hash is using an out-of-date algorithm (or the
+            // legacy md5 algorithm).
             if (update_internal_user_password($puser, $newpassword)) {
                 $user->password = $puser->password;
                 return true;
index e50c09e..777fe9f 100644 (file)
@@ -59,6 +59,9 @@ class auth_plugin_email extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index 6cd7fd7..9bdd5b3 100644 (file)
@@ -529,6 +529,9 @@ class auth_plugin_ldap extends auth_plugin_base {
         profile_save_data($user);
 
         $this->update_user_record($user->username);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         update_internal_user_password($user, $plainslashedpassword);
 
         $user = $DB->get_record('user', array('id'=>$user->id));
index 29cb59a..0c521e8 100644 (file)
@@ -82,6 +82,9 @@ class auth_plugin_manual extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index f0771d8..00eaf04 100644 (file)
@@ -59,6 +59,9 @@ class auth_plugin_none extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index 15a7598..59e1603 100644 (file)
@@ -85,6 +85,9 @@ class auth_plugin_webservice extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index d91e2f2..8f7ec30 100644 (file)
@@ -1279,6 +1279,12 @@ class moodle1_file_manager implements loggable {
      */
     public function migrate_directory($rootpath, $relpath='/') {
 
+        // Check the trailing slash in the $rootpath
+        if (substr($rootpath, -1) === '/') {
+            debugging('moodle1_file_manager::migrate_directory() expects $rootpath without the trailing slash', DEBUG_DEVELOPER);
+            $rootpath = substr($rootpath, 0, strlen($rootpath) - 1);
+        }
+
         if (!file_exists($this->basepath.'/'.$rootpath.$relpath)) {
             return array();
         }
index 154b6c8..8fb50b6 100644 (file)
@@ -266,13 +266,6 @@ class moodle1_converter_testcase extends advanced_testcase {
         $fileids = $fileman->get_fileids();
         $this->assertEquals(gettype($fileids), 'array');
         $this->assertEquals(0, count($fileids));
-        // try to migrate a non-existing directory
-        $returned = $fileman->migrate_directory('not/existing/directory');
-        $this->assertEquals(gettype($returned), 'array');
-        $this->assertEquals(0, count($returned));
-        $fileids = $fileman->get_fileids();
-        $this->assertEquals(gettype($fileids), 'array');
-        $this->assertEquals(0, count($fileids));
         // try to migrate an invalid file
         $fileman->itemid = 1;
         $thrown = false;
@@ -314,6 +307,72 @@ class moodle1_converter_testcase extends advanced_testcase {
         $converter->drop_stash_storage();
     }
 
+    public function test_migrate_directory() {
+        $this->resetAfterTest(true);
+
+        // Set-up the file manager.
+        $converter = convert_factory::get_converter('moodle1', $this->tempdir);
+        $converter->create_stash_storage();
+        $contextid = $converter->get_contextid(CONTEXT_MODULE, 32);
+        $fileman   = $converter->get_file_manager($contextid, 'mod_unittest', 'testarea');
+        // This fileman has not converted anything yet.
+        $fileids = $fileman->get_fileids();
+        $this->assertEquals(gettype($fileids), 'array');
+        $this->assertEquals(0, count($fileids));
+        // Try to migrate a non-existing directory.
+        $returned = $fileman->migrate_directory('not/existing/directory');
+        $this->assertEquals(gettype($returned), 'array');
+        $this->assertEquals(0, count($returned));
+        $fileids = $fileman->get_fileids();
+        $this->assertEquals(gettype($fileids), 'array');
+        $this->assertEquals(0, count($fileids));
+        // Try to migrate whole course_files.
+        $returned = $fileman->migrate_directory('course_files');
+        $this->assertEquals(gettype($returned), 'array');
+        $this->assertEquals(4, count($returned)); // Two files, two directories.
+        $fileids = $fileman->get_fileids();
+        $this->assertEquals(gettype($fileids), 'array');
+        $this->assertEquals(4, count($fileids));
+        $subdir = substr($this->iconhash, 0, 2);
+        $this->assertTrue(is_file($converter->get_workdir_path().'/files/'.$subdir.'/'.$this->iconhash));
+
+        // Check the file records.
+        $files = array();
+        $filerecordids = $converter->get_stash_itemids('files');
+        foreach ($filerecordids as $filerecordid) {
+            $filerecord = $converter->get_stash('files', $filerecordid);
+            $files[$filerecord['filepath'].$filerecord['filename']] = $filerecord;
+        }
+        $this->assertEquals('array', gettype($files['/.']));
+        $this->assertEquals('array', gettype($files['/file1.gif']));
+        $this->assertEquals('array', gettype($files['/sub1/.']));
+        $this->assertEquals('array', gettype($files['/sub1/file2.gif']));
+        $this->assertEquals(sha1(''), $files['/.']['contenthash']);
+        $this->assertEquals(sha1(''), $files['/sub1/.']['contenthash']);
+        $this->assertEquals($this->iconhash, $files['/file1.gif']['contenthash']);
+        $this->assertEquals($this->iconhash, $files['/sub1/file2.gif']['contenthash']);
+
+        $converter->drop_stash_storage();
+    }
+
+    public function test_migrate_directory_with_trailing_slash() {
+        $this->resetAfterTest(true);
+
+        // Set-up the file manager.
+        $converter = convert_factory::get_converter('moodle1', $this->tempdir);
+        $converter->create_stash_storage();
+        $contextid = $converter->get_contextid(CONTEXT_MODULE, 32);
+        $fileman   = $converter->get_file_manager($contextid, 'mod_unittest', 'testarea');
+        // Try to migrate a subdirectory passed with the trailing slash.
+        $returned = $fileman->migrate_directory('course_files/sub1/');
+        // Debugging message must be thrown in this case.
+        $this->assertDebuggingCalled(null, DEBUG_DEVELOPER);
+        $this->assertEquals(gettype($returned), 'array');
+        $this->assertEquals(2, count($returned)); // One file, one directory.
+
+        $converter->drop_stash_storage();
+    }
+
     public function test_convert_path() {
         $path = new convert_path('foo_bar', '/ROOT/THINGS/FOO/BAR');
         $this->assertEquals('foo_bar', $path->get_name());
index 82c4897..148afa3 100644 (file)
@@ -1052,7 +1052,7 @@ abstract class restore_dbops {
 
                 // Most external plugins do not store passwords locally
                 if (!empty($userauth->preventpassindb)) {
-                    $user->password = 'not cached';
+                    $user->password = AUTH_PASSWORD_NOT_CACHED;
 
                 // If Moodle is responsible for storing/validating pwd and reset functionality is available, mark
                 } else if ($userauth->isinternal and $userauth->canresetpwd) {
index fb0e975..7a733ee 100644 (file)
@@ -81,6 +81,7 @@ abstract class base_moodleform extends moodleform {
     function definition() {
         $ui = $this->uistage->get_ui();
         $mform = $this->_form;
+        $mform->setDisableShortforms();
         $stage = $mform->addElement('hidden', 'stage', $this->uistage->get_stage());
         $stage = $mform->addElement('hidden', $ui->get_name(), $ui->get_uniqueid());
         $params = $this->uistage->get_params();
index d8e260a..48782b7 100644 (file)
@@ -76,6 +76,17 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
      * @param string $path xml path which parsing has started
      */
     public function before_path($path) {
+        if ($this->path_is_grouped($path) and !isset($this->currentdata[$path])) {
+            // If the grouped element itself does not contain any final tags,
+            // we would not get any chunk data for it. So we add an artificial
+            // empty data chunk here that will be eventually replaced with
+            // real data later in {@link self::postprocess_chunk()}.
+            $this->currentdata[$path] = array(
+                'path' => $path,
+                'level' => substr_count($path, '/') + 1,
+                'tags' => array(),
+            );
+        }
         if (!$this->grouped_parent_exists($path)) {
             parent::before_path($path);
         }
@@ -93,7 +104,10 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
             // currentdata, properly built
             $data = $this->currentdata[$path];
             unset($this->currentdata[$path]);
+            // Always, before dispatching any chunk, send all pending start notifications.
+            $this->process_pending_startend_notifications($path, 'start');
             // TODO: If running under DEBUG_DEVELOPER notice about >1MB grouped chunks
+            // And, finally, dispatch it.
             $this->dispatch_chunk($data);
         }
         // Normal notification of path end
@@ -167,7 +181,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
      */
     protected function build_currentdata($grouped, $data) {
         // Check the grouped already exists into currentdata
-        if (!array_key_exists($grouped, $this->currentdata)) {
+        if (!is_array($this->currentdata) or !array_key_exists($grouped, $this->currentdata)) {
             $a = new stdclass();
             $a->grouped = $grouped;
             $a->child = $data['path'];
diff --git a/backup/util/xml/parser/tests/fixtures/test6.xml b/backup/util/xml/parser/tests/fixtures/test6.xml
new file mode 100644 (file)
index 0000000..a0399e8
--- /dev/null
@@ -0,0 +1,60 @@
+<test>
+  <MOODLE_BACKUP>
+    <COURSE>
+      <FORMATDATA>
+        <WEEKS>
+          <WEEK>
+            <SECTION>1</SECTION>
+            <HIDENUMBER>1</HIDENUMBER>
+            <HIDEDATE>0</HIDEDATE>
+            <SHOWTO></SHOWTO>
+            <OFFLINEMATERIAL>0</OFFLINEMATERIAL>
+          </WEEK>
+          <WEEK>
+            <SECTION>2</SECTION>
+            <HIDENUMBER>0</HIDENUMBER>
+            <HIDEDATE>0</HIDEDATE>
+            <RESETNUMBER>0</RESETNUMBER>
+            <SHOWTO></SHOWTO>
+            <OFFLINEMATERIAL>0</OFFLINEMATERIAL>
+          </WEEK>
+        </WEEKS>
+        <IMPORTED>
+        </IMPORTED>
+      </FORMATDATA>
+      <GROUPEDNOTOBSERVED>
+        <NOBODYOBSERVERSTHIS>
+          <NOTOBSERVED>Muhehe</NOTOBSERVED>
+        </NOBODYOBSERVERSTHIS>
+      </GROUPEDNOTOBSERVED>
+      <EMPTYGROUPED>
+      </EMPTYGROUPED>
+      <SECONDGROUPED>
+        <SUBS>
+          <SUB>
+            <PROP>Unit tests rock!</PROP>
+          </SUB>
+        </SUBS>
+      </SECONDGROUPED>
+    </COURSE>
+  </MOODLE_BACKUP>
+  <moodle2>
+    <grouped id="this is not parsed at the moment because there are no final elements">
+      <subs>
+        <sub id="34">
+          <prop>Oh yeah</prop>
+        </sub>
+      </subs>
+    </grouped>
+    <groupednonemptywithattr id="78">
+      <prop>Go baby go</prop>
+      <subs>
+        <sub id="89">
+          <prop>http://moodle.org</prop>
+        </sub>
+      </subs>
+    </groupednonemptywithattr>
+    <groupedemptywithattr attr="ay?">
+    </groupedemptywithattr>
+  </moodle2>
+</test>
index 2a1b5cc..39bf2d7 100644 (file)
@@ -632,6 +632,93 @@ class progressive_parser_test extends advanced_testcase {
         $this->assertEquals($errcount, 0); // No errors found, plz
     }
 
+    /**
+     */
+    function test_grouped_at_empty_node() {
+        global $CFG;
+        // Instantiate progressive_parser.
+        $pp =  new progressive_parser();
+        // Instantiate grouped_parser_processor.
+        $pr = new mock_grouped_parser_processor();
+        $this->assertTrue($pr instanceof progressive_parser_processor);
+        // Add interesting paths - moodle1 style.
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/FORMATDATA', true);
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/FORMATDATA/WEEKS/WEEK');
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', true);
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', true);
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED/SUBS/SUB');
+        // Add interesting paths - moodle2 style.
+        $pr->add_path('/test/moodle2/grouped', true);
+        $pr->add_path('/test/moodle2/grouped/subs/sub');
+        $pr->add_path('/test/moodle2/groupedemptywithattr', true);
+        $pr->add_path('/test/moodle2/groupednonemptywithattr', true);
+        $pr->add_path('/test/moodle2/groupednonemptywithattr/subs/sub');
+        // Assign processor to parser.
+        $pp->set_processor($pr);
+        // Set file from fixtures.
+        $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test6.xml');
+        // Process the file.
+        $pp->process();
+
+        // Get all the simplified chunks and perform various validations.
+        $chunks = $pr->get_chunks();
+        $this->assertEquals(count($chunks), 6); // All grouped elements.
+
+        // Check some random data.
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $chunks[0]['path']);
+        $this->assertEquals(2, $chunks[0]['tags']['WEEKS']['WEEK'][1]['SECTION']);
+
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $chunks[1]['path']);
+        $this->assertEquals(array(), $chunks[1]['tags']);
+
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $chunks[2]['path']);
+        $this->assertEquals('Unit tests rock!', $chunks[2]['tags']['SUBS']['SUB'][0]['PROP']);
+
+        $this->assertEquals('/test/moodle2/grouped', $chunks[3]['path']);
+        $this->assertFalse(isset($chunks[3]['tags']['id'])); // No final elements, this should be fixed one day.
+        $this->assertEquals(34, $chunks[3]['tags']['subs']['sub'][0]['id']); // We have final element so this is parsed.
+        $this->assertEquals('Oh yeah', $chunks[3]['tags']['subs']['sub'][0]['prop']);
+
+        $this->assertEquals('/test/moodle2/groupednonemptywithattr', $chunks[4]['path']);
+        $this->assertEquals(78, $chunks[4]['tags']['id']); // We have final element so this is parsed.
+        $this->assertEquals('Go baby go', $chunks[4]['tags']['prop']);
+        $this->assertEquals(89, $chunks[4]['tags']['subs']['sub'][0]['id']);
+        $this->assertEquals('http://moodle.org', $chunks[4]['tags']['subs']['sub'][0]['prop']);
+
+        $this->assertEquals('/test/moodle2/groupedemptywithattr', $chunks[5]['path']);
+        $this->assertFalse(isset($chunks[5]['tags']['attr'])); // No final elements, this should be fixed one day.
+
+        // Now check start notifications.
+        $snotifs = $pr->get_start_notifications();
+        // Check we have received the correct number of notifications.
+        $this->assertEquals(count($snotifs), 6);
+        // Check the order of notifications (in order they appear in test6.xml).
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $snotifs[0]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $snotifs[1]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $snotifs[2]);
+        $this->assertEquals('/test/moodle2/grouped', $snotifs[3]);
+        $this->assertEquals('/test/moodle2/groupednonemptywithattr', $snotifs[4]);
+        $this->assertEquals('/test/moodle2/groupedemptywithattr', $snotifs[5]);
+
+        // Now check end notifications.
+        $enotifs = $pr->get_end_notifications();
+        // Check we have received the correct number of notifications.
+        $this->assertEquals(count($enotifs), 6);
+        // Check the order of notifications (in order they appear in test6.xml).
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $enotifs[0]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $enotifs[1]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $enotifs[2]);
+        $this->assertEquals('/test/moodle2/grouped', $enotifs[3]);
+        $this->assertEquals('/test/moodle2/groupednonemptywithattr', $enotifs[4]);
+        $this->assertEquals('/test/moodle2/groupedemptywithattr', $enotifs[5]);
+
+        // Now verify that the start/process/end order is correct.
+        $allnotifs = $pr->get_all_notifications();
+        $this->assertEquals(count($allnotifs), count($snotifs) + count($enotifs) + count($chunks));
+        // Check integrity of the notifications.
+        $errcount = $this->helper_check_notifications_order_integrity($allnotifs);
+        $this->assertEquals(0, $errcount);
+    }
 
     /**
      * Helper function that given one array of ordered start/process/end notifications will
index 045e72d..fe21d76 100644 (file)
@@ -142,7 +142,7 @@ $fromformdata['subject'] = optional_param('subject', 'all', PARAM_ALPHANUMEXT);
 $fromformdata['audience'] = optional_param('audience', 'all', PARAM_ALPHANUMEXT);
 $fromformdata['language'] = optional_param('language', current_language(), PARAM_ALPHANUMEXT);
 $fromformdata['educationallevel'] = optional_param('educationallevel', 'all', PARAM_ALPHANUMEXT);
-$fromformdata['downloadable'] = optional_param('downloadable', 0, PARAM_ALPHANUM);
+$fromformdata['downloadable'] = optional_param('downloadable', 1, PARAM_ALPHANUM);
 $fromformdata['orderby'] = optional_param('orderby', 'newest', PARAM_ALPHA);
 $fromformdata['huburl'] = optional_param('huburl', HUB_MOODLEORGHUBURL, PARAM_URL);
 $fromformdata['search'] = $search;
index 641ef17..3e00e62 100644 (file)
@@ -76,7 +76,7 @@ class community_hub_search_form extends moodleform {
         if (isset($this->_customdata['downloadable'])) {
             $downloadable = $this->_customdata['downloadable'];
         } else {
-            $downloadable = 0;
+            $downloadable = 1;
         }
         if (isset($this->_customdata['orderby'])) {
             $orderby = $this->_customdata['orderby'];
@@ -135,60 +135,69 @@ class community_hub_search_form extends moodleform {
         }
 
         if (!empty($hubs)) {
-            //TODO: sort hubs by trusted/prioritize
-            //Public hub list
-            $options = array();
-            $firsthub = false;
+            $htmlhubs = array();
             foreach ($hubs as $hub) {
+                // Name can come from hub directory - need some cleaning.
+                $hubname = clean_text($hub['name'], PARAM_TEXT);
+                $smalllogohtml = '';
                 if (array_key_exists('id', $hub)) {
-                    $params = array('hubid' => $hub['id'],
-                        'filetype' => HUB_HUBSCREENSHOT_FILE_TYPE);
-                    $imgurl = new moodle_url(HUB_HUBDIRECTORYURL .
-                                    "/local/hubdirectory/webservice/download.php", $params);
-                    $ascreenshothtml = html_writer::empty_tag('img',
-                                    array('src' => $imgurl, 'alt' => $hub['name']));
-
-                    $hubdescription = html_writer::tag('a', $hub['name'],
-                                    array('class' => 'hublink clearfix', 'href' => $hub['url'],
-                                        'onclick' => 'this.target="_blank"'));
-                    $hubdescription .= html_writer::tag('span', $ascreenshothtml,
-                                    array('class' => 'hubscreenshot'));
-                    $hubdescriptiontext = html_writer::tag('span', format_text($hub['description'], FORMAT_PLAIN),
-                                    array('class' => 'hubdescription'));
+
+                    // Retrieve hub logo + generate small logo.
+                    $params = array('hubid' => $hub['id'], 'filetype' => HUB_HUBSCREENSHOT_FILE_TYPE);
+                    $imgurl = new moodle_url(HUB_HUBDIRECTORYURL . "/local/hubdirectory/webservice/download.php", $params);
+                    $imgsize = getimagesize($imgurl->out(false));
+                    if ($imgsize[0] > 1) {
+                        $ascreenshothtml = html_writer::empty_tag('img', array('src' => $imgurl, 'alt' => $hubname));
+                        $smalllogohtml = html_writer::empty_tag('img', array('src' => $imgurl, 'alt' => $hubname
+                                        , 'height' => 30, 'width' => 40));
+                    } else {
+                        $ascreenshothtml = '';
+                    }
+                    $hubimage = html_writer::tag('div', $ascreenshothtml, array('class' => 'hubimage'));
+
+                    // Statistics + trusted info.
+                    $hubstats = '';
                     if (isset($hub['enrollablecourses'])) { //check needed to avoid warnings for Moodle version < 2011081700
                         $additionaldesc = get_string('enrollablecourses', 'block_community') . ': ' . $hub['enrollablecourses'] . ' - ' .
                                 get_string('downloadablecourses', 'block_community') . ': ' . $hub['downloadablecourses'];
-                        $hubdescriptiontext .= html_writer::tag('span', $additionaldesc,
-                                        array('class' => 'hubadditionaldesc'));
+                        $hubstats .= html_writer::tag('div', $additionaldesc);
                     }
                     if ($hub['trusted']) {
-                    $hubtrusted =  get_string('hubtrusted', 'block_community');
-                    $hubdescriptiontext .= html_writer::tag('span',
-                                    $hubtrusted . ' ' . $OUTPUT->doc_link('trusted_hubs'),
-                                    array('class' => 'trusted'));
-
+                        $hubtrusted =  get_string('hubtrusted', 'block_community');
+                        $hubstats .= $OUTPUT->doc_link('trusted_hubs') . html_writer::tag('div', $hubtrusted);
                     }
-                    $hubdescriptiontext = html_writer::tag('span', $hubdescriptiontext,
-                            array('class' => 'hubdescriptiontext'));
+                    $hubstats = html_writer::tag('div', $hubstats, array('class' => 'hubstats'));
 
-                    $hubdescription = html_writer::tag('span',
-                                    $hubdescription . $hubdescriptiontext,
-                                    array('class' => $hub['trusted'] ? 'hubtrusted' : 'hubnottrusted'));
-                } else {
-                    $hubdescription = html_writer::tag('a', $hub['name'],
-                                    array('class' => 'hublink hubtrusted', 'href' => $hub['url']));
-                }
+                    // hub name link + hub description.
+                    $hubnamelink = html_writer::link($hub['url'], html_writer::tag('h2',$hubname),
+                                    array('class' => 'hubtitlelink'));
+                    // The description can come from the hub directory - need to clean.
+                    $hubdescription = clean_param($hub['description'], PARAM_TEXT);
+                    $hubdescriptiontext = html_writer::tag('div', format_text($hubdescription, FORMAT_PLAIN),
+                                    array('class' => 'hubdescription'));
 
-                if (empty($firsthub)) {
-                    $mform->addElement('radio', 'huburl', get_string('selecthub', 'block_community'),
-                            $hubdescription, $hub['url']);
-                    $mform->setDefault('huburl', $huburl);
-                    $firsthub = true;
+                    $hubtext = html_writer::tag('div', $hubdescriptiontext . $hubstats, array('class' => 'hubtext'));
+
+                    $hubimgandtext = html_writer::tag('div', $hubimage . $hubtext, array('class' => 'hubimgandtext'));
+
+                    $hubfulldesc = html_writer::tag('div', $hubnamelink . $hubimgandtext, array('class' => 'hubmainhmtl'));
                 } else {
-                    $mform->addElement('radio', 'huburl', '', $hubdescription, $hub['url']);
+                    $hubfulldesc = html_writer::link($hub['url'], $hubname);
                 }
+
+                // Add hub to the hub items.
+                $hubinfo = new stdClass();
+                $hubinfo->mainhtml = $hubfulldesc;
+                $hubinfo->rowhtml = html_writer::tag('div', $smalllogohtml , array('class' => 'hubsmalllogo')) . $hubname;
+                $hubitems[$hub['url']] = $hubinfo;
             }
 
+            // Hub listing form element.
+            $mform->addElement('listing','huburl', '', '', array('items' => $hubitems,
+                'showall' => get_string('showall', 'block_community'),
+                'hideall' => get_string('hideall', 'block_community')));
+            $mform->setDefault('huburl', $huburl);
+
             //display enrol/download select box if the USER has the download capability on the course
             if (has_capability('moodle/community:download',
                             context_course::instance($this->_customdata['courseid']))) {
@@ -197,6 +206,8 @@ class community_hub_search_form extends moodleform {
                 $mform->addElement('select', 'downloadable', get_string('enroldownload', 'block_community'),
                         $options);
                 $mform->addHelpButton('downloadable', 'enroldownload', 'block_community');
+
+                $mform->setDefault('downloadable', $downloadable);
             } else {
                 $mform->addElement('hidden', 'downloadable', 0);
             }
@@ -262,23 +273,30 @@ class community_hub_search_form extends moodleform {
             collatorlib::asort($languages);
             $languages = array_merge(array('all' => get_string('any')), $languages);
             $mform->addElement('select', 'language', get_string('language'), $languages);
+
             $mform->setDefault('language', $language);
             $mform->addHelpButton('language', 'language', 'block_community');
 
-            $mform->addElement('radio', 'orderby', get_string('orderby', 'block_community'),
-                    get_string('orderbynewest', 'block_community'), 'newest');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbyeldest', 'block_community'), 'eldest');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbyname', 'block_community'), 'fullname');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbypublisher', 'block_community'), 'publisher');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbyratingaverage', 'block_community'), 'ratingaverage');
+            $mform->addElement('select', 'orderby', get_string('orderby', 'block_community'),
+                array('newest' => get_string('orderbynewest', 'block_community'),
+                    'eldest' => get_string('orderbyeldest', 'block_community'),
+                    'fullname' => get_string('orderbyname', 'block_community'),
+                    'publisher' => get_string('orderbypublisher', 'block_community'),
+                    'ratingaverage' => get_string('orderbyratingaverage', 'block_community')));
+
             $mform->setDefault('orderby', $orderby);
+            $mform->addHelpButton('orderby', 'orderby', 'block_community');
             $mform->setType('orderby', PARAM_ALPHA);
 
-            $mform->addElement('text', 'search', get_string('keywords', 'block_community'));
+            $mform->setAdvanced('audience');
+            $mform->setAdvanced('educationallevel');
+            $mform->setAdvanced('subject');
+            $mform->setAdvanced('licence');
+            $mform->setAdvanced('language');
+            $mform->setAdvanced('orderby');
+
+            $mform->addElement('text', 'search', get_string('keywords', 'block_community'),
+                array('size' => 30));
             $mform->addHelpButton('search', 'keywords', 'block_community');
 
 
@@ -298,4 +316,4 @@ class community_hub_search_form extends moodleform {
         return $errors;
     }
 
-}
\ No newline at end of file
+}
index ef509e3..df06794 100644 (file)
@@ -68,9 +68,11 @@ $string['enrollablecourses'] = 'Enrollable courses';
 $string['errorcourselisting'] = 'An error occurred when retrieving the course listing from the selected hub, please try again later. ({$a})';
 $string['errorhublisting'] = 'An error occurred when retrieving the hub listing from Moodle.org, please try again later. ({$a})';
 $string['fileinfo'] = 'Language: {$a->lang} - License: {$a->license} -  Time updated: {$a->timeupdated}';
+$string['hideall'] = 'Hide hubs';
 $string['hub'] = 'hub';
 $string['hubnottrusted'] = 'Not trusted';
 $string['hubtrusted'] = 'This hub is trusted by Moodle.org';
+$string['install'] = 'Install';
 $string['keywords'] = 'Keywords';
 $string['keywords_help'] = 'You can search for courses containing specific text in the name, description and other fields of the database.';
 $string['langdesc'] = 'Language: {$a} - ';
@@ -106,6 +108,7 @@ $string['searchcourse'] = 'Search for community course';
 $string['selecthub'] = 'Select hub';
 $string['selecthub_help'] = 'Select hub where to search the courses.';
 $string['sites'] = 'Sites';
+$string['showall'] = 'Show all hubs';
 $string['subject'] = 'Subject';
 $string['subject_help'] = 'To narrow your search to courses about a particular subject, choose one from this list.';
 $string['userinfo'] = 'Creator: {$a->creatorname} - Publisher: {$a->publishername}';
index 28d6162..96598cf 100644 (file)
@@ -2,30 +2,23 @@
 
 /* HUB SELECTOR */
 #page-blocks-community-communitycourse .hubscreenshot {float: left; }
-#page-blocks-community-communitycourse .hubdescription {
-    color: #003333;
-    font-size: 95%;
-    display:block;
-}
-#page-blocks-community-communitycourse .hubdescriptiontext {margin-left:160px;display:block;}
-#page-blocks-community-communitycourse .hubadditionaldesc {
-    color: #666666;
-    font-size: 90%;
-    display:block;
-}
+#page-blocks-community-communitycourse .hubtitlelink {color: #999; }
+#page-blocks-community-communitycourse .hubsmalllogo {padding-left: 3px; padding-right: 7px; float: left; }
+#page-blocks-community-communitycourse .hubtext {display: block; width: 68%; padding-left: 165px;}
+#page-blocks-community-communitycourse .hubimgandtext {display:table;}
+#page-blocks-community-communitycourse .hubimage {float: left; display: block; width: 100px;}
+#page-blocks-community-communitycourse .hubdescriptiontext {}
+#page-blocks-community-communitycourse .hubstats {padding-top: 10px}
+#page-blocks-community-communitycourse .hubstats .iconhelp {float: left; padding-right: 3px;}
+#page-blocks-community-communitycourse .hubadditionaldesc {color: #666666; font-size: 90%; display:block;}
 #page-blocks-community-communitycourse .hubscreenshot {margin-right: 10px;}
-#page-blocks-community-communitycourse .hubnottrusted {margin-left: 6px;}
-#page-blocks-community-communitycourse .hubtrusted {display:inline;margin-left: 6px;}
+#page-blocks-community-communitycourse .hubnottrusted {}
+#page-blocks-community-communitycourse .hubtrusted {display:inline;}
 #page-blocks-community-communitycourse .hubnottrusted {}
 #page-blocks-community-communitycourse .trustedtr {background-color: #ffe1c3;}
 #page-blocks-community-communitycourse .prioritisetr {background-color: #ffd4ff;}
 #page-blocks-community-communitycourse .blockdescription {font-size: 80%; color: #555555;}
-#page-blocks-community-communitycourse .trusted {
-    font-size: 90%;
-    color: #006633;
-    font-weight: normal;
-    font-style: italic;
-}
+#page-blocks-community-communitycourse .trusted {font-size: 90%; color: #006633; font-weight: normal; font-style: italic;}
 
 /* COURSES RESULT */
 #page-blocks-community-communitycourse .additionaldesc {font-size: 80%; color: #8B8989;}
index 46c0479..d555668 100644 (file)
@@ -906,7 +906,7 @@ M.core_dock.genericblock.prototype = {
         }
 
         // Must set the image src seperatly of we get an error with XML strict headers
-        var movetoimg = Y.Node.create('<img alt="'+M.str.block.undockitem+'" title="'+M.str.block.undockitem+'" />');
+        var movetoimg = Y.Node.create('<img alt="'+M.str.block.undockitem+'" title="'+M.util.get_string('undockblock', 'block', blocktitle.innerHTML)+'" />');
         var icon = 't/dock_to_block';
         if (right_to_left()) {
             icon = 't/dock_to_block_rtl';
index 884c909..db094c9 100644 (file)
@@ -43,8 +43,18 @@ class block_glossary_random extends block_base {
                 $this->instance_config_commit();
             }
 
+            // Get glossary instance, if not found then return without error, as this will be handled in get_content.
+            if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
+                return false;
+            }
+
+            $this->config->globalglossary = $glossary->globalglossary;
+
+            // Save course id in config, so we can get correct course module.
+            $this->config->courseid = $glossary->course;
+
             // Get module and context, to be able to rewrite urls
-            if (! $cm = get_coursemodule_from_instance("glossary", $this->config->glossary, $this->course->id)) {
+            if (! $cm = get_coursemodule_from_instance('glossary', $glossary->id, $this->config->courseid)) {
                 return false;
             }
             $glossaryctx = context_module::instance($cm->id);
@@ -144,13 +154,22 @@ class block_glossary_random extends block_base {
         }
 
         require_once($CFG->dirroot.'/course/lib.php');
-        $course = $this->page->course;
-        $modinfo = get_fast_modinfo($course);
-        $glossaryid = $this->config->glossary;
 
-        if (!isset($modinfo->instances['glossary'][$glossaryid])) {
-            // we can get here if the glossary has been deleted, so
-            // unconfigure the glossary from the block..
+        // If $this->config->globalglossary is not set then get glossary info from db.
+        if (!isset($this->config->globalglossary)) {
+            if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
+                return '';
+            } else {
+                $this->config->courseid = $glossary->course;
+                $this->config->globalglossary = $glossary->globalglossary;
+                $this->instance_config_commit();
+            }
+        }
+
+        $modinfo = get_fast_modinfo($this->config->courseid);
+        // If deleted glossary or non-global glossary on different course page, then reset.
+        if (!isset($modinfo->instances['glossary'][$this->config->glossary])
+                || ((empty($this->config->globalglossary) && ($this->config->courseid != $this->page->course->id)))) {
             $this->config->glossary = 0;
             $this->config->cache = '';
             $this->instance_config_commit();
@@ -161,8 +180,7 @@ class block_glossary_random extends block_base {
             return $this->content;
         }
 
-        $cm = $modinfo->instances['glossary'][$glossaryid];
-
+        $cm = $modinfo->instances['glossary'][$this->config->glossary];
         if (!has_capability('mod/glossary:view', context_module::instance($cm->id))) {
             return '';
         }
@@ -176,12 +194,10 @@ class block_glossary_random extends block_base {
         }
 
         $this->content = new stdClass();
-        $this->content->text = $this->config->cache;
-
-        // place link to glossary in the footer if the glossary is visible
 
-        //Obtain the visible property from the instance
-        if ($cm->uservisible) {
+        // Show glossary if visible and place links in footer.
+        if ($cm->visible) {
+            $this->content->text = $this->config->cache;
             if (has_capability('mod/glossary:write', context_module::instance($cm->id))) {
                 $this->content->footer = '<a href="'.$CFG->wwwroot.'/mod/glossary/edit.php?cmid='.$cm->id
                 .'" title="'.$this->config->addentry.'">'.$this->config->addentry.'</a><br />';
@@ -192,7 +208,7 @@ class block_glossary_random extends block_base {
             $this->content->footer .= '<a href="'.$CFG->wwwroot.'/mod/glossary/view.php?id='.$cm->id
                 .'" title="'.$this->config->viewglossary.'">'.$this->config->viewglossary.'</a>';
 
-        // otherwise just place some text, no link
+        // Otherwise just place some text, no link.
         } else {
             $this->content->footer = $this->config->invisible;
         }
index d9c613c..24b2df1 100644 (file)
@@ -41,7 +41,7 @@ class block_glossary_random_edit_form extends block_edit_form {
         $mform->setType('config_title', PARAM_TEXT);
 
         // Select glossaries to put in dropdown box ...
-        $glossaries = $DB->get_records_menu('glossary', array('course' => $this->block->course->id), 'name', 'id,name');
+        $glossaries = $DB->get_records_select_menu('glossary', 'course = ? OR globalglossary = ?', array($this->block->course->id, 1), 'name', 'id,name');
         foreach($glossaries as $key => $value) {
             $glossaries[$key] = strip_tags(format_string($value, true));
         }
index a5d21fa..67280cc 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112902;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013020400;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_glossary_random'; // Full name of the plugin (used for diagnostics)
index 88a2c5b..0cdcc5f 100644 (file)
@@ -63,28 +63,7 @@ $CFG->dboptions = array(
 
 
 //=========================================================================
-// 2. SECRET PASSWORD SALT
-//=========================================================================
-// User password salt is very important security feature, it is created
-// automatically in installer, you have to uncomment and modify value
-// on the next line if you are creating config.php manually.
-//
-// $CFG->passwordsaltmain = 'a_very_long_random_string_of_characters#@6&*1';
-//
-// After changing the main salt you have to copy old value into one
-// of the following settings - this allows migration to the new salt
-// during the next login of each user.
-//
-// $CFG->passwordsaltalt1 = '';
-// $CFG->passwordsaltalt2 = '';
-// $CFG->passwordsaltalt3 = '';
-// ....
-// $CFG->passwordsaltalt19 = '';
-// $CFG->passwordsaltalt20 = '';
-
-
-//=========================================================================
-// 3. WEB SITE LOCATION
+// 2. WEB SITE LOCATION
 //=========================================================================
 // Now you need to tell Moodle where it is located. Specify the full
 // web address to where moodle has been installed.  If your web site
@@ -98,7 +77,7 @@ $CFG->wwwroot   = 'http://example.com/moodle';
 
 
 //=========================================================================
-// 4. DATA FILES LOCATION
+// 3. DATA FILES LOCATION
 //=========================================================================
 // Now you need a place where Moodle can save uploaded files.  This
 // directory should be readable AND WRITEABLE by the web server user
@@ -114,7 +93,7 @@ $CFG->dataroot  = '/home/example/moodledata';
 
 
 //=========================================================================
-// 5. DATA FILES PERMISSIONS
+// 4. DATA FILES PERMISSIONS
 //=========================================================================
 // The following parameter sets the permissions of new directories
 // created by Moodle within the data directory.  The format is in
@@ -128,7 +107,7 @@ $CFG->directorypermissions = 02777;
 
 
 //=========================================================================
-// 6. DIRECTORY LOCATION  (most people can just ignore this setting)
+// 5. DIRECTORY LOCATION  (most people can just ignore this setting)
 //=========================================================================
 // A very few webhosts use /admin as a special URL for you to access a
 // control panel or something.  Unfortunately this conflicts with the
@@ -140,7 +119,7 @@ $CFG->admin = 'admin';
 
 
 //=========================================================================
-// 7. OTHER MISCELLANEOUS SETTINGS (ignore these for new installations)
+// 6. OTHER MISCELLANEOUS SETTINGS (ignore these for new installations)
 //=========================================================================
 //
 // These are additional tweaks for which no GUI exists in Moodle yet.
@@ -471,7 +450,7 @@ $CFG->admin = 'admin';
 //      $CFG->svgicons = false;
 //
 //=========================================================================
-// 8. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
+// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
 //
 // Force a debugging mode regardless the settings in the site administration
@@ -512,7 +491,7 @@ $CFG->admin = 'admin';
 // $CFG->showcrondebugging = true;
 //
 //=========================================================================
-// 9. FORCED SETTINGS
+// 8. FORCED SETTINGS
 //=========================================================================
 // It is possible to specify normal admin settings here, the point is that
 // they can not be changed through the standard admin settings pages any more.
@@ -527,12 +506,35 @@ $CFG->admin = 'admin';
 //                                        'otherplugin' => array('mysetting' => 'myvalue', 'thesetting' => 'thevalue'));
 //
 //=========================================================================
-// 10. PHPUNIT SUPPORT
+// 9. PHPUNIT SUPPORT
 //=========================================================================
 // $CFG->phpunit_prefix = 'phpu_';
 // $CFG->phpunit_dataroot = '/home/example/phpu_moodledata';
 // $CFG->phpunit_directorypermissions = 02777; // optional
 //
+//
+//=========================================================================
+// 10. SECRET PASSWORD SALT
+//=========================================================================
+// A single site-wide password salt is no longer required *unless* you are
+// upgrading an older version of Moodle (prior to 2.5), or if you are using
+// a PHP version below 5.3.7. If upgrading, keep any values from your old
+// config.php file. If you are using PHP < 5.3.7 set to a long random string
+// below:
+//
+// $CFG->passwordsaltmain = 'a_very_long_random_string_of_characters#@6&*1';
+//
+// You may also have some alternative salts to allow migration from previously
+// used salts.
+//
+// $CFG->passwordsaltalt1 = '';
+// $CFG->passwordsaltalt2 = '';
+// $CFG->passwordsaltalt3 = '';
+// ....
+// $CFG->passwordsaltalt19 = '';
+// $CFG->passwordsaltalt20 = '';
+//
+//
 //=========================================================================
 // 11. BEHAT SUPPORT
 //=========================================================================
index 1ee8395..ee8eb70 100644 (file)
@@ -435,7 +435,7 @@ M.course_dndupload = {
      * @param section the DOM element reperesenting the course section
      * @return DOM element containing the new item
      */
-    add_resource_element: function(name, section) {
+    add_resource_element: function(name, section, module) {
         var modsel = this.get_mods_element(section);
 
         var resel = {
@@ -451,7 +451,7 @@ M.course_dndupload = {
             progress: document.createElement('span')
         };
 
-        resel.li.className = 'activity resource modtype_resource';
+        resel.li.className = 'activity ' + module + ' modtype_' + module;
 
         resel.indentdiv.className = 'mod-indent';
         resel.li.appendChild(resel.indentdiv);
@@ -707,7 +707,7 @@ M.course_dndupload = {
         }
 
         // Add the file to the display
-        var resel = this.add_resource_element(file.name, section);
+        var resel = this.add_resource_element(file.name, section, module);
 
         // Update the progress bar as the file is uploaded
         xhr.upload.addEventListener('progress', function(e) {
@@ -726,30 +726,37 @@ M.course_dndupload = {
                     if (result) {
                         if (result.error == 0) {
                             // All OK - update the dummy element
-                            resel.icon.src = result.icon;
-                            resel.a.href = result.link;
-                            resel.namespan.innerHTML = result.name;
-                            if (!parseInt(result.visible, 10)) {
-                                resel.a.className = 'dimmed';
-                            }
-
-                            if (result.groupingname) {
-                                resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                            if (result.content) {
+                                // A label
+                                resel.indentdiv.innerHTML = '<div class="activityinstance" ></div>' + result.content + result.commands;
                             } else {
-                                resel.div.removeChild(resel.groupingspan);
+                                // Not a label
+                                resel.icon.src = result.icon;
+                                resel.a.href = result.link;
+                                resel.namespan.innerHTML = result.name;
+
+                                if (!parseInt(result.visible, 10)) {
+                                    resel.a.className = 'dimmed';
+                                }
+
+                                if (result.groupingname) {
+                                    resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                                } else {
+                                    resel.div.removeChild(resel.groupingspan);
+                                }
+
+                                resel.div.removeChild(resel.progressouter);
+                                resel.indentdiv.innerHTML += result.commands;
+                                if (result.onclick) {
+                                    resel.a.onclick = result.onclick;
+                                }
+                                if (self.Y.UA.gecko > 0) {
+                                    // Fix a Firefox bug which makes sites with a '~' in their wwwroot
+                                    // log the user out when clicking on the link (before refreshing the page).
+                                    resel.div.innerHTML = unescape(resel.div.innerHTML);
+                                }
                             }
-
-                            resel.div.removeChild(resel.progressouter);
                             resel.li.id = result.elementid;
-                            resel.indentdiv.innerHTML += result.commands;
-                            if (result.onclick) {
-                                resel.a.onclick = result.onclick;
-                            }
-                            if (self.Y.UA.gecko > 0) {
-                                // Fix a Firefox bug which makes sites with a '~' in their wwwroot
-                                // log the user out when clicking on the link (before refreshing the page).
-                                resel.div.innerHTML = unescape(resel.div.innerHTML);
-                            }
                             self.add_editing(result.elementid);
                         } else {
                             // Error - remove the dummy element
@@ -905,7 +912,7 @@ M.course_dndupload = {
         var self = this;
 
         // Add the item to the display
-        var resel = this.add_resource_element(name, section);
+        var resel = this.add_resource_element(name, section, module);
 
         // Wait for the AJAX call to complete, then update the
         // dummy element with the returned details
@@ -916,30 +923,37 @@ M.course_dndupload = {
                     if (result) {
                         if (result.error == 0) {
                             // All OK - update the dummy element
-                            resel.icon.src = result.icon;
-                            resel.a.href = result.link;
-                            resel.namespan.innerHTML = result.name;
-                            if (!parseInt(result.visible, 10)) {
-                                resel.a.className = 'dimmed';
-                            }
-
-                            if (result.groupingname) {
-                                resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                            if (result.content) {
+                                // A label
+                                resel.indentdiv.innerHTML = '<div class="activityinstance" ></div>' + result.content + result.commands;
                             } else {
-                                resel.div.removeChild(resel.groupingspan);
+                                // Not a label
+                                resel.icon.src = result.icon;
+                                resel.a.href = result.link;
+                                resel.namespan.innerHTML = result.name;
+
+                                if (!parseInt(result.visible, 10)) {
+                                    resel.a.className = 'dimmed';
+                                }
+
+                                if (result.groupingname) {
+                                    resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                                } else {
+                                    resel.div.removeChild(resel.groupingspan);
+                                }
+
+                                resel.div.removeChild(resel.progressouter);
+                                resel.div.innerHTML += result.commands;
+                                if (result.onclick) {
+                                    resel.a.onclick = result.onclick;
+                                }
+                                if (self.Y.UA.gecko > 0) {
+                                    // Fix a Firefox bug which makes sites with a '~' in their wwwroot
+                                    // log the user out when clicking on the link (before refreshing the page).
+                                    resel.div.innerHTML = unescape(resel.div.innerHTML);
+                                }
                             }
-
-                            resel.div.removeChild(resel.progressouter);
                             resel.li.id = result.elementid;
-                            resel.div.innerHTML += result.commands;
-                            if (result.onclick) {
-                                resel.a.onclick = result.onclick;
-                            }
-                            if (self.Y.UA.gecko > 0) {
-                                // Fix a Firefox bug which makes sites with a '~' in their wwwroot
-                                // log the user out when clicking on the link (before refreshing the page).
-                                resel.div.innerHTML = unescape(resel.div.innerHTML);
-                            }
                             self.add_editing(result.elementid, sectionnumber);
                         } else {
                             // Error - remove the dummy element
index 73c8a79..0cdd8bc 100644 (file)
@@ -678,7 +678,12 @@ class dndupload_ajax_processor {
         $resp->error = self::ERROR_OK;
         $resp->icon = $mod->get_icon_url()->out();
         $resp->name = $mod->name;
-        $resp->link = $mod->get_url()->out();
+        if ($mod->has_view()) {
+            $resp->link = $mod->get_url()->out();
+        } else {
+            $resp->link = null;
+        }
+        $resp->content = $mod->get_content();
         $resp->elementid = 'module-'.$mod->id;
         $actions = course_get_cm_edit_actions($mod, 0, $mod->sectionnum);
         $resp->commands = ' '. $courserenderer->course_section_cm_edit_actions($actions);
index 82af237..f353a4b 100644 (file)
@@ -336,6 +336,7 @@ class format_legacy extends format_base {
      * @return bool whether there were any changes to the options values
      */
     public function update_course_format_options($data, $oldcourse = null) {
+        global $DB;
         if ($oldcourse !== null) {
             $data = (array)$data;
             $oldcourse = (array)$oldcourse;
index 71e8797..172d1d9 100644 (file)
@@ -40,7 +40,7 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus
+    // Swap menus.
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
 }
 
@@ -59,7 +59,13 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
     };
 
     if (response.action == 'move') {
-        // update titles in all affected sections
+        // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
+        if (sectionfrom > sectionto) {
+            var temp = sectionto;
+            sectionto = sectionfrom;
+            sectionfrom = temp;
+        }
+        // Update titles in all affected sections.
         for (var i = sectionfrom; i <= sectionto; i++) {
             sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
         }
index f26cafd..debe045 100644 (file)
@@ -271,6 +271,7 @@ class format_topics extends format_base {
      * @return bool whether there were any changes to the options values
      */
     public function update_course_format_options($data, $oldcourse = null) {
+        global $DB;
         if ($oldcourse !== null) {
             $data = (array)$data;
             $oldcourse = (array)$oldcourse;
index 28ec82a..6433702 100644 (file)
@@ -40,7 +40,7 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus
+    // Swap menus.
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
 }
 
@@ -59,7 +59,13 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
     };
 
     if (response.action == 'move') {
-        // update titles in all affected sections
+        // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
+        if (sectionfrom > sectionto) {
+            var temp = sectionto;
+            sectionto = sectionfrom;
+            sectionfrom = temp;
+        }
+        // Update titles in all affected sections.
         for (var i = sectionfrom; i <= sectionto; i++) {
             sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
         }
index a5c1153..40686d4 100644 (file)
@@ -276,6 +276,7 @@ class format_weeks extends format_base {
      * @return bool whether there were any changes to the options values
      */
     public function update_course_format_options($data, $oldcourse = null) {
+        global $DB;
         if ($oldcourse !== null) {
             $data = (array)$data;
             $oldcourse = (array)$oldcourse;
index 241580f..5b901b2 100644 (file)
@@ -2193,15 +2193,22 @@ function course_delete_module($cmid) {
     if (file_exists($modlib)) {
         require_once($modlib);
     } else {
-        throw new moodle_exception("This module is missing mod/$modulename/lib.php", '', '',
-            null, 'failedtodeletemodulemissinglibfile');
+        throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null,
+            "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
     }
 
-    $deleteinstancefunction = $modulename . "_delete_instance";
+    $deleteinstancefunction = $modulename . '_delete_instance';
 
+    // Ensure the delete_instance function exists for this module.
+    if (!function_exists($deleteinstancefunction)) {
+        throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
+            "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
+    }
+
+    // Call the delete_instance function, if it returns false throw an exception.
     if (!$deleteinstancefunction($cm->instance)) {
-        throw new moodle_exception("Could not delete the $modulename (instance)", '', '',
-            null, 'failedtodeletemoduleinstance');
+        throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
+            "Cannot delete the module $modulename (instance).");
     }
 
     // Remove all module files in case modules forget to do that.
@@ -2240,8 +2247,8 @@ function course_delete_module($cmid) {
 
     // Delete module from that section.
     if (!delete_mod_from_section($cm->id, $cm->section)) {
-        throw new moodle_exception("Could not delete the $modulename from section", '', '',
-            null, 'failedtodeletemodulefromsection');
+        throw new moodle_exception('cannotdeletemodulefromsection', '', '', null,
+            "Cannot delete the module $modulename (instance) from section.");
     }
 
     // Trigger a mod_deleted event with information about this module.
index 75eba57..33c0f0a 100644 (file)
@@ -25,14 +25,13 @@ Feature: Add activities to courses
       | Introduction | Test database description |
       | Required entries | 9 |
       | Comments | Yes |
-      | ID number | ASD123 |
+    And I turn editing mode off
     Then I should not see "Adding a new"
     And I follow "Test name"
     And I follow "Edit settings"
     And the "Name" field should match "Test name" value
     And the "Required entries" field should match "9" value
     And the "Comments" field should match "Yes" value
-    And the "ID number" field should match "ASD123" value
 
   @javascript
   Scenario: Add an activity without the required fields
index 3f5b4ba..e20cdc5 100644 (file)
@@ -66,10 +66,6 @@ class behat_course extends behat_base {
      */
     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
 
-        $activity = $this->fixStepArgument($activity);
-        $section = $this->fixStepArgument($section);
-
-        // The 'I wait until the page is ready' is just in case.
         return array(
             new Given('I add a "'.$activity.'" to section "'.$section.'"'),
             new Given('I fill the moodle form with:', $data),
@@ -81,6 +77,7 @@ class behat_course extends behat_base {
      * Opens the activity chooser and opens the activity/resource form page.
      *
      * @Given /^I add a "(?P<activity_or_resource_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      * @param string $activity
      * @param string $section
      */
@@ -90,14 +87,14 @@ class behat_course extends behat_base {
         $section = $this->fixStepArgument($section);
 
         // Clicks add activity or resource section link.
-        $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/*/*";
-        $section = $this->getSession()->getPage()->find('xpath', $sectionxpath);
-        $section->click();
+        $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/span/a";
+        $sectionnode = $this->find('xpath', $sectionxpath);
+        $sectionnode->click();
 
         // Clicks the selected activity if it exists.
         $activityxpath = ".//label[contains(.,'" . $activity . "')]/input";
-        $activity = $this->getSession()->getPage()->find('xpath', $activityxpath);
-        $activity->doubleClick();
+        $activitynode = $this->find('xpath', $activityxpath);
+        $activitynode->doubleClick();
     }
 
 }
index a969354..16ad4a2 100644 (file)
@@ -406,6 +406,7 @@ function enrol_manual_migrate_plugin_enrolments($enrol) {
 
         if (!$minstance) {
             // This should never happen unless adding of default instance fails unexpectedly.
+            debugging('Failed to find manual enrolment instance', DEBUG_DEVELOPER);
             continue;
         }
 
index c1d9b5b..658d5ce 100644 (file)
@@ -132,6 +132,7 @@ if ($courseid and $outcomes = grade_outcome::fetch_all_local($courseid)) {
         $scale = $outcome->load_scale();
         if (empty($scale->id)) {   // hopefully never happens
             $line[] = $scale->get_name();
+            debugging("Found a scale with no ID ({$scale->get_name()}) while outputting course outcomes", DEBUG_DEVELOPER);
         } else {
             if (empty($scale->courseid)) {
                 $caneditthisscale = $caneditsystemscales;
@@ -181,6 +182,7 @@ if ($outcomes = grade_outcome::fetch_all_global()) {
         $scale = $outcome->load_scale();
         if (empty($scale->id)) {   // hopefully never happens
             $line[] = $scale->get_name();
+            debugging("Found a scale with no ID ({$scale->get_name()}) while outputting global outcomes", DEBUG_DEVELOPER);
         } else {
             if (empty($scale->courseid)) {
                 $caneditthisscale = $caneditsystemscales;
index 82fc9eb..f0c7188 100644 (file)
@@ -81,10 +81,13 @@ switch ($action) {
                 // Warn if the grade is out of bounds.
                 if (is_null($finalgrade)) {
                     // ok
-                } else if ($finalgrade < $grade_item->grademin) {
-                    $errorstr = 'lessthanmin';
-                } else if ($finalgrade > $grade_item->grademax) {
-                    $errorstr = 'morethanmax';
+                } else {
+                    $bounded = $grade_item->bounded_grade($finalgrade);
+                    if ($bounded > $finalgrade) {
+                        $errorstr = 'lessthanmin';
+                    } else if ($bounded < $finalgrade) {
+                        $errorstr = 'morethanmax';
+                    }
                 }
 
                 if ($errorstr) {
index 2890159..067453c 100644 (file)
@@ -256,7 +256,7 @@ class grade_report_grader extends grade_report {
                         } else {
                             $bounded = $gradeitem->bounded_grade($finalgrade);
                             if ($bounded > $finalgrade) {
-                            $errorstr = 'lessthanmin';
+                                $errorstr = 'lessthanmin';
                             } else if ($bounded < $finalgrade) {
                                 $errorstr = 'morethanmax';
                             }
diff --git a/grade/tests/reportgrader_test.php b/grade/tests/reportgrader_test.php
new file mode 100644 (file)
index 0000000..07be5e3
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * Unit tests for grade/report/user/lib.php.
+ *
+ * @package  core_grade
+ * @category phpunit
+ * @copyright 2012 Andrew Davis
+ * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/grade/lib.php');
+require_once($CFG->dirroot.'/grade/report/grader/lib.php');
+
+/**
+ * Tests grade_report_grader (the grader report)
+ */
+class grade_report_graderlib_testcase extends advanced_testcase {
+
+    /**
+     * Tests grade_report_grader::process_data()
+     *
+     * process_data() processes submitted grade and feedback data
+     */
+    public function test_process_data() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        // Create and enrol a student.
+        $student = $this->getDataGenerator()->create_user(array('username' => 'Student Sam'));
+        $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $role->id);
+
+        // Test with limited grades.
+        $CFG->unlimitedgrades = 0;
+
+        $forummax = 80;
+        $forum1 = $this->getDataGenerator()->create_module('forum', array('assessed' => 1, 'scale' => $forummax, 'course' => $course->id));
+        // Switch the stdClass instance for a grade item instance.
+        $forum1 = grade_item::fetch(array('itemtype' => 'mod', 'itemmodule' => 'forum', 'iteminstance' => $forum1->id, 'courseid' => $course->id));
+
+        $report = $this->create_report($course, $coursecontext);
+        $testgrade = 60.00;
+
+        $data = new stdClass();
+        $data->id = $course->id;
+        $data->report = 'grader';
+
+        $data->grade = array();
+        $data->grade[$student->id] = array();
+        $data->grade[$student->id][$forum1->id] = $testgrade;
+
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 0);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, $testgrade);
+
+        // Grade above max. Should be pulled down to max.
+        $toobig = 200.00;
+        $data->grade[$student->id][$forum1->id] = $toobig;
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 1);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, $forummax);
+
+        // Grade below min. Should be pulled up to min.
+        $toosmall = -10.00;
+        $data->grade[$student->id][$forum1->id] = $toosmall;
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 1);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, 0);
+
+        // Test unlimited grades so we can give a student a grade about max.
+        $CFG->unlimitedgrades = 1;
+
+        $data->grade[$student->id][$forum1->id] = $toobig;
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 0);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, $toobig);
+    }
+
+    private function create_report($course, $coursecontext) {
+
+        $gpr = new grade_plugin_return(array('type' => 'report', 'plugin'=>'grader', 'courseid' => $course->id));
+        $report = new grade_report_grader($course->id, $gpr, $coursecontext);
+
+        return $report;
+    }
+}
index 6afdd97..4323461 100644 (file)
--- a/help.php
+++ b/help.php
@@ -48,15 +48,15 @@ $PAGE->set_context(context_system::instance());
 
 if ($ajax) {
     @header('Content-Type: text/plain; charset=utf-8');
-} else {
-    echo $OUTPUT->header();
 }
 
 if (!$sm->string_exists($identifier.'_help', $component)) {
-    // strings on-diskc cache may be dirty - try to rebuild it and check again
+    // strings on disk-cache may be dirty - try to rebuild it and check again
     $sm->load_component_strings($component, current_language(), true);
 }
 
+$data = new stdClass();
+
 if ($sm->string_exists($identifier.'_help', $component)) {
     $options = new stdClass();
     $options->trusted = false;
@@ -67,26 +67,38 @@ if ($sm->string_exists($identifier.'_help', $component)) {
     $options->newlines = false;
     $options->overflowdiv = !$ajax;
 
-    if ($ajax) {
-        // When using AJAX, the header should be H2 as it is in the same DOM as the calling page.
-        echo $OUTPUT->heading(format_string(get_string($identifier, $component)), 2, 'helpheading');
-    } else {
-        // When not using AJAX, the header should be H1 as it is in it's own window.
-        echo $OUTPUT->heading(format_string(get_string($identifier, $component)), 1, 'helpheading');
-    }
+    $data->heading = format_string(get_string($identifier, $component));
     // Should be simple wiki only MDL-21695
-    echo format_text(get_string($identifier.'_help', $component), FORMAT_MARKDOWN, $options);
+    $data->text =  format_text(get_string($identifier.'_help', $component), FORMAT_MARKDOWN, $options);
 
-    if ($sm->string_exists($identifier.'_link', $component)) {  // Link to further info in Moodle docs
-        $link = get_string($identifier.'_link', $component);
+    $helplink = $identifier . '_link';
+    if ($sm->string_exists($helplink, $component)) {  // Link to further info in Moodle docs
+        $link = get_string($helplink, $component);
         $linktext = get_string('morehelp');
-        echo '<div class="helpdoclink">'.$OUTPUT->doc_link($link, $linktext).'</div>';
-    }
 
+        $data->doclink = new stdClass();
+        $url = new moodle_url(get_docs_url($link));
+        $data->doclink->link = $url->out();
+        $data->doclink->linktext = $linktext;
+        $data->doclink->class = ($CFG->doctonewwindow) ? 'helplinkpopup' : '';
+
+        $completedoclink = html_writer::tag('div', $OUTPUT->doc_link($link, $linktext), array('class' => 'helpdoclink'));
+    }
 } else {
-    echo "<p><strong>TODO</strong>: missing help string [{$identifier}_help, $component]</p>";
+    $data->text = html_writer::tag('p',
+            html_writer::tag('strong', 'TODO') . ": missing help string [{$identifier}_help, {$component}]");
 }
 
-if (!$ajax) {
+if ($ajax) {
+    echo json_encode($data);
+} else {
+    echo $OUTPUT->header();
+    if (isset($data->heading)) {
+        echo $OUTPUT->heading($data->heading, 1, 'helpheading');
+    }
+    echo $data->text;
+    if (isset($completedoclink)) {
+        echo $completedoclink;
+    }
     echo $OUTPUT->footer();
 }
index 0a1a1b2..8e7d0c0 100644 (file)
@@ -63,6 +63,7 @@ $string['subpages'] = 'Select pages';
 $string['restrictpagetypes'] = 'Display on page types';
 $string['thisspecificpage'] = 'This specific page';
 $string['undockall'] = 'Undock all';
+$string['undockblock'] = 'Undock {$a} block';
 $string['undockitem'] = 'Undock this item';
 $string['visible'] = 'Visible';
 $string['weight'] = 'Weight';
index d5f2c6c..e1db4bc 100644 (file)
@@ -58,6 +58,8 @@ $string['security'] = 'Security';
 $string['selectallornone'] = 'Select all/none';
 $string['selected'] = 'Selected';
 $string['showadvanced'] = 'Show advanced';
+$string['showless'] = 'Show less...';
+$string['showmore'] = 'Show more...';
 $string['showeditortoolbar'] = 'Show editing tools';
 $string['somefieldsrequired'] = 'There are required fields in this form marked {$a}.';
 $string['time'] = 'Time';
index 1543b76..93361fe 100644 (file)
@@ -361,7 +361,6 @@ $string['coursestart'] = 'Course start';
 $string['coursesummary'] = 'Course summary';
 $string['coursesummary_help'] = 'The course summary is displayed in the list of courses. A course search searches course summary text in addition to course names.';
 $string['courseupdates'] = 'Course updates';
-$string['courseuploadlimit'] = 'Course upload limit';
 $string['create'] = 'Create';
 $string['createaccount'] = 'Create my new account';
 $string['createcategory'] = 'Create category';
@@ -924,6 +923,7 @@ $string['list'] = 'List';
 $string['listfiles'] = 'List of files in {$a}';
 $string['listofallpeople'] = 'List of all people';
 $string['listofcourses'] = 'List of courses';
+$string['loadinghelp'] = 'Loading...';
 $string['local'] = 'Local';
 $string['localplugindeleteconfirm'] = 'You are about to completely delete the local plugin \'{$a}\'. This will completely delete everything in the database associated with this plugin. Are you SURE you want to continue?';
 $string['localplugins'] = 'Local plugins';
@@ -1714,6 +1714,7 @@ $string['uploadfailednotrecovering'] = 'Your file upload has failed because ther
 $string['uploadfilelog'] = 'Upload log for file {$a}';
 $string['uploadformlimit'] = 'Uploaded file {$a} exceeded the maximum size limit set by the form';
 $string['uploadlabel'] = 'Title:';
+$string['uploadlimitwithsize'] = '{$a->contextname} upload limit ({$a->displaysize})';
 $string['uploadnewfile'] = 'Upload new file';
 $string['uploadnofilefound'] = 'No file was found - are you sure you selected one to upload?';
 $string['uploadnotallowed'] = 'Uploads are not allowed';
index c6d7f2c..5a59ea6 100644 (file)
@@ -393,6 +393,7 @@ function has_capability($capability, context $context, $user = null, $doanything
     if (!isset($USER->id)) {
         // should never happen
         $USER->id = 0;
+        debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
     }
 
     // make sure there is a real user specified
@@ -2062,6 +2063,7 @@ function can_access_course(stdClass $course, $user = null, $withcapability = '',
     if (!isset($USER->id)) {
         // should never happen
         $USER->id = 0;
+        debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
     }
 
     // make sure there is a user specified
index c957837..41505e0 100644 (file)
@@ -28,6 +28,9 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
+use Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
 /**
  * Steps definitions base class.
  *
@@ -76,6 +79,89 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return 0 !== strpos($path, 'http') ? $startUrl . ltrim($path, '/') : $path;
     }
 
+    /**
+     * Adapter to Behat\Mink\Element\Element::find() using the spin() method.
+     *
+     * @link http://mink.behat.org/#traverse-the-page-selectors
+     * @param Exception $exception Otherwise we throw expcetion with generic info
+     * @param string $selector The selector type (css, xpath, named...)
+     * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
+     * @return NodeElement
+     */
+    protected function find($selector, $locator, $exception = false) {
+
+        // Generic info.
+        if (!$exception) {
+
+            // With named selectors we can be more specific.
+            if ($selector == 'named') {
+                $exceptiontype = $locator[0];
+                $exceptionlocator = $locator[1];
+            } else {
+                $exceptiontype = $selector;
+                $exceptionlocator = $locator;
+            }
+
+            $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator);
+        }
+
+        // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception.
+        return $this->spin(
+            function($context, $args) {
+                return $context->getSession()->getPage()->find($args[0], $args[1]);
+            },
+            array($selector, $locator),
+            self::TIMEOUT,
+            $exception
+       );
+    }
+
+    /**
+     * Finds DOM nodes in the page using named selectors.
+     *
+     * The point of using this method instead of Mink ones is the spin
+     * method of behat_base::find() that looks for the element until it
+     * is available or it timeouts, this avoids the false failures received
+     * when selenium tries to execute commands on elements that are not
+     * ready to be used.
+     *
+     * All steps that requires elements to be available before interact with
+     * them should use one of the find* methods.
+     *
+     * The methods calls requires a {'find_' . $elementtype}($locator)
+     * format, like find_link($locator), find_select($locator),
+     * find_button($locator)...
+     *
+     * @link http://mink.behat.org/#named-selectors
+     * @throws coding_exception
+     * @param string $method The name of the called method
+     * @param mixed $arguments
+     * @return NodeElement
+     */
+    public function __call($name, $arguments) {
+
+        if (substr($name, 0, 5) !== 'find_') {
+            throw new coding_exception('The "' . $name . '" method does not exist');
+        }
+
+        // Only the named selector identifier.
+        $cleanname = substr($name, 5);
+
+        // All named selectors shares the interface.
+        if (count($arguments) !== 1) {
+            throw new coding_exception('The "' . $cleanname . '" named selector needs the locator as it\'s single argument');
+        }
+
+        // Redirecting execution to the find method with the specified selector.
+        // It will detect if it's pointing to an unexisting named selector.
+        return $this->find('named',
+            array(
+                $cleanname,
+                $this->getSession()->getSelectorsHandler()->xpathLiteral($arguments[0])
+            )
+        );
+    }
+
     /**
      * Executes the passed closure until returns true or time outs.
      *
@@ -91,15 +177,20 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      * - Must return something != false if finishes as expected, this will be the (mixed) value
      * returned by spin()
      *
-     * Requires the exception to provide more accurate feedback to tests writers.
+     * The arguments of the closure are mixed, use $args depending on your needs.
      *
-     * @throws Exception If it timeouts without receiving something != false from the closure
-     * @param Closure $lambda The function to execute.
-     * @param Exception $exception The exception to throw in case it time outs.
-     * @param array $args Arguments to pass to the closure
+     * You can provide an exception to give more accurate feedback to tests writers, otherwise the
+     * closure exception will be used, but you must provide an exception if the closure does not throws
+     * an exception.
+     *
+     * @throws Exception            If it timeouts without receiving something != false from the closure
+     * @param  Closure   $lambda    The function to execute.
+     * @param  mixed     $args      Arguments to pass to the closure
+     * @param  int       $timeout   Timeout
+     * @param  Exception $exception The exception to throw in case it time outs.
      * @return mixed The value returned by the closure
      */
-    protected function spin($lambda, $exception, $args, $timeout = false) {
+    protected function spin($lambda, $args = false, $timeout = false, $exception = false) {
 
         // Using default timeout which is pretty high.
         if (!$timeout) {
@@ -112,11 +203,18 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             try {
 
                 // We don't check with !== because most of the time closures will return
-                // direct Behat methods returns and we are not sure it will be always (bool)false.
+                // direct Behat methods returns and we are not sure it will be always (bool)false
+                // if it just runs the behat method without returning anything $return == null.
                 if ($return = $lambda($this, $args)) {
                     return $return;
                 }
             } catch(Exception $e) {
+
+                // We would use the first closure exception if no exception has been provided.
+                if (!$exception) {
+                    $exception = $e;
+                }
+
                 // We wait until no exception is thrown or timeout expires.
                 continue;
             }
@@ -124,6 +222,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             sleep(1);
         }
 
+        // Using coding_exception as is a development issue if no exception has been provided.
+        if (!$exception) {
+            $exception = new coding_exception('spin method requires an exception if the closure doesn\'t throw an exception itself');
+        }
+
         // Throwing exception to the user.
         throw $exception;
     }
index 1c45dbd..e0ec4d4 100644 (file)
@@ -189,6 +189,9 @@ class behat_config_manager {
                         'features' => $features,
                         'steps_definitions' => $stepsdefinitions
                     )
+                ),
+                'formatter' => array(
+                    'name' => 'progress'
                 )
             )
         );
index 79e808b..0ae0ae3 100644 (file)
@@ -216,7 +216,11 @@ function cron_run() {
 
         // note: we can not send emails to suspended accounts
         foreach ($newusers as $newuser) {
-            if (setnew_password_and_mail($newuser)) {
+            // Use a low cost factor when generating bcrypt hash otherwise
+            // hashing would be slow when emailing lots of users. Hashes
+            // will be automatically updated to a higher cost factor the first
+            // time the user logs in.
+            if (setnew_password_and_mail($newuser, true)) {
                 unset_user_preference('create_password', $newuser);
                 set_user_preference('auth_forcepasswordchange', 1, $newuser);
             } else {
index a1e5ab9..f54eae6 100644 (file)
@@ -57,7 +57,9 @@ function get_admin() {
     static $mainadmin = null;
     static $prevadmins = null;
 
-    if (empty($CFG->siteadmins)) {  // Should not happen on an ordinary site.
+    if (empty($CFG->siteadmins)) {
+        // Should not happen on an ordinary site.
+        // It does however happen during unit tests.
         return false;
     }
 
index d4eb4fc..1602999 100644 (file)
         <FIELD NAME="suspended" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="suspended flag prevents users to log in"/>
         <FIELD NAME="mnethostid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="username" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="password" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="idnumber" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="firstname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="lastname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
index 3dcc1d1..b563121 100644 (file)
@@ -1564,6 +1564,18 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012120300.07);
     }
 
+    if ($oldversion < 2013021100.01) {
+
+        // Changing precision of field password on table user to (255).
+        $table = new xmldb_table('user');
+        $field = new xmldb_field('password', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'username');
+
+        // Launch change of precision for field password.
+        $dbman->change_field_precision($table, $field);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021100.01);
+    }
 
     return true;
 }
index 013f6c5..d5b84aa 100644 (file)
@@ -4290,7 +4290,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            $bprecord = $DB->get_record('block_positions', array('blockinstanceid' => $context->instanceid), 'visible');
+            $bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));
             // User can't access file, if block is hidden or doesn't have block:view capability
             if (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {
                  send_file_not_found();
index 1f7856b..e9f9490 100644 (file)
@@ -6,58 +6,6 @@
 // Namespace for the form bits and bobs
 M.form = M.form || {};
 
-/**
- * Initialises the show advanced functionality and events.
- * This should only ever happen ONCE per page.
- *
- * @param {YUI} Y
- * @param {object} config
- */
-M.form.initShowAdvanced = function(Y, config) {
-    if (M.form.showAdvanced) {
-        return M.form.showAdvanced;
-    }
-    var showAdvanced = function(config) {
-        showAdvanced.superclass.constructor.apply(this, arguments);
-    };
-    showAdvanced.prototype = {
-        _advButtons : [],
-        _advAreas : [],
-        _stateInput : null,
-        initializer : function() {
-            this._advAreas = Y.all('form .advanced');
-            this._advButtons = Y.all('.showadvancedbtn');
-            if (this._advButtons.size() > 0) {
-                this._stateInput = new Y.NodeList(document.getElementsByName('mform_showadvanced_last'));
-                this._advButtons.on('click', this.switchState, this);
-                this._advButtons.set('type', 'button');
-            }
-        },
-        /**
-         * Toggles between showing advanced items and hiding them.
-         * Should be fired by an event.
-         */
-        switchState : function(e) {
-            e.preventDefault();
-            if (this._stateInput.get('value')=='1') {
-                this._stateInput.set('value', '0');
-                this._advButtons.setAttribute('value', M.str.form.showadvanced);
-                this._advAreas.addClass('hide');
-            } else {
-                this._stateInput.set('value', '1');
-                this._advButtons.setAttribute('value', M.str.form.hideadvanced);
-                this._advAreas.removeClass('hide');
-            }
-        }
-    };
-    // Extend it with the YUI widget fw.
-    Y.extend(showAdvanced, Y.Base, showAdvanced.prototype, {
-        NAME : 'mform-showAdvanced'
-    });
-    M.form.showAdvanced = new showAdvanced(config);
-    return M.form.showAdvanced;
-};
-
 /**
  * Stores a list of the dependencyManager for each form on the page.
  */
diff --git a/lib/form/listing.php b/lib/form/listing.php
new file mode 100644 (file)
index 0000000..102b492
--- /dev/null
@@ -0,0 +1,149 @@
+<?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/>.
+
+/**
+ * Listing form element.
+ *
+ * Contains HTML class for a listing form element.
+ *
+ * @package   core_form
+ * @copyright 2012 Jerome Mouneyrac
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+if (!defined('MOODLE_INTERNAL')) {
+    die('Direct access to this script is forbidden.');
+}
+
+require_once("HTML/QuickForm/input.php");
+
+/**
+* The listing element is a simple customizable "select" without the input type=select.
+* One main div contains the "large" html of an item.
+* A show/hide div shows a hidden div containing the list of all items.
+* This list is composed by the "small" html of each item.
+*
+* How to use it:
+* The options parameter is an array containing:
+*   - items => array of object: the key is the value of the form input
+*                               $item->rowhtml => small html
+*                               $item->mainhtml => large html
+*   - showall/hideall => string for the Show/Hide button
+*
+* WARNINGS: The form lets you display HTML. So it is subject to CROSS-SCRIPTING if you send it uncleaned HTML.
+*           Don't forget to escape your HTML as soon as one string comes from an input/external source.
+*
+* How to customize it:
+*   You can change the css in core.css. For example if you remove float:left; from .formlistingrow,
+*   then the item list is not display as tabs but as rows.
+*
+* @package   core_form
+* @copyright 2012 Jerome Mouneyrac
+* @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+*/
+class MoodleQuickForm_listing extends HTML_QuickForm_input {
+
+    /** @var array items to display. */
+    protected $items = array();
+
+    /** @var string language string for Show All. */
+    protected $showall;
+
+    /** @var string language string for Hide. */
+    protected $hideall;
+
+    /**
+     * Constructor.
+     *
+     * @param string $elementName (optional) name of the listing.
+     * @param string $elementLabel (optional) listing label.
+     * @param array $attributes (optional) Either a typical HTML attribute string or an associative array.
+     * @param array $options set of options to initalize listing.
+     */
+    function MoodleQuickForm_listing($elementName=null, $elementLabel=null, $attributes=null, $options=array()) {
+
+       $this->_type = 'listing';
+        if (!empty($options['items'])) {
+            $this->items = $options['items'];
+        }
+        if (!empty($options['showall'])) {
+            $this->showall = $options['showall'];
+        } else {
+            $this->showall = get_string('showall');
+        }
+        if (!empty($options['hideall'])) {
+            $this->hideall = $options['hideall'];
+        } else {
+            $this->hideall = get_string('hide');
+        }
+        parent::HTML_QuickForm_input($elementName, $elementLabel, $attributes);
+    }
+
+    /**
+     * Returns HTML for listing form element.
+     *
+     * @return string the HTML.
+     */
+    function toHtml() {
+        global $CFG, $PAGE;
+
+        $mainhtml = html_writer::tag('div', $this->items[$this->getValue()]->mainhtml,
+                array('id' => $this->getName().'_items_main', 'class' => 'formlistingmain'));
+
+        // Add the main div containing the selected item (+ the caption: "More items").
+        $html = html_writer::tag('div', $mainhtml .
+                    html_writer::tag('div', $this->showall,
+                        array('id' => $this->getName().'_items_caption', 'class' => 'formlistingmore')),
+                    array('id'=>$this->getName().'_items', 'class' => 'formlisting hide'));
+
+        // Add collapsible region: all the items.
+        $itemrows = '';
+        $html .= html_writer::tag('div', $itemrows,
+                array('id' => $this->getName().'_items_all', 'class' => 'formlistingall'));
+
+        // Add radio buttons for non javascript support.
+        $radiobuttons = '';
+        foreach ($this->items as $itemid => $item) {
+            $radioparams = array('name' => $this->getName(), 'value' => $itemid,
+                    'id' => 'id_'.$itemid, 'class' => 'formlistinginputradio', 'type' => 'radio');
+            if ($itemid == $this->getValue()) {
+                $radioparams['checked'] = 'checked';
+            }
+            $radiobuttons .= html_writer::tag('div', html_writer::tag('input',
+                html_writer::tag('div', $item->rowhtml, array('class' => 'formlistingradiocontent')), $radioparams),
+                array('class' => 'formlistingradio'));
+        }
+
+        // Container for the hidden hidden input which will contain the selected item.
+        $html .= html_writer::tag('div', $radiobuttons,
+                array('id' => 'formlistinginputcontainer_' . $this->getName(), 'class' => 'formlistinginputcontainer'));
+
+        $module = array('name'=>'form_listing', 'fullpath'=>'/lib/form/yui/listing/listing.js',
+            'requires'=>array('node', 'event', 'transition', 'escape'));
+
+        $PAGE->requires->js_init_call('M.form_listing.init',
+                 array(array(
+                'elementid' => $this->getName().'_items',
+                'hideall' => $this->hideall,
+                'showall' => $this->showall,
+                'hiddeninputid' => $this->getAttribute('id'),
+                'items' => $this->items,
+                'inputname' => $this->getName(),
+                'currentvalue' => $this->getValue())), true, $module);
+
+        return $html;
+    }
+}
diff --git a/lib/form/yui/listing/listing.js b/lib/form/yui/listing/listing.js
new file mode 100644 (file)
index 0000000..0e420e0
--- /dev/null
@@ -0,0 +1,93 @@
+// 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/>.
+
+/**
+ * Form listing Javascript.
+ *
+ * It mainly handles loading the main content div when cliking on a tab/row.
+ * @copyright 2012 Jerome Mouneyrac
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+M.form_listing = {};
+M.form_listing.Y = null;
+M.form_listing.instances = [];
+
+/**
+ * This function is called for each listing form on page.
+ *
+ * @param {Array} params :  {int} hiddeninputid - the id of the hidden input element
+ *                          {int} elementid - the id of the full form element
+ *                          {Array} items - items has for key the value return by the form, and for content an array with two attributs: mainhtml and rowhtml.
+ *                          {string} hideall - button label to hide all tabs(rows).
+ *                          {string} showall - button label to show all tabs(rows).
+ *                          {string} inputname - the name of the input element
+ *                          {string} currentvalue - the currently selected tab(row)
+ */
+M.form_listing.init = function(Y, params) {
+    if (params && params.hiddeninputid && params.elementid) {
+
+        // Replace the radio buttons by a hidden input.
+        Y.one('#formlistinginputcontainer_' + params.inputname).setHTML('<input name='+params.inputname+' type=hidden id='+params.hiddeninputid+' value='+params.currentvalue+' />');
+
+        var caption = Y.one('#'+params.elementid+'_caption');
+        var allitems = Y.one('#'+params.elementid+'_all');
+        var selecteditem = Y.one('#'+params.elementid+'_main');
+        var hiddeninput = Y.one('#'+params.hiddeninputid);
+
+        // Do not display the listing by default.
+        var show = 0;
+        allitems.hide();
+
+        // Refresh the main item + set the hidden input to its value.
+        var selectitem = function(e) {
+            var index = this.get('id').replace(params.elementid+'_all_',"");
+            hiddeninput.set('value', items[index]);
+            selecteditem.setHTML(params.items[items[index]].mainhtml);
+        }
+
+        // Caption Onlick event to display/hide the listing.
+        var onclick = function(e) {
+            if (!show) {
+                allitems.show(true);
+                show = 1;
+                caption.setHTML(params.hideall);
+            } else {
+                allitems.hide(true);
+                show = 0;
+                caption.setHTML(params.showall);
+            }
+        };
+
+        caption.on('click', onclick);
+
+        // Fill the item rows with html + add event.
+        // PS: we need to save the items into a temporary "items[]" array because params.items keys could be url.
+        // This temporary items[] avoid not working calls like Y.one('#myitems_http:www.google.com').
+        var items = [];
+        var itemindex = 0;
+        for (itemid in params.items) {
+            items[itemindex] = itemid;
+
+            // Add the row.
+            allitems.append("<div id="+params.elementid+'_all_'+itemindex+" class='formlistingrow'>" + params.items[itemid].rowhtml + "</div>");
+
+            // Add click event to the row.
+            Y.one('#'+params.elementid+'_all_'+itemindex).on('click', selectitem);
+
+            itemindex = itemindex + 1;
+        }
+    }
+};
diff --git a/lib/form/yui/shortforms/shortforms.js b/lib/form/yui/shortforms/shortforms.js
new file mode 100644 (file)
index 0000000..c874791
--- /dev/null
@@ -0,0 +1,96 @@
+YUI.add('moodle-form-shortforms', function(Y) {
+    /**
+     * Provides the form shortforms class.
+     *
+     * @module moodle-form-shortforms
+     */
+
+    /**
+     * A class for a shortforms.
+     *
+     * @param {Object} config Object literal specifying shortforms configuration properties.
+     * @class M.form.shortforms
+     * @constructor
+     * @extends Y.Base
+     */
+    function SHORTFORMS(config) {
+        SHORTFORMS.superclass.constructor.apply(this, [config]);
+    }
+
+    var SELECTORS = {
+            FIELDSETCOLLAPSIBLE : 'fieldset.collapsible',
+            LEGENDFTOGGLER : 'legend.ftoggler'
+        },
+        CSS = {
+            COLLAPSED : 'collapsed',
+            FHEADER : 'fheader',
+            JSPROCESSED : 'jsprocessed'
+        },
+        ATTRS = {};
+
+    /**
+     * Static property provides a string to identify the JavaScript class.
+     *
+     * @property NAME
+     * @type String
+     * @static
+     */
+    SHORTFORMS.NAME = 'moodle-form-shortforms';
+
+    /**
+     * Static property used to define the default attribute configuration for the Shortform.
+     *
+     * @property ATTRS
+     * @type String
+     * @static
+     */
+    SHORTFORMS.ATTRS = ATTRS;
+
+    /**
+     * The form ID attribute definition.
+     *
+     * @attribute formid
+     * @type String
+     * @default ''
+     * @writeOnce
+     */
+    ATTRS.formid = {
+        value : null
+    };
+
+    Y.extend(SHORTFORMS, Y.Base, {
+        initializer : function() {
+            var fieldlist = Y.Node.all('#'+this.get('formid')+' '+SELECTORS.FIELDSETCOLLAPSIBLE);
+            // Look through collapsible fieldset divs.
+            fieldlist.each(this.process_fieldset, this);
+            // Subscribe collapsible fieldsets to click event.
+            Y.one('#'+this.get('formid')).delegate('click', this.switch_state, SELECTORS.FIELDSETCOLLAPSIBLE+' .'+CSS.FHEADER);
+        },
+        process_fieldset : function(fieldset) {
+            fieldset.addClass(CSS.JSPROCESSED);
+            // Get legend element.
+            var legendelement = fieldset.one(SELECTORS.LEGENDFTOGGLER);
+
+            // Turn headers to links for accessibility.
+            var headerlink = Y.Node.create('<a href="#"></a>');
+            headerlink.addClass(CSS.FHEADER);
+            headerlink.appendChild(legendelement.get('firstChild'));
+            legendelement.prepend(headerlink);
+        },
+        switch_state : function(e) {
+            e.preventDefault();
+            var fieldset = this.ancestor(SELECTORS.FIELDSETCOLLAPSIBLE);
+            // Toggle collapsed class.
+            fieldset.toggleClass(CSS.COLLAPSED);
+            // Get corresponding hidden variable
+            // - and invert it.
+            var statuselement = new Y.one('input[name=mform_isexpanded_'+fieldset.get('id')+']');
+            statuselement.set('value', Math.abs(Number(statuselement.get('value'))-1));
+        }
+    });
+
+    M.form = M.form || {};
+    M.form.shortforms = M.form.shortforms || function(params) {
+        return new SHORTFORMS(params);
+    };
+}, '@VERSION@', {requires:['base', 'node', 'selector-css3']});
diff --git a/lib/form/yui/showadvanced/showadvanced.js b/lib/form/yui/showadvanced/showadvanced.js
new file mode 100644 (file)
index 0000000..562e20e
--- /dev/null
@@ -0,0 +1,104 @@
+YUI.add('moodle-form-showadvanced', function(Y) {
+    /**
+     * Provides the form showadvanced class.
+     *
+     * @module moodle-form-showadvanced
+     */
+
+    /**
+     * A class for a showadvanced.
+     *
+     * @param {Object} config Object literal specifying showadvanced configuration properties.
+     * @class M.form.showadvanced
+     * @constructor
+     * @extends Y.Base
+     */
+    function SHOWADVANCED(config) {
+        SHOWADVANCED.superclass.constructor.apply(this, [config]);
+    }
+
+    var SELECTORS = {
+            FIELDSETCONTAINSADVANCED : 'fieldset.containsadvancedelements',
+            DIVFITEMADVANCED : 'div.fitem.advanced',
+            DIVFCONTAINER : 'div.fcontainer'
+        },
+        CSS = {
+            HIDE : 'hide',
+            MORELESSTOGGLER : 'morelesstoggler'
+        },
+        ATTRS = {};
+
+    /**
+     * Static property provides a string to identify the JavaScript class.
+     *
+     * @property NAME
+     * @type String
+     * @static
+     */
+    SHOWADVANCED.NAME = 'moodle-form-showadvanced';
+
+    /**
+     * Static property used to define the default attribute configuration for the Showadvanced.
+     *
+     * @property ATTRS
+     * @type String
+     * @static
+     */
+    SHOWADVANCED.ATTRS = ATTRS;
+
+    /**
+     * The form ID attribute definition.
+     *
+     * @attribute formid
+     * @type String
+     * @default ''
+     * @writeOnce
+     */
+    ATTRS.formid = {
+        value : null
+    };
+
+    Y.extend(SHOWADVANCED, Y.Base, {
+        initializer : function() {
+            var fieldlist = Y.Node.all('#'+this.get('formid')+' '+SELECTORS.FIELDSETCONTAINSADVANCED);
+            // Look through fieldset divs that contain advanced elements.
+            fieldlist.each(this.process_fieldset, this);
+            // Subscribe more/less links to click event.
+            Y.one('#'+this.get('formid')).delegate('click', this.switch_state, SELECTORS.FIELDSETCONTAINSADVANCED+' .'+CSS.MORELESSTOGGLER);
+        },
+        process_fieldset : function(fieldset) {
+            var statuselement = new Y.one('input[name=mform_showmore_'+fieldset.get('id')+']');
+            var morelesslink = Y.Node.create('<a href="#"></a>');
+            morelesslink.addClass(CSS.MORELESSTOGGLER);
+            if (statuselement.get('value') === '0') {
+                morelesslink.setHTML(M.str.form.showmore);
+                // Hide advanced stuff initially.
+                fieldset.all(SELECTORS.DIVFITEMADVANCED).addClass(CSS.HIDE);
+            } else {
+                morelesslink.setHTML(M.str.form.showless);
+            }
+            fieldset.one(SELECTORS.DIVFCONTAINER).append(morelesslink);
+        },
+        switch_state : function(e) {
+            e.preventDefault();
+            var fieldset = this.ancestor(SELECTORS.FIELDSETCONTAINSADVANCED);
+            // Toggle collapsed class.
+            fieldset.all(SELECTORS.DIVFITEMADVANCED).toggleClass(CSS.HIDE);
+            // Get corresponding hidden variable.
+            var statuselement = new Y.one('input[name=mform_showmore_'+fieldset.get('id')+']');
+            // Invert it and change the link text.
+            if (statuselement.get('value') === '0') {
+                statuselement.set('value', 1);
+                this.setHTML(M.util.get_string('showless', 'form'));
+            } else {
+                statuselement.set('value', 0);
+                this.setHTML(M.util.get_string('showmore', 'form'));
+            }
+        }
+    });
+
+    M.form = M.form || {};
+    M.form.showadvanced = M.form.showadvanced || function(params) {
+        return new SHOWADVANCED(params);
+    };
+}, '@VERSION@', {requires:['base', 'node', 'selector-css3']});
index 03c8c33..92c16a2 100644 (file)
@@ -1069,6 +1069,9 @@ abstract class moodleform {
                                 $mform->setType($elementname, $params);
                             }
                             break;
+                        case 'expanded' :
+                            $mform->setExpanded($realelementname, $params);
+                            break;
                     }
                 }
             }
@@ -1222,11 +1225,7 @@ abstract class moodleform {
         return array(
             'name' => 'mform',
             'fullpath' => '/lib/form/form.js',
-            'requires' => array('base', 'node'),
-            'strings' => array(
-                array('showadvanced', 'form'),
-                array('hideadvanced', 'form')
-            )
+            'requires' => array('base', 'node')
         );
     }
 }
@@ -1260,8 +1259,19 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
     /** @var array Array whose keys are element names. If the key exists this is a advanced element */
     var $_advancedElements = array();
 
-    /** @var bool Whether to display advanced elements (on page load) */
-    var $_showAdvanced = null;
+    /**
+     * Array whose keys are element names and the the boolean values reflect the current state. If the key exists this is a collapsible element.
+     *
+     * @var array
+     */
+    var $_collapsibleElements = array();
+
+    /**
+     * Whether to enable shortforms for this form
+     *
+     * @var boolean
+     */
+    var $_disableShortforms = false;
 
     /** @var bool whether to automatically initialise M.formchangechecker for this form. */
     protected $_use_form_change_checker = true;
@@ -1280,6 +1290,14 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
      */
     var $_pageparams = '';
 
+    /**
+     * The maximum number of headers the form should contain in order not to be
+     * defined as collapsible.
+     *
+     * @var int
+     */
+    var $_non_collapsible_headers = 2;
+
     /**
      * Class constructor - same parameters as HTML_QuickForm_DHTMLRulesTableless
      *
@@ -1340,59 +1358,84 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
         } elseif (isset($this->_advancedElements[$elementName])) {
             unset($this->_advancedElements[$elementName]);
         }
-        if ($advanced && $this->getElementType('mform_showadvanced_last')===false){
-            $this->setShowAdvanced();
-            $this->registerNoSubmitButton('mform_showadvanced');
+    }
 
-            $this->addElement('hidden', 'mform_showadvanced_last');
-            $this->setType('mform_showadvanced_last', PARAM_INT);
+    /**
+     * Use this method to indicate that the fieldset should be shown as expanded.
+     * The method is applicable to header elements only.
+     *
+     * @param string $headerName header element name
+     * @param boolean $expanded default true sets the element to expanded. False makes the element collapsed.
+     */
+    function setExpanded($headerName, $expanded=true){
+        if ($this->getElementType('mform_isexpanded_'.$headerName)===false) {
+            // see if we the form has been submitted already
+            $formexpanded = optional_param('mform_isexpanded_'.$headerName, -1, PARAM_INT);
+            if (!$expanded && $formexpanded != -1) {
+                // override expanded state with the form variable
+                $expanded = $formexpanded;
+            }
+            // create the form element for storing expanded state
+            $this->addElement('hidden', 'mform_isexpanded_'.$headerName);
+            $this->setType('mform_isexpanded_'.$headerName, PARAM_INT);
+            $this->setConstant('mform_isexpanded_' . $headerName, (int)$expanded);
         }
+        $this->_collapsibleElements[$headerName] = !$expanded;
     }
+
     /**
-     * Set whether to show advanced elements in the form on first displaying form. Default is not to
-     * display advanced elements in the form until 'Show Advanced' is pressed.
-     *
-     * You can get the last state of the form and possibly save it for this user by using
-     * value 'mform_showadvanced_last' in submitted data.
+     * Use this method to add show more/less status element required for passing
+     * over the advanced elements visibility status on the form submission.
      *
-     * @param bool $showadvancedNow if true will show adavance elements.
+     * @param string $headerName header element name.
+     * @param boolean $showmore default false sets the advanced elements to be hidden.
      */
-    function setShowAdvanced($showadvancedNow = null){
-        if ($showadvancedNow === null){
-            if ($this->_showAdvanced !== null){
-                return;
-            } else { //if setShowAdvanced is called without any preference
-                     //make the default to not show advanced elements.
-                $showadvancedNow = get_user_preferences(
-                                textlib::strtolower($this->_formName.'_showadvanced', 0));
-            }
-        }
-        //value of hidden element
-        $hiddenLast = optional_param('mform_showadvanced_last', -1, PARAM_INT);
-        //value of button
-        $buttonPressed = optional_param('mform_showadvanced', 0, PARAM_RAW);
-        //toggle if button pressed or else stay the same
-        if ($hiddenLast == -1) {
-            $next = $showadvancedNow;
-        } elseif ($buttonPressed) { //toggle on button press
-            $next = !$hiddenLast;
-        } else {
-            $next = $hiddenLast;
-        }
-        $this->_showAdvanced = $next;
-        if ($showadvancedNow != $next){
-            set_user_preference($this->_formName.'_showadvanced', $next);
+    function addAdvancedStatusElement($headerName, $showmore=false){
+        // Add extra hidden element to store advanced items state for each section.
+        if ($this->getElementType('mform_showmore_' . $headerName) === false) {
+            // See if we the form has been submitted already.
+            $formshowmore = optional_param('mform_showmore_' . $headerName, -1, PARAM_INT);
+            if (!$showmore && $formshowmore != -1) {
+                // Override showmore state with the form variable.
+                $showmore = $formshowmore;
+            }
+            // Create the form element for storing advanced items state.
+            $this->addElement('hidden', 'mform_showmore_' . $headerName);
+            $this->setType('mform_showmore_' . $headerName, PARAM_INT);
+            $this->setConstant('mform_showmore_' . $headerName, (int)$showmore);
         }
-        $this->setConstants(array('mform_showadvanced_last'=>$next));
     }
 
     /**
-     * Gets show advance value, if advance elements are visible it will return true else false
+     * This function has been deprecated. Show advanced has been replaced by
+     * "Show more.../Show less..." in the shortforms javascript module.
      *
-     * @return bool
-     */
+     * @deprecated since Moodle 2.5
+     * @param bool $showadvancedNow if true will show advanced elements.
+      */
+    function setShowAdvanced($showadvancedNow = null){
+        debugging('Call to deprecated function setShowAdvanced. See "Show more.../Show less..." in shortforms yui module.');
+    }
+
+    /**
+     * This function has been deprecated. Show advanced has been replaced by
+     * "Show more.../Show less..." in the shortforms javascript module.
+     *
+     * @deprecated since Moodle 2.5
+     * @return bool (Always false)
+      */
     function getShowAdvanced(){
-        return $this->_showAdvanced;
+        debugging('Call to deprecated function setShowAdvanced. See "Show more.../Show less..." in shortforms yui module.');
+        return false;
+    }
+
+    /**
+     * Use this method to indicate that the form will not be using shortforms.
+     *
+     * @param boolean $disable default true, controls if the shortforms are disabled.
+     */
+    function setDisableShortforms ($disable = true) {
+        $this->_disableShortforms = $disable;
     }
 
     /**
@@ -1427,13 +1470,14 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
     */
     function accept(&$renderer) {
         if (method_exists($renderer, 'setAdvancedElements')){
-            //check for visible fieldsets where all elements are advanced
+            //Check for visible fieldsets where all elements are advanced
             //and mark these headers as advanced as well.
-            //And mark all elements in a advanced header as advanced
+            //Also mark all elements in a advanced header as advanced.
             $stopFields = $renderer->getStopFieldSetElements();
             $lastHeader = null;
             $lastHeaderAdvanced = false;
             $anyAdvanced = false;
+            $anyError = false;
             foreach (array_keys($this->_elements) as $elementIndex){
                 $element =& $this->_elements[$elementIndex];
 
@@ -1441,6 +1485,7 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
                 if ($element->getType()=='header' || in_array($element->getName(), $stopFields)){
                     if ($anyAdvanced && !is_null($lastHeader)){
                         $this->setAdvanced($lastHeader->getName());
+                        $this->addAdvancedStatusElement($lastHeader->getName(), $anyError);
                     }
                     $lastHeaderAdvanced = false;
                     unset($lastHeader);
@@ -1452,17 +1497,73 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
                 if ($element->getType()=='header'){
                     $lastHeader =& $element;
                     $anyAdvanced = false;
+                    $anyError = false;
                     $lastHeaderAdvanced = isset($this->_advancedElements[$element->getName()]);
                 } elseif (isset($this->_advancedElements[$element->getName()])){
                     $anyAdvanced = true;
+                    if (isset($this->_errors[$element->getName()])) {
+                        $anyError = true;
+                    }
                 }
             }
             // the last header may not be closed yet...
             if ($anyAdvanced && !is_null($lastHeader)){
                 $this->setAdvanced($lastHeader->getName());
+                $this->addAdvancedStatusElement($lastHeader->getName(), $anyError);
             }
             $renderer->setAdvancedElements($this->_advancedElements);
-
+        }
+        if (method_exists($renderer, 'setCollapsibleElements') && !$this->_disableShortforms){
+            // Check how many headers we have in total, if less than $_non_collapsible_headers,
+            // the form should not be collapsible at all (unless overidden in the form definition).
+            $headercounter = 0;
+            foreach (array_keys($this->_elements) as $elementIndex){
+                $element =& $this->_elements[$elementIndex];
+                if ($element->getType()=='header') {
+                    $headercounter++;
+                }
+            }
+            if ($headercounter > $this->_non_collapsible_headers) {
+                // So, we have more than $_non_collapsible_headers headers
+                // add all headers to collapsible elements array (if they have not been added yet).
+                unset($lastHeader);
+                $lastHeader = null;
+                $anyRequiredOrError = false;
+                $headercounter = 0;
+                foreach (array_keys($this->_elements) as $elementIndex){
+                    $element =& $this->_elements[$elementIndex];
+                    if ($element->getType()=='header') {
+                        if (!is_null($lastHeader)) {
+                            // Check if we had any required elements or
+                            // we are at the top header that should be expanded by default.
+                            if ($anyRequiredOrError || $headercounter === 1) {
+                                $this->setExpanded($lastHeader->getName());
+                            } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) {
+                                // Define element as collapsed by default.
+                                $this->setExpanded($lastHeader->getName(), false);
+                            }
+                        }
+                        $headercounter++;
+                        $lastHeader =& $element;
+                        $anyRequiredOrError = false;
+                    } elseif (in_array($element->getName(), $this->_required) || isset($this->_errors[$element->getName()])) {
+                        $anyRequiredOrError = true;
+                    }
+                }
+                // Process very last header.
+                if (!is_null($lastHeader)){
+                    // Check if we had any required elements or
+                    // we are at the top header that should be expanded by default.
+                    if ($anyRequiredOrError || $headercounter === 1) {
+                        $this->setExpanded($lastHeader->getName());
+                    } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) {
+                        // Define element as collapsed by default.
+                        $this->setExpanded($lastHeader->getName(), false);
+                    }
+                }
+            }
+            // Pass the array to renderer object.
+            $renderer->setCollapsibleElements($this->_collapsibleElements, $this->getAttribute('id'));
         }
         parent::accept($renderer);
     }
@@ -2225,10 +2326,10 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
 
     /** @var string Header Template string */
     var $_headerTemplate =
-       "\n\t\t<legend class=\"ftoggler\">{header}</legend>\n\t\t<div class=\"advancedbutton\">{advancedimg}{button}</div><div class=\"fcontainer clearfix\">\n\t\t";
+       "\n\t\t<legend class=\"ftoggler\">{header}</legend>\n\t\t<div class=\"fcontainer clearfix\">\n\t\t";
 
     /** @var string Template used when opening a fieldset */
-    var $_openFieldsetTemplate = "\n\t<fieldset class=\"clearfix\" {id}>";
+    var $_openFieldsetTemplate = "\n\t<fieldset class=\"{classes}\" {id}>";
 
     /** @var string Template used when closing a fieldset */
     var $_closeFieldsetTemplate = "\n\t\t</div></fieldset>";
@@ -2236,12 +2337,19 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
     /** @var string Required Note template string */
     var $_requiredNoteTemplate =
         "\n\t\t<div class=\"fdescription required\">{requiredNote}</div>";
-
-    /** @var array list of elements which are marked as advance and will be grouped together */
+    /**
+     * Array whose keys are element names. If the key exists this is a advanced element
+     *
+     * @var array
+     */
     var $_advancedElements = array();
 
-    /** @var int Whether to display advanced elements (on page load) 1 => show, 0 => hide */
-    var $_showAdvanced;
+    /**
+     * Array whose keys are element names and the the boolean values reflect the current state. If the key exists this is a collapsible element.
+     *
+     * @var array
+     */
+    var $_collapsibleElements = array();
 
     /**
      * Constructor
@@ -2274,6 +2382,15 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         $this->_advancedElements = $elements;
     }
 
+    /**
+     * Setting collapsible elements
+     *
+     * @param array $elements
+     */
+    function setCollapsibleElements($elements) {
+        $this->_collapsibleElements = $elements;
+    }
+
     /**
      * What to do when starting the form
      *
@@ -2284,7 +2401,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         $this->_reqHTML = $form->getReqHTML();
         $this->_elementTemplates = str_replace('{req}', $this->_reqHTML, $this->_elementTemplates);
         $this->_advancedHTML = $form->getAdvancedHTML();
-        $this->_showAdvanced = $form->getShowAdvanced();
+        $formid = $form->getAttribute('id');
         parent::startForm($form);
         if ($form->isFrozen()){
             $this->_formTemplate = "\n<div class=\"mform frozen\">\n{content}\n</div>";
@@ -2297,11 +2414,18 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $PAGE->requires->yui_module('moodle-core-formchangechecker',
                     'M.core_formchangechecker.init',
                     array(array(
-                        'formid' => $form->getAttribute('id')
+                        'formid' => $formid
                     ))
             );
             $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
         }
+        if (count($this->_collapsibleElements)) {
+            $PAGE->requires->yui_module('moodle-form-shortforms', 'M.form.shortforms', array(array('formid' => $formid)));
+        }
+        if (!empty($this->_advancedElements)){
+            $PAGE->requires->strings_for_js(array('showmore', 'showless'), 'form');
+            $PAGE->requires->yui_module('moodle-form-showadvanced', 'M.form.showadvanced', array(array('formid' => $formid)));
+        }
     }
 
     /**
@@ -2321,13 +2445,9 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $html = $this->_elementTemplates['default'];
 
         }
-        if ($this->_showAdvanced){
-            $advclass = ' advanced';
-        } else {
-            $advclass = ' advanced hide';
-        }
+
         if (isset($this->_advancedElements[$group->getName()])){
-            $html =str_replace(' {advanced}', $advclass, $html);
+            $html =str_replace(' {advanced}', ' advanced', $html);
             $html =str_replace('{advancedimg}', $this->_advancedHTML, $html);
         } else {
             $html =str_replace(' {advanced}', '', $html);
@@ -2354,6 +2474,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         }
         parent::startGroup($group, $required, $error);
     }
+
     /**
      * Renders element
      *
@@ -2376,13 +2497,8 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         }else{
             $html = $this->_elementTemplates['default'];
         }
-        if ($this->_showAdvanced){
-            $advclass = ' advanced';
-        } else {
-            $advclass = ' advanced hide';
-        }
         if (isset($this->_advancedElements[$element->getName()])){
-            $html =str_replace(' {advanced}', $advclass, $html);
+            $html =str_replace(' {advanced}', ' advanced', $html);
         } else {
             $html =str_replace(' {advanced}', '', $html);
         }
@@ -2451,38 +2567,27 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $header_html = str_replace('{header}', $header->toHtml(), $this->_headerTemplate);
         }
 
-        if (isset($this->_advancedElements[$name])){
-            $header_html =str_replace('{advancedimg}', $this->_advancedHTML, $header_html);
-            $elementName='mform_showadvanced';
-            if ($this->_showAdvanced==0){
-                $buttonlabel = get_string('showadvanced', 'form');
-            } else {
-                $buttonlabel = get_string('hideadvanced', 'form');
-            }
-            $button = '<input name="'.$elementName.'" class="showadvancedbtn" value="'.$buttonlabel.'" type="submit" />';
-            $PAGE->requires->js_init_call('M.form.initShowAdvanced', array(), false, moodleform::get_js_module());
-            $header_html = str_replace('{button}', $button, $header_html);
-        } else {
-            $header_html =str_replace('{advancedimg}', '', $header_html);
-            $header_html = str_replace('{button}', '', $header_html);
-        }
-
         if ($this->_fieldsetsOpen > 0) {
             $this->_html .= $this->_closeFieldsetTemplate;
             $this->_fieldsetsOpen--;
         }
 
-        $openFieldsetTemplate = str_replace('{id}', $id, $this->_openFieldsetTemplate);
-        if ($this->_showAdvanced){
-            $advclass = ' class="advanced"';
-        } else {
-            $advclass = ' class="advanced hide"';
+        // Define collapsible classes for fieldsets.
+        $fieldsetclasses = array('clearfix');
+        if (isset($this->_collapsibleElements[$name])) {
+            $fieldsetclasses[] = 'collapsible';
+            if ($this->_collapsibleElements[$name]) {
+                $fieldsetclasses[] = 'collapsed';
+            }
         }
+
         if (isset($this->_advancedElements[$name])){
-            $openFieldsetTemplate = str_replace('{advancedclass}', $advclass, $openFieldsetTemplate);
-        } else {
-            $openFieldsetTemplate = str_replace('{advancedclass}', '', $openFieldsetTemplate);
+            $fieldsetclasses[] = 'containsadvancedelements';
         }
+
+        $openFieldsetTemplate = str_replace('{id}', $id, $this->_openFieldsetTemplate);
+        $openFieldsetTemplate = str_replace('{classes}', join(' ', $fieldsetclasses), $openFieldsetTemplate);
+
         $this->_html .= $openFieldsetTemplate . $header_html;
         $this->_fieldsetsOpen++;
     }
@@ -2582,6 +2687,7 @@ MoodleQuickForm::registerElementType('group', "$CFG->libdir/form/group.php", 'Mo
 MoodleQuickForm::registerElementType('header', "$CFG->libdir/form/header.php", 'MoodleQuickForm_header');
 MoodleQuickForm::registerElementType('hidden', "$CFG->libdir/form/hidden.php", 'MoodleQuickForm_hidden');
 MoodleQuickForm::registerElementType('htmleditor', "$CFG->libdir/form/htmleditor.php", 'MoodleQuickForm_htmleditor');
+MoodleQuickForm::registerElementType('listing', "$CFG->libdir/form/listing.php", 'MoodleQuickForm_listing');
 MoodleQuickForm::registerElementType('modgrade', "$CFG->libdir/form/modgrade.php", 'MoodleQuickForm_modgrade');
 MoodleQuickForm::registerElementType('modvisible', "$CFG->libdir/form/modvisible.php", 'MoodleQuickForm_modvisible');
 MoodleQuickForm::registerElementType('password', "$CFG->libdir/form/password.php", 'MoodleQuickForm_password');
index 9ebc9b5..dcdb3c8 100644 (file)
@@ -35,7 +35,7 @@ require_once($CFG->libdir . '/gradelib.php');
  * category1 => array(category2 => array(grade_item1, grade_item2), category3 => array(grade_item3))
  * 3 users for 3 grade_items
  */
-class grade_base_testcase extends advanced_testcase {
+abstract class grade_base_testcase extends advanced_testcase {
 
     protected $course;
     protected $activities = array();
index 7b8cca2..30e1297 100644 (file)
@@ -233,7 +233,10 @@ function install_generate_configphp($database, $cfg) {
     }
     $configphp .= '$CFG->directorypermissions = ' . $chmod . ';' . PHP_EOL . PHP_EOL;
 
-    $configphp .= '$CFG->passwordsaltmain = '.var_export(complex_random_string(), true) . ';' . PHP_EOL . PHP_EOL;
+    // A site-wide salt is only needed if bcrypt is not properly supported by the current version of PHP.
+    if (password_compat_not_supported()) {
+        $configphp .= '$CFG->passwordsaltmain = '.var_export(complex_random_string(), true) . ';' . PHP_EOL . PHP_EOL;
+    }
 
     $configphp .= 'require_once(dirname(__FILE__) . \'/lib/setup.php\');' . PHP_EOL . PHP_EOL;
     $configphp .= '// There is no php closing tag in this file,' . PHP_EOL;
index d709396..b1e2ed4 100644 (file)
@@ -1480,131 +1480,27 @@ M.util.help_popups = {
     }
 }
 
+/**
+ * This code bas been deprecated and will be removed from Moodle 2.7
+ *
+ * Please see lib/yui/popuphelp/popuphelp.js for its replacement
+ */
 M.util.help_icon = {
-    Y : null,
-    instance : null,
     initialised : false,
-    setup : function(Y) {
-        if (this.initialised) {
-            // Exit early if we have already completed setup
-            return;
-        }
-        this.Y = Y;
-        Y.one('body').delegate('click', this.display, 'span.helplink a.tooltip', this);
-        this.initialised = true;
-    },
-    add : function(Y, properties) {
-        this.setup(Y);
+    setup : function(Y, properties) {
+        this.add(Y, properties);
     },
-    display : function(event) {
-        event.preventDefault();
-        if (M.util.help_icon.instance === null) {
-            var Y = M.util.help_icon.Y;
-            Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', 'escape', function(Y) {
-                var help_content_overlay = {
-                    helplink : null,
-                    overlay : null,
-                    init : function() {
-
-                        var strclose = Y.Escape.html(M.str.form.close);
-                        var footerbtn = Y.Node.create('<button class="closebtn">'+strclose+'</button>');
-                        // Create an overlay from markup
-                        this.overlay = new Y.Overlay({
-                            footerContent: footerbtn,
-                            bodyContent: '',
-                            id: 'helppopupbox',
-                            width:'400px',
-                            visible : false,
-                            constrain : true
-                        });
-                        this.overlay.render(Y.one(document.body));
-
-                        footerbtn.on('click', this.close, this);
-
-                        var boundingBox = this.overlay.get("boundingBox");
-
-                        //  Hide the menu if the user clicks outside of its content
-                        boundingBox.get("ownerDocument").on("mousedown", function (event) {
-                            var oTarget = event.target;
-                            var menuButton = this.helplink;
-
-                            if (!oTarget.compareTo(menuButton) &&
-                                !menuButton.contains(oTarget) &&
-                                !oTarget.compareTo(boundingBox) &&
-                                !boundingBox.contains(oTarget)) {
-                                this.overlay.hide();
-                            }
-                        }, this);
-                    },
-
-                    close : function(e) {
-                        e.preventDefault();
-                        this.helplink.focus();
-                        this.overlay.hide();
-                    },
-
-                    display : function(event) {
-                        var overlayPosition;
-                        this.helplink = event.target.ancestor('span.helplink a', true);
-                        if (Y.one('html').get('dir') === 'rtl') {
-                            overlayPosition = [Y.WidgetPositionAlign.TR, Y.WidgetPositionAlign.LC];
-                        } else {
-                            overlayPosition = [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC];
-                        }
-
-                        this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
-                        this.overlay.set("align", {node:this.helplink, points: overlayPosition});
-
-                        var cfg = {
-                            method: 'get',
-                            context : this,
-                            data : {
-                                ajax : 1
-                            },
-                            on: {
-                                success: function(id, o, node) {
-                                    this.display_callback(o.responseText);
-                                },
-                                failure: function(id, o, node) {
-                                    var debuginfo = o.statusText;
-                                    if (M.cfg.developerdebug) {
-                                        o.statusText += ' (' + ajaxurl + ')';
-                                    }
-                                    this.display_callback('bodyContent',debuginfo);
-                                }
-                            }
-                        };
-
-                        Y.io(this.helplink.get('href'), cfg);
-                        this.overlay.show();
-                    },
-
-                    display_callback : function(content) {
-                        var contentnode, heading;
-                        contentnode = Y.Node.create('<div role="alert">' + content + '</div>');
-                        this.overlay.set('bodyContent', contentnode);
-                        heading = contentnode.one('h2');
-                        if (heading) {
-                            heading.set('tabIndex', 0);
-                            heading.focus();
-                        }
-                    },
-
-                    hideContent : function() {
-                        help = this;
-                        help.overlay.hide();
-                    }
-                };
-                help_content_overlay.init();
-                M.util.help_icon.instance = help_content_overlay;
-                M.util.help_icon.instance.display(event);
+    add : function(Y) {
+        if (M.cfg.developerdebug) {
+            Y.log("You are using a deprecated function call (M.util.help_icon.add). " +
+                    "Please look at rewriting your call to support lib/yui/popuphelp/popuphelp.js");
+        }
+        if (!this.initialised) {
+            YUI().use('moodle-core-popuphelp', function() {
+                M.core.init_popuphelp([]);
             });
-        } else {
-            M.util.help_icon.instance.display(event);
         }
-    },
-    init : function(Y) {
-        this.Y = Y;
+        this.initialised = true;
     }
 };
 
index cd7b102..eca1097 100644 (file)
@@ -116,7 +116,11 @@ define('PARAM_BOOL',     'bool');
 define('PARAM_CAPABILITY',   'capability');
 
 /**
- * PARAM_CLEANHTML - cleans submitted HTML code. use only for text in HTML format. This cleaning may fix xhtml strictness too.
+ * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
+ * to use this. The normal mode of operation is to use PARAM_RAW when recieving
+ * the input (required/optional_param or formslib) and then sanitse the HTML
+ * using format_text on output. This is for the rare cases when you want to
+ * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
  */
 define('PARAM_CLEANHTML', 'cleanhtml');
 
@@ -493,6 +497,11 @@ define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 define('COURSE_DISPLAY_SINGLEPAGE', 0); // display all sections on one page
 define('COURSE_DISPLAY_MULTIPAGE', 1); // split pages into a page per section
 
+/**
+ * Authentication constants.
+ */
+define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); // String used in password field when password is not stored.
+
 /// PARAMETER HANDLING ////////////////////////////////////////////////////
 
 /**
@@ -3845,6 +3854,7 @@ function create_user_record($username, $password, $auth = 'manual') {
     if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
         set_user_preference('auth_forcepasswordchange', 1, $user);
     }
+    // Set the password.
     update_internal_user_password($user, $password);
 
     // fetch full user record for the event, the complete user data contains too much info
@@ -4197,7 +4207,10 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
                 $user->auth = $auth;
             }
 
-            update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
+            // If the existing hash is using an out-of-date algorithm (or the
+            // legacy md5 algorithm), then we should update to the current
+            // hash algorithm while we have access to the user's password.
+            update_internal_user_password($user, $password);
 
             if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
                 $user = update_user_record($username);
@@ -4307,28 +4320,81 @@ function complete_user_login($user) {
 }
 
 /**
- * Compare password against hash stored in internal user table.
- * If necessary it also updates the stored hash to new format.
+ * Check a password hash to see if it was hashed using the
+ * legacy hash algorithm (md5).
+ *
+ * @param string $password String to check.
+ * @return boolean True if the $password matches the format of an md5 sum.
+ */
+function password_is_legacy_hash($password) {
+    return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
+}
+
+/**
+ * Checks whether the password compatibility library will work with the current
+ * version of PHP. This cannot be done using PHP version numbers since the fix
+ * has been backported to earlier versions in some distributions.
+ *
+ * See https://github.com/ircmaxell/password_compat/issues/10 for
+ * more details.
+ *
+ * @return bool True if the library is NOT supported.
+ */
+function password_compat_not_supported() {
+
+    $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
+
+    // Create a one off application cache to store bcrypt support status as
+    // the support status doesn't change and crypt() is slow.
+    $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
+
+    if (!$bcryptsupport = $cache->get('bcryptsupport')) {
+        $test = crypt('password', $hash);
+        // Cache string instead of boolean to avoid MDL-37472.
+        if ($test == $hash) {
+            $bcryptsupport = 'supported';
+        } else {
+            $bcryptsupport = 'not supported';
+        }
+        $cache->set('bcryptsupport', $bcryptsupport);
+    }
+
+    // Return true if bcrypt *not* supported.
+    return ($bcryptsupport !== 'supported');
+}
+
+/**
+ * Compare password against hash stored in user object to determine if it is valid.
  *
- * @param stdClass $user (password property may be updated)
- * @param string $password plain text password
- * @return bool is password valid?
+ * If necessary it also updates the stored hash to the current format.
+ *
+ * @param stdClass $user (Password property may be updated).
+ * @param string $password Plain text password.
+ * @return bool True if password is valid.
  */
 function validate_internal_user_password($user, $password) {
     global $CFG;
+    require_once($CFG->libdir.'/password_compat/lib/password.php');
 
-    if (!isset($CFG->passwordsaltmain)) {
-        $CFG->passwordsaltmain = '';
+    if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
+        // Internal password is not used at all, it can not validate.
+        return false;
     }
 
-    $validated = false;
+    // If hash isn't a legacy (md5) hash, validate using the library function.
+    if (!password_is_legacy_hash($user->password)) {
+        return password_verify($password, $user->password);
+    }
+
+    // Otherwise we need to check for a legacy (md5) hash instead. If the hash
+    // is valid we can then update it to the new algorithm.
 
-    if ($user->password === 'not cached') {
-        // internal password is not used at all, it can not validate
+    $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
+    $validated = false;
 
-    } else if ($user->password === md5($password.$CFG->passwordsaltmain)
+    if ($user->password === md5($password.$sitesalt)
             or $user->password === md5($password)
-            or $user->password === md5(addslashes($password).$CFG->passwordsaltmain)
+            or $user->password === md5(addslashes($password).$sitesalt)
             or $user->password === md5(addslashes($password))) {
         // note: we are intentionally using the addslashes() here because we
         //       need to accept old password hashes of passwords with magic quotes
@@ -4347,7 +4413,8 @@ function validate_internal_user_password($user, $password) {
     }
 
     if ($validated) {
-        // force update of password hash using latest main password salt and encoding if needed
+        // If the password matches the existing md5 hash, update to the
+        // current hash algorithm while we have access to the user's password.
         update_internal_user_password($user, $password);
     }
 
@@ -4355,39 +4422,85 @@ function validate_internal_user_password($user, $password) {
 }
 
 /**
- * Calculate hashed value from password using current hash mechanism.
+ * Calculate hash for a plain text password.
  *
- * @param string $password
- * @return string password hash
+ * @param string $password Plain text password to be hashed.
+ * @param bool $fasthash If true, use a low cost factor when generating the hash
+ *                       This is much faster to generate but makes the hash
+ *                       less secure. It is used when lots of hashes need to
+ *                       be generated quickly.
+ * @return string The hashed password.
+ *
+ * @throws moodle_exception If a problem occurs while generating the hash.
  */
-function hash_internal_user_password($password) {
+function hash_internal_user_password($password, $fasthash = false) {
     global $CFG;
+    require_once($CFG->libdir.'/password_compat/lib/password.php');
 
-    if (isset($CFG->passwordsaltmain)) {
-        return md5($password.$CFG->passwordsaltmain);
-    } else {
-        return md5($password);
+    // Use the legacy hashing algorithm (md5) if PHP is not new enough
+    // to support bcrypt properly
+    if (password_compat_not_supported()) {
+        if (isset($CFG->passwordsaltmain)) {
+            return md5($password.$CFG->passwordsaltmain);
+        } else {
+            return md5($password);
+        }
+    }
+
+    // Set the cost factor to 4 for fast hashing, otherwise use default cost.
+    $options = ($fasthash) ? array('cost' => 4) : array();
+
+    $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
+
+    if ($generatedhash === false) {
+        throw new moodle_exception('Failed to generate password hash.');
     }
+
+    return $generatedhash;
 }
 
 /**
- * Update password hash in user object.
+ * Update password hash in user object (if necessary).
  *
- * @param stdClass $user (password property may be updated)
- * @param string $password plain text password
- * @return bool always returns true
+ * The password is updated if:
+ * 1. The password has changed (the hash of $user->password is different
+ *    to the hash of $password).
+ * 2. The existing hash is using an out-of-date algorithm (or the legacy
+ *    md5 algorithm).
+ *
+ * Updating the password will modify the $user object and the database
+ * record to use the current hashing algorithm.
+ *
+ * @param stdClass $user User object (password property may be updated).
+ * @param string $password Plain text password.
+ * @return bool Always returns true.
  */
 function update_internal_user_password($user, $password) {
-    global $DB;
+    global $CFG, $DB;
+    require_once($CFG->libdir.'/password_compat/lib/password.php');
+
+    // Use the legacy hashing algorithm (md5) if PHP doesn't support
+    // bcrypt properly.
+    $legacyhash = password_compat_not_supported();
 
+    // Figure out what the hashed password should be.
     $authplugin = get_auth_plugin($user->auth);
     if ($authplugin->prevent_local_passwords()) {
-        $hashedpassword = 'not cached';
+        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
     } else {
         $hashedpassword = hash_internal_user_password($password);
     }
 
-    if ($user->password !== $hashedpassword) {
+    if ($legacyhash) {
+        $passwordchanged = ($user->password !== $hashedpassword);
+        $algorithmchanged = false;
+    } else {
+        // If verification fails then it means the password has changed.
+        $passwordchanged = !password_verify($password, $user->password);
+        $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
+    }
+
+    if ($passwordchanged || $algorithmchanged) {
         $DB->set_field('user', 'password',  $hashedpassword, array('id'=>$user->id));
         $user->password = $hashedpassword;
     }
@@ -5588,9 +5701,10 @@ function generate_email_supportuser() {
  * @global object
  * @global object
  * @param user $user A {@link $USER} object
+ * @param boolean $fasthash If true, use a low cost factor when generating the hash for speed.
  * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
  */
-function setnew_password_and_mail($user) {
+function setnew_password_and_mail($user, $fasthash = false) {
     global $CFG, $DB;
 
     // we try to send the mail in language the user understands,
@@ -5604,7 +5718,8 @@ function setnew_password_and_mail($user) {
 
     $newpassword = generate_password();
 
-    $DB->set_field('user', 'password', hash_internal_user_password($newpassword), array('id'=>$user->id));
+    $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
+    $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
 
     $a = new stdClass();
     $a->firstname   = fullname($user, true);
@@ -6022,7 +6137,10 @@ function get_user_max_upload_file_size($context, $sitebytes=0, $coursebytes=0, $
  * array of possible sizes in an array, translated to the
  * local language.
  *
- * @todo Finish documenting this function
+ * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
+ *
+ * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
+ * with the value set to 0. This option will be the first in the list.
  *
  * @global object
  * @uses SORT_NUMERIC
@@ -6041,7 +6159,7 @@ function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0
     }
 
     $filesize = array();
-    $filesize[intval($maxsize)] = display_size($maxsize);
+    $filesize[(string)intval($maxsize)] = display_size($maxsize);
 
     $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
                       5242880, 10485760, 20971520, 52428800, 104857600);
@@ -6063,12 +6181,31 @@ function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0
     }
 
     foreach ($sizelist as $sizebytes) {
-       if ($sizebytes < $maxsize) {
-           $filesize[intval($sizebytes)] = display_size($sizebytes);
+       if ($sizebytes < $maxsize && $sizebytes > 0) {
+           $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
        }
     }
 
     krsort($filesize, SORT_NUMERIC);
+    $limitlevel = '';
+    $displaysize = '';
+    if ($modulebytes &&
+        (($modulebytes < $coursebytes || $coursebytes == 0) &&
+         ($modulebytes < $sitebytes || $sitebytes == 0))) {
+        $limitlevel = get_string('activity', 'core');
+        $displaysize = display_size($modulebytes);
+    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
+        $limitlevel = get_string('course', 'core');
+        $displaysize = display_size($coursebytes);
+    } else if ($sitebytes) {
+        $limitlevel = get_string('site', 'core');
+        $displaysize = display_size($sitebytes);
+    }
+
+    if ($limitlevel) {
+        $params = (object) array('contextname'=>$limitlevel, 'displaysize'=>$displaysize);
+        $filesize  = array('0'=>get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
+    }
 
     return $filesize;
 }
index fb4808c..4e0545b 100644 (file)
@@ -364,9 +364,16 @@ class core_renderer extends renderer_base {
         // flow player embedding support
         $this->page->requires->js_function_call('M.util.load_flowplayer');
 
-        // Set up help link popups for all links with the helplinkpopup class
+        // Set up help link popups for all links with the helptooltip class
         $this->page->requires->js_init_call('M.util.help_popups.setup');
 
+        // Setup help icon overlays.
+        $this->page->requires->yui_module('moodle-core-popuphelp', 'M.core.init_popuphelp');
+        $this->page->requires->strings_for_js(array(
+            'morehelp',
+            'loadinghelp',
+        ), 'moodle');
+
         $this->page->requires->js_function_call('setTimeout', array('fix_column_widths()', 20));
 
         $focus = $this->page->focuscontrol;
@@ -1932,7 +1939,7 @@ class core_renderer extends renderer_base {
         $this->page->requires->string_for_js('close', 'form');
 
         // and finally span
-        return html_writer::tag('span', $output, array('class' => 'helplink'));
+        return html_writer::tag('span', $output, array('class' => 'helptooltip'));
     }
 
     /**
@@ -1989,14 +1996,11 @@ class core_renderer extends renderer_base {
         // note: this title is displayed only if JS is disabled, otherwise the link will have the new ajax tooltip
         $title = get_string('helpprefix2', '', trim($title, ". \t"));
 
-        $attributes = array('href'=>$url, 'title'=>$title, 'aria-haspopup' => 'true', 'class' => 'tooltip');
+        $attributes = array('href' => $url, 'title' => $title, 'aria-haspopup' => 'true');
         $output = html_writer::tag('a', $output, $attributes);
 
-        $this->page->requires->js_init_call('M.util.help_icon.setup');
-        $this->page->requires->string_for_js('close', 'form');
-
         // and finally span
-        return html_writer::tag('span', $output, array('class' => 'helplink'));
+        return html_writer::tag('span', $output, array('class' => 'helptooltip'));
     }
 
     /**
index 941cb1e..89d509f 100644 (file)
@@ -423,7 +423,7 @@ class page_requirements_manager {
                     $module = array('name'     => 'core_dock',
                                     'fullpath' => '/blocks/dock.js',
                                     'requires' => array('base', 'node', 'event-custom', 'event-mouseenter', 'event-resize'),
-                                    'strings' => array(array('addtodock', 'block'),array('undockitem', 'block'),array('undockall', 'block'),array('thisdirectionvertical', 'langconfig'),array('hidedockpanel', 'block'),array('hidepanel', 'block')));
+                                    'strings' => array(array('addtodock', 'block'),array('undockitem', 'block'),array('undockblock', 'block'),array('undockall', 'block'),array('thisdirectionvertical', 'langconfig'),array('hidedockpanel', 'block'),array('hidepanel', 'block')));
                     break;
                 case 'core_message':
                     $module = array('name'     => 'core_message',
diff --git a/lib/password_compat/lib/password.php b/lib/password_compat/lib/password.php
new file mode 100644 (file)
index 0000000..6732e9f
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ */
+
+if (!defined('PASSWORD_BCRYPT')) {
+
+       define('PASSWORD_BCRYPT', 1);
+       define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+
+       /**
+        * Hash the password using the specified algorithm
+        *
+        * @param string $password The password to hash
+        * @param int    $algo     The algorithm to use (Defined by PASSWORD_* constants)
+        * @param array  $options  The options for the algorithm to use
+        *
+        * @return string|false The hashed password, or false on error.
+        */
+       function password_hash($password, $algo, array $options = array()) {
+               if (!function_exists('crypt')) {
+                       trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+                       return null;
+               }
+               if (!is_string($password)) {
+                       trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+                       return null;
+               }
+               if (!is_int($algo)) {
+                       trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+                       return null;
+               }
+               switch ($algo) {
+                       case PASSWORD_BCRYPT:
+                               // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
+                               $cost = 10;
+                               if (isset($options['cost'])) {
+                                       $cost = $options['cost'];
+                                       if ($cost < 4 || $cost > 31) {
+                                               trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+                                               return null;
+                                       }
+                               }
+                               $required_salt_len = 22;
+                               $hash_format = sprintf("$2y$%02d$", $cost);
+                               break;
+                       default:
+                               trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+                               return null;
+               }
+               if (isset($options['salt'])) {
+                       switch (gettype($options['salt'])) {
+                               case 'NULL':
+                               case 'boolean':
+                               case 'integer':
+                               case 'double':
+                               case 'string':
+                                       $salt = (string) $options['salt'];
+                                       break;
+                               case 'object':
+                                       if (method_exists($options['salt'], '__tostring')) {
+                                               $salt = (string) $options['salt'];
+                                               break;
+                                       }
+                               case 'array':
+                               case 'resource':
+                               default:
+                                       trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
+                                       return null;
+                       }
+                       if (strlen($salt) < $required_salt_len) {
+                               trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
+                               return null;
+                       } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+                               $salt = str_replace('+', '.', base64_encode($salt));
+                       }
+               } else {
+                       $buffer = '';
+                       $raw_length = (int) ($required_salt_len * 3 / 4 + 1);
+                       $buffer_valid = false;
+                       if (function_exists('mcrypt_create_iv')) {
+                               $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
+                               if ($buffer) {
+                                       $buffer_valid = true;
+                               }
+                       }
+                       if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+                               $buffer = openssl_random_pseudo_bytes($raw_length);
+                               if ($buffer) {
+                                       $buffer_valid = true;
+                               }
+                       }
+                       if (!$buffer_valid && file_exists('/dev/urandom')) {
+                               $f = @fopen('/dev/urandom', 'r');
+                               if ($f) {
+                                       $read = strlen($buffer);
+                                       while ($read < $raw_length) {
+                                               $buffer .= fread($f, $raw_length - $read);
+                                               $read = strlen($buffer);
+                                       }
+                                       fclose($f);
+                                       if ($read >= $raw_length) {
+                                               $buffer_valid = true;
+                                       }
+                               }
+                       }
+                       if (!$buffer_valid || strlen($buffer) < $raw_length) {
+                               $bl = strlen($buffer);
+                               for ($i = 0; $i < $raw_length; $i++) {
+                                       if ($i < $bl) {
+                                               $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+                                       } else {
+                                               $buffer .= chr(mt_rand(0, 255));
+                                       }
+                               }
+                       }
+                       $salt = str_replace('+', '.', base64_encode($buffer));
+
+               }
+               $salt = substr($salt, 0, $required_salt_len);
+
+               $hash = $hash_format . $salt;
+
+               $ret = crypt($password, $hash);
+
+               if (!is_string($ret) || strlen($ret) <= 13) {
+                       return false;
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Get information about the password hash. Returns an array of the information
+        * that was used to generate the password hash.
+        *
+        * array(
+        *    'algo' => 1,
+        *    'algoName' => 'bcrypt',
+        *    'options' => array(
+        *        'cost' => 10,
+        *    ),
+        * )
+        *
+        * @param string $hash The password hash to extract info from
+        *
+        * @return array The array of information about the hash.
+        */
+       function password_get_info($hash) {
+               $return = array(
+                       'algo' => 0,
+                       'algoName' => 'unknown',
+                       'options' => array(),
+               );
+               if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
+                       $return['algo'] = PASSWORD_BCRYPT;
+                       $return['algoName'] = 'bcrypt';
+                       list($cost) = sscanf($hash, "$2y$%d$");
+                       $return['options']['cost'] = $cost;
+               }
+               return $return;
+       }
+
+       /**
+        * Determine if the password hash needs to be rehashed according to the options provided
+        *
+        * If the answer is true, after validating the password using password_verify, rehash it.
+        *
+        * @param string $hash    The hash to test
+        * @param int    $algo    The algorithm used for new password hashes
+        * @param array  $options The options array passed to password_hash
+        *
+        * @return boolean True if the password needs to be rehashed.
+        */
+       function password_needs_rehash($hash, $algo, array $options = array()) {
+               $info = password_get_info($hash);
+               if ($info['algo'] != $algo) {
+                       return true;
+               }
+               switch ($algo) {
+                       case PASSWORD_BCRYPT:
+                               $cost = isset($options['cost']) ? $options['cost'] : 10;
+                               if ($cost != $info['options']['cost']) {
+                                       return true;
+                               }
+                               break;
+               }
+               return false;
+       }
+
+       /**
+        * Verify a password against a hash using a timing attack resistant approach
+        *
+        * @param string $password The password to verify
+        * @param string $hash     The hash to verify against
+        *
+        * @return boolean If the password matches the hash
+        */
+    function password_verify($password, $hash) {
+               if (!function_exists('crypt')) {
+                       trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
+                       return false;
+               }
+               $ret = crypt($password, $hash);
+               if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
+                       return false;
+               }
+
+               $status = 0;
+               for ($i = 0; $i < strlen($ret); $i++) {
+                       $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+               }
+
+               return $status === 0;
+       }
+}
diff --git a/lib/password_compat/readme_moodle.txt b/lib/password_compat/readme_moodle.txt
new file mode 100644 (file)
index 0000000..9740c2e
--- /dev/null
@@ -0,0 +1,37 @@
+Description of password_compat import into Moodle:
+==================================================
+
+Imported from: https://github.com/ircmaxell/password_compat/commit/2a7b6355d27c65f7e0de1fbbc0016b5b6cd8226b
+Copyright: (c) 2012 Anthony Ferrara
+License: MIT License
+
+Removed:
+* README.md, LICENSE.md and composer.json files.
+* bootstrap.php and phpunit.xml.dist files from test directory.
+
+Added:
+* None.
+
+Our changes:
+* Moved tests from test/Unit/ to tests/ directory.
+* Removed tabs and trailing whitespace from test files.
+* Added markTestSkipped() check to tests so they only run if password_compat is supported
+
+Moodle commit history:
+======================
+
+MDL-35332   Initial commit
+
+
+Library description:
+====================
+
+Compatibility with the password_* functions being worked on for PHP 5.5.
+
+This library requires PHP >= 5.3.7 due to a PHP security issue prior to that
+version.
+
+See the RFC (https://wiki.php.net/rfc/password_hash) for more information.
+
+Latest code available from https://github.com/ircmaxell/password_compat/
+under MIT license.
diff --git a/lib/password_compat/tests/PasswordGetInfoTest.php b/lib/password_compat/tests/PasswordGetInfoTest.php
new file mode 100644 (file)
index 0000000..215cd3c
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+global $CFG;
+require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
+
+class PasswordGetInfoTest extends PHPUnit_Framework_TestCase {
+
+    protected function setUp() {
+        if (password_compat_not_supported()) {
+            // Skip test if password_compat is not supported.
+            $this->markTestSkipped('password_compat not supported');
+        }
+    }
+
+    public static function provideInfo() {
+        return array(
+            array('foo', array('algo' => 0, 'algoName' => 'unknown', 'options' => array())),
+            array('$2y$', array('algo' => 0, 'algoName' => 'unknown', 'options' => array())),
+            array('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', array('algo' => PASSWORD_BCRYPT, 'algoName' => 'bcrypt', 'options' => array('cost' => 7))),
+            array('$2y$10$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', array('algo' => PASSWORD_BCRYPT, 'algoName' => 'bcrypt', 'options' => array('cost' => 10))),
+
+        );
+    }
+
+    public function testFuncExists() {
+        $this->assertTrue(function_exists('password_get_info'));
+    }
+
+    /**
+     * @dataProvider provideInfo
+     */
+    public function testInfo($hash, $info) {
+        $this->assertEquals($info, password_get_info($hash));
+    }
+
+}
diff --git a/lib/password_compat/tests/PasswordHashTest.php b/lib/password_compat/tests/PasswordHashTest.php
new file mode 100644 (file)
index 0000000..010ea34
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+
+global $CFG;
+require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
+
+class PasswordHashTest extends PHPUnit_Framework_TestCase {
+
+    protected function setUp() {
+        if (password_compat_not_supported()) {
+            // Skip test if password_compat is not supported.
+            $this->markTestSkipped('password_compat not supported');
+        }
+    }
+
+    public function testFuncExists() {
+        $this->assertTrue(function_exists('password_hash'));
+    }
+
+    public function testStringLength() {
+        $this->assertEquals(60, strlen(password_hash('foo', PASSWORD_BCRYPT)));
+    }
+
+    public function testHash() {
+        $hash = password_hash('foo', PASSWORD_BCRYPT);
+        $this->assertEquals($hash, crypt('foo', $hash));
+    }
+
+    public function testKnownSalt() {
+        $hash = password_hash("rasmuslerdorf", PASSWORD_BCRYPT, array("cost" => 7, "salt" => "usesomesillystringforsalt"));
+        $this->assertEquals('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', $hash);
+    }
+
+    public function testRawSalt() {
+        $hash = password_hash("test", PASSWORD_BCRYPT, array("salt" => "123456789012345678901" . chr(0)));
+        $this->assertEquals('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', $hash);
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidAlgo() {
+        password_hash('foo', array());
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidAlgo2() {
+        password_hash('foo', 2);
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidPassword() {
+        password_hash(array(), 1);
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidSalt() {
+        password_hash('foo', PASSWORD_BCRYPT, array('salt' => array()));
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidBcryptCostLow() {
+        password_hash('foo', PASSWORD_BCRYPT, array('cost' => 3));
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidBcryptCostHigh() {
+        password_hash('foo', PASSWORD_BCRYPT, array('cost' => 32));
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidBcryptCostInvalid() {
+        password_hash('foo', PASSWORD_BCRYPT, array('cost' => 'foo'));
+    }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error
+     */
+    public function testInvalidBcryptSaltShort() {
+        password_hash('foo', PASSWORD_BCRYPT, array('salt' => 'abc'));
+    }
+
+}
diff --git a/lib/password_compat/tests/PasswordNeedsRehashTest.php b/lib/password_compat/tests/PasswordNeedsRehashTest.php
new file mode 100644 (file)
index 0000000..f6fa0e3
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+global $CFG;
+require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
+
+class PasswordNeedsRehashTest extends PHPUnit_Framework_TestCase {
+
+    protected function setUp() {
+        if (password_compat_not_supported()) {
+            // Skip test if password_compat is not supported.
+            $this->markTestSkipped('password_compat not supported');
+        }
+    }
+
+    public static function provideCases() {
+        return array(
+            array('foo', 0, array(), false),
+            array('foo', 1, array(), true),
+            array('$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi', PASSWORD_BCRYPT, array(), true),
+            array('$2y$07$usesomesillystringfore2udlvp1ii2e./u9c8sbjqp8i90dh6hi', PASSWORD_BCRYPT, array('cost' => 7), false),
+            array('$2y$07$usesomesillystringfore2udlvp1ii2e./u9c8sbjqp8i90dh6hi', PASSWORD_BCRYPT, array('cost' => 5), true),
+        );
+    }
+
+    public function testFuncExists() {
+        $this->assertTrue(function_exists('password_needs_rehash'));
+    }
+
+    /**
+     * @dataProvider provideCases
+     */
+    public function testCases($hash, $algo, $options, $valid) {
+        $this->assertEquals($valid, password_needs_rehash($hash, $algo, $options));
+    }
+
+}
diff --git a/lib/password_compat/tests/PasswordVerifyTest.php b/lib/password_compat/tests/PasswordVerifyTest.php
new file mode 100644 (file)
index 0000000..80b007b
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+global $CFG;
+require_once($CFG->dirroot . '/lib/password_compat/lib/password.php');
+
+class PasswordVerifyTest extends PHPUnit_Framework_TestCase {
+
+    protected function setUp() {
+        if (password_compat_not_supported()) {
+            // Skip test if password_compat is not supported.
+            $this->markTestSkipped('password_compat not supported');
+        }
+    }
+
+    public function testFuncExists() {
+        $this->assertTrue(function_exists('password_verify'));
+    }
+
+    public function testFailedType() {
+        $this->assertFalse(password_verify(123, 123));
+    }
+
+    public function testSaltOnly() {
+        $this->assertFalse(password_verify('foo', '$2a$07$usesomesillystringforsalt$'));
+    }
+
+    public function testInvalidPassword() {
+        $this->assertFalse(password_verify('rasmusler', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
+    }
+
+    public function testValidPassword() {
+        $this->assertTrue(password_verify('rasmuslerdorf', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi'));
+    }
+
+    public function testInValidHash() {
+        $this->assertFalse(password_verify('rasmuslerdorf', '$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hj'));
+    }
+
+}
index 94031b9..560d7fe 100644 (file)
@@ -189,8 +189,6 @@ error_reporting($CFG->debug);
 ini_set('display_errors', '1');
 ini_set('log_errors', '1');
 
-$CFG->passwordsaltmain = 'phpunit'; // makes login via normal UI impossible
-
 $CFG->noemailever = true; // better not mail anybody from tests, override temporarily if necessary
 $CFG->cachetext = 0; // disable this very nasty setting
 
index 88fd5fd..34ead8c 100644 (file)
@@ -137,14 +137,23 @@ class moodle_exception extends Exception {
 
         if (get_string_manager()->string_exists($errorcode, $module)) {
             $message = get_string($errorcode, $module, $a);
+            $haserrorstring = true;
         } else {
             $message = $module . '/' . $errorcode;
+            $haserrorstring = false;
         }
 
         if (defined('PHPUNIT_TEST') and PHPUNIT_TEST and $debuginfo) {
             $message = "$message ($debuginfo)";
         }
 
+        if (!$haserrorstring and defined('PHPUNIT_TEST') and PHPUNIT_TEST) {
+            // Append the contents of $a to $debuginfo so helpful information isn't lost.
+            // This emulates what {@link get_exception_info()} does. Unfortunately that
+            // function is not used by phpunit.
+            $message .= PHP_EOL.'$a contents: '.print_r($a, true);
+        }
+
         parent::__construct($message, 0);
     }
 }
@@ -1140,8 +1149,8 @@ function disable_output_buffering() {
  */
 function redirect_if_major_upgrade_required() {
     global $CFG;
-    $lastmajordbchanges = 2012110201;
-    if (empty($CFG->version) or (int)$CFG->version < $lastmajordbchanges or
+    $lastmajordbchanges = 2013021100.01;
+    if (empty($CFG->version) or (float)$CFG->version < $lastmajordbchanges or
             during_initial_install() or !empty($CFG->adminsetuppending)) {
         try {
             @session_get_instance()->terminate_current();
index 9cce441..bbd9e17 100644 (file)
@@ -123,7 +123,7 @@ abstract class testing_util {
      *
      * @return bool
      */
-    protected static function is_test_data_updated() {
+    public static function is_test_data_updated() {
         global $CFG;
 
         $framework = self::get_framework();
index 7ab11b1..0afeb63 100644 (file)
@@ -204,7 +204,8 @@ EOD;
         $record['timemodified'] = $record['timecreated'];
         $record['lastip'] = '0.0.0.0';
 
-        $record['password'] = hash_internal_user_password($record['password']);
+        // Use fast hash during testing.
+        $record['password'] = hash_internal_user_password($record['password'], true);
 
         if ($record['deleted']) {
             $delname = $record['email'].'.'.time();
index 2e6d386..d409231 100644 (file)
@@ -222,7 +222,7 @@ class behat_data_generators extends behat_base {
         global $DB;
 
         if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
-            throw new Exception('The specified user with username "' . $username . '" does not exists');
+            throw new Exception('The specified user with username "' . $username . '" does not exist');
         }
         return $id;
     }
@@ -237,7 +237,7 @@ class behat_data_generators extends behat_base {
         global $DB;
 
         if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
-            throw new Exception('The specified role with shortname"' . $roleshortname . '" does not exists');
+            throw new Exception('The specified role with shortname"' . $roleshortname . '" does not exist');
         }
 
         return $id;
@@ -258,7 +258,7 @@ class behat_data_generators extends behat_base {
         }
 
         if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
-            throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exists');
+            throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
         }
 
         return $id;
@@ -274,7 +274,7 @@ class behat_data_generators extends behat_base {
         global $DB;
 
         if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
-            throw new Exception('The specified course with shortname"' . $shortname . '" does not exists');
+            throw new Exception('The specified course with shortname"' . $shortname . '" does not exist');
         }
         return $id;
     }
@@ -289,7 +289,7 @@ class behat_data_generators extends behat_base {
         global $DB;
 
         if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
-            throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exists');
+            throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
         }
         return $id;
     }
@@ -304,7 +304,7 @@ class behat_data_generators extends behat_base {
         global $DB;
 
         if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
-            throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exists');
+            throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
         }
         return $id;
     }
index 972b016..c9df3e9 100644 (file)
@@ -48,19 +48,22 @@ class behat_forms extends behat_base {
     /**
      * Presses button with specified id|name|title|alt|value.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @When /^I press "(?P<button_string>(?:[^"]|\\")*)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function press_button($button) {
         $button = $this->fixStepArgument($button);
-        $this->getSession()->getPage()->pressButton($button);
+
+        // Ensures the button is present.
+        $buttonnode = $this->find_button($button);
+        $buttonnode->press();
     }
 
     /**
      * Fills a moodle form with field/value data.
      *
-     * @throws ElementNotFoundException
      * @Given /^I fill the moodle form with:$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      * @param TableNode $data
      */
     public function i_fill_the_moodle_form_with(TableNode $data) {
@@ -75,21 +78,8 @@ class behat_forms extends behat_base {
             // Removing \\ that escapes " of the steps arguments.
             $locator = $this->fixStepArgument($locator);
 
-            // Finds the element in the page waiting until it appears (or timeouts)
-            // otherwise spin() throws exception.
-            $exception = new ElementNotFoundException(
-                $this->getSession(), 'form field', 'id|name|label|value', $locator
-            );
-
-            // $context is $this and will be passed to the function by spin().
-            $args['locator'] = $locator;
-
-            // Closure to ensure field($locator) exists.
-            $fieldnode = $this->spin(
-                function($context, $args) {
-                    return $context->getSession()->getPage()->findField($args['locator']);
-                }, $exception, $args
-            );
+            // Getting the NodeElement.
+            $fieldnode = $this->find_field($locator);
 
             // Gets the field type from a parent node.
             $field = $this->get_field($fieldnode, $locator);
@@ -103,65 +93,66 @@ class behat_forms extends behat_base {
     /**
      * Fills in form field with specified id|name|label|value.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @When /^I fill in "(?P<field_string>(?:[^"]|\\")*)" with "(?P<value_string>(?:[^"]|\\")*)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function fill_field($field, $value) {
         $field = $this->fixStepArgument($field);
         $value = $this->fixStepArgument($value);
-        $this->getSession()->getPage()->fillField($field, $value);
+
+        $fieldnode = $this->find_field($field);
+        $fieldnode->setValue($value);
     }
 
     /**
      * Selects option in select field with specified id|name|label|value.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
-     * @throws ElementNotFoundException
      * @When /^I select "(?P<option_string>(?:[^"]|\\")*)" from "(?P<select_string>(?:[^"]|\\")*)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function select_option($option, $select) {
         $select = $this->fixStepArgument($select);
         $option = $this->fixStepArgument($option);
 
-        // We add the click event to deal with autosubmit drop down menus.
-        $selectnode = $this->getSession()->getPage()->findField($select);
-        if ($selectnode == null) {
-            throw new ElementNotFoundException(
-                $this->getSession(), 'form field', 'id|name|label|value', $select
-            );
-        }
+        $selectnode = $this->find_field($select);
         $selectnode->selectOption($option);
+
+        // Adding a click as Selenium requires it to fire some JS events.
         $selectnode->click();
     }
 
     /**
      * Checks checkbox with specified id|name|label|value.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @When /^I check "(?P<option_string>(?:[^"]|\\")*)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function check_option($option) {
         $option = $this->fixStepArgument($option);
-        $this->getSession()->getPage()->checkField($option);
+
+        $checkboxnode = $this->find_field($option);
+        $checkboxnode->check();
     }
 
     /**
      * Unchecks checkbox with specified id|name|label|value.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @When /^I uncheck "(?P<option_string>(?:[^"]|\\")*)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function uncheck_option($option) {
         $option = $this->fixStepArgument($option);
-        $this->getSession()->getPage()->uncheckField($option);
+
+        $checkboxnode = $this->find_field($option);
+        $checkboxnode->uncheck();
     }
 
     /**
      * Checks that the form element field have the specified value.
      *
-     * @throws ElementNotFoundException
-     * @throws ExpectationException
      * @Then /^the "(?P<field_string>(?:[^"]|\\")*)" field should match "(?P<value_string>(?:[^"]|\\")*)" value$/
+     * @throws ExpectationException
+     * @throws ElementNotFoundException Thrown by behat_base::find
      * @param mixed $locator
      * @param mixed $value
      */
@@ -170,12 +161,7 @@ class behat_forms extends behat_base {
         $locator = $this->fixStepArgument($locator);
         $value = $this->fixStepArgument($value);
 
-        $fieldnode = $this->getSession()->getPage()->findField($locator);
-        if (null === $fieldnode) {
-            throw new ElementNotFoundException(
-                $this->getSession(), 'form field', 'id|name|label|value', $locator
-            );
-        }
+        $fieldnode = $this->find_field($locator);
 
         // Gets the field instance.
         $field = $this->get_field($fieldnode, $locator);
@@ -192,8 +178,8 @@ class behat_forms extends behat_base {
     /**
      * Checks, that checkbox with specified in|name|label|value is checked.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @Then /^the "(?P<checkbox_string>(?:[^"]|\\")*)" checkbox should be checked$/
+     * @see Behat\MinkExtension\Context\MinkContext
      */
     public function assert_checkbox_checked($checkbox) {
         $checkbox = $this->fixStepArgument($checkbox);
@@ -203,8 +189,8 @@ class behat_forms extends behat_base {
     /**
      * Checks, that checkbox with specified in|name|label|value is unchecked.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @Then /^the "(?P<checkbox_string>(?:[^"]|\\")*)" checkbox should not be checked$/
+     * @see Behat\MinkExtension\Context\MinkContext
      */
     public function assert_checkbox_not_checked($checkbox) {
         $checkbox = $this->fixStepArgument($checkbox);
@@ -214,9 +200,9 @@ class behat_forms extends behat_base {
     /**
      * Checks, that given select box contains the specified option.
      *
-     * @throws ExpectationException
-     * @throws ElementNotFoundException
      * @Then /^the "(?P<select_string>(?:[^"]|\\")*)" select box should contain "(?P<option_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @throws ElementNotFoundException Thrown by behat_base::find
      * @param string $select The select element name
      * @param string $option The option text/value
      */
@@ -225,12 +211,7 @@ class behat_forms extends behat_base {
         $select = $this->fixStepArgument($select);
         $option = $this->fixStepArgument($option);
 
-        $selectnode = $this->getSession()->getPage()->findField($select);
-        if ($selectnode == null) {
-            throw new ElementNotFoundException(
-                $this->getSession(), 'form field', 'id|name|label|value', $select
-            );
-        }
+        $selectnode = $this->find_field($select);
 
         $regex = '/' . preg_quote($option, '/') . '/ui';
         if (!preg_match($regex, $selectnode->getText())) {
@@ -245,9 +226,9 @@ class behat_forms extends behat_base {
     /**
      * Checks, that given select box does not contain the specified option.
      *
-     * @throws ExpectationException
-     * @throws ElementNotFoundException
      * @Then /^the "(?P<select_string>(?:[^"]|\\")*)" select box should not contain "(?P<option_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @throws ElementNotFoundException Thrown by behat_base::find
      * @param string $select The select element name
      * @param string $option The option text/value
      */
@@ -256,12 +237,7 @@ class behat_forms extends behat_base {
         $select = $this->fixStepArgument($select);
         $option = $this->fixStepArgument($option);
 
-        $selectnode = $this->getSession()->getPage()->findField($select);
-        if ($selectnode == null) {
-            throw new ElementNotFoundException(
-                $this->getSession(), 'form field', 'id|name|label|value', $select
-            );
-        }
+        $selectnode = $this->find_field($select);
 
         $regex = '/' . preg_quote($option, '/') . '/ui';
         if (preg_match($regex, $selectnode->getText())) {
index 0b178b9..7fe1bb7 100644 (file)
@@ -47,7 +47,6 @@ class behat_general extends behat_base {
     /**
      * Opens Moodle homepage.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @Given /^I am on homepage$/
      */
     public function i_am_on_homepage() {
@@ -57,12 +56,14 @@ class behat_general extends behat_base {
     /**
      * Clicks link with specified id|title|alt|text.
      *
-     * @see Behat\MinkExtension\Context\MinkContext
      * @When /^I follow "(?P<link_string>(?:[^"]|\\")*)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      */
     public function click_link($link) {
         $link = $this->fixStepArgument($link);
-        $this->getSession()->getPage()->clickLink($link);
+
+        $linknode = $this->find_link($link);
+        $linknode->click();
     }
 
     /**
@@ -87,16 +88,17 @@ class behat_general extends behat_base {
     /**
      * Mouse over a CSS element.
      *
-     * @throws ExpectationException
-     * @see Sanpi/Behatch/Context/BrowserContext.php
      * @When /^I hover "(?P<element_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException Thrown by behat_base::find
      * @param string $element
      */
     public function i_hover($element) {
-        $node = $this->getSession()->getPage()->find('css', $element);
-        if ($node === null) {
-            throw new ExpectationException('The hovered element "' . $element . '" was not found anywhere in the page', $this->getSession());
-        }
+
+        $exception = new ExpectationException(
+            'The hovered element "' . $element . '" was not found anywhere in the page', $this->getSession()
+        );
+
+        $node = $this->find('css', $element, $exception);
         $node->mouseOver();
     }
 
@@ -125,8 +127,7 @@ class behat_general extends behat_base {
      *
      * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" element$/
      */
-    public function assert_element_contains_text($element, $text) {
-        $element = $this->fixStepArgument($element);
+    public function assert_element_contains_text($text, $element) {
         $this->assertSession()->elementTextContains('css', $element, $this->fixStepArgument($text));
     }
 
@@ -135,27 +136,21 @@ class behat_general extends behat_base {
      *
      * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" element$/
      */
-    public function assert_element_not_contains_text($element, $text) {
-        $element = $this->fixStepArgument($element);
+    public function assert_element_not_contains_text($text, $element) {
         $this->assertSession()->elementTextNotContains('css', $element, $this->fixStepArgument($text));
     }
 
     /**
      * Checks, that element with given CSS is disabled.
      *
-     * @throws ExpectationException
-     * @see Sanpi/Behatch/Context/BrowserContext
      * @Then /^the element "(?P<element_string>(?:[^"]|\\")*)" should be disabled$/
+     * @throws ExpectationException Thrown by behat_base::find
      * @param string $element
      */
     public function the_element_should_be_disabled($element) {
 
-        $element = $this->fixStepArgument($element);
-
-        $node = $this->getSession()->getPage()->find('css', $element);
-        if ($node == null) {
-            throw new ExpectationException('There is no "' . $element . '" element', $this->getSession());
-        }
+        $exception = new ExpectationException('There is no "' . $element . '" element', $this->getSession());
+        $node = $this->find('css', $element, $exception);
 
         if (!$node->hasAttribute('disabled')) {
             throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
@@ -165,16 +160,14 @@ class behat_general extends behat_base {
     /**
      * Checks, that element with given CSS is enabled.
      *
-     * @throws ExpectationException
-     * @see Sanpi/Behatch/Context/BrowserContext.php
      * @Then /^the element "(?P<element_string>(?:[^"]|\\")*)" should be enabled$/
+     * @throws ExpectationException Thrown by behat_base::find
      * @param string $element
      */
     public function the_element_should_be_enabled($element) {
-        $node = $this->getSession()->getPage()->find('css', $element);
-        if ($node == null) {
-            throw new ExpectationException('There is no "' . $element . '" element', $this->getSession());
-        }
+
+        $exception = new ExpectationException('There is no "' . $element . '" element', $this->getSession());
+        $node = $this->find('css', $element, $exception);
 
         if ($node->hasAttribute('disabled')) {
             throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
index c92e138..f7aa002 100644 (file)
@@ -52,11 +52,6 @@ use Behat\Behat\Event\StepEvent as StepEvent;
  */
 class behat_hooks extends behat_base {
 
-    /**
-     * @var string The last visited URL.
-     */
-    private $lasturl = null;
-
     /**
      * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
      *
@@ -74,6 +69,7 @@ class behat_hooks extends behat_base {
         define('BEHAT_RUNNING', 1);
         define('CLI_SCRIPT', 1);
 
+        // With BEHAT_RUNNING we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
         require_once(__DIR__ . '/../../../config.php');
 
         // Now that we are MOODLE_INTERNAL.
@@ -93,6 +89,11 @@ class behat_hooks extends behat_base {
             throw new Exception($CFG->behat_wwwroot . ' is not available, ensure you started your PHP built-in server. More info in ' . behat_command::DOCS_URL . '#Running_tests');
         }
 
+        // Prevents using outdated data, upgrade script would start and tests would fail.
+        if (!behat_util::is_test_data_updated()) {
+            $commandpath = 'php admin/tool/behat/cli/util.php';
+            throw new Exception('Your behat test site is outdated, please run ' . $commandpath . ' from your moodle dirroot to drop and install the behat test site again.');
+        }
         // Avoid parallel tests execution, it continues when the previous lock is released.
         test_lock::acquire('behat');
     }
@@ -162,12 +163,8 @@ class behat_hooks extends behat_base {
             return;
         }
 
-        // Wait until the page is ready if we are in a new URL.
-        $currenturl = $this->getSession()->getCurrentUrl();
-        if (is_null($this->lasturl) || $currenturl !== $this->lasturl) {
-            $this->lasturl = $currenturl;
-            $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
-        }
+        // Wait until the page is ready.
+        $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
     }
 
 }
index 169e67d..ad5031e 100644 (file)
@@ -40,8 +40,9 @@ class behat_navigation extends behat_base {
 
     /**
      * Expands the selected node of the navigation tree that matches the text.
-     *
      * @Given /^I expand "(?P<nodetext_string>(?:[^"]|\\")*)" node$/
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
      * @param string $nodetext
      */
     public function i_expand_node($nodetext) {
@@ -53,7 +54,7 @@ class behat_navigation extends behat_base {
 /descendant::p[contains(concat(' ', normalize-space(@class), ' '), ' branch')]
 /descendant::span[contains(concat(' ', normalize-space(.), ' '), '" . $nodetext . "')]";
 
-        $node = $this->getSession()->getPage()->find('xpath', $xpath);
+        $node = $this->find('xpath', $xpath);
         $node->click();
     }
 
index 45f1fa4..b5807b3 100644 (file)
@@ -2245,4 +2245,201 @@ class moodlelib_testcase extends advanced_testcase {
         set_config('phpunit_test_get_config_4', 'test c', 'mod_forum');
         $this->assertFalse($cache->get('mod_forum'));
     }
+
+    function test_get_max_upload_sizes() {
+        // Test with very low limits so we are not affected by php upload limits.
+        // Test activity limit smallest.
+        $sitebytes = 102400;
+        $coursebytes = 51200;
+        $modulebytes = 10240;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
+
+        $this->assertEquals('Activity upload limit (10KB)', $result['0']);
+        $this->assertEquals(2, count($result));
+
+        // Test course limit smallest.
+        $sitebytes = 102400;
+        $coursebytes = 10240;
+        $modulebytes = 51200;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
+
+        $this->assertEquals('Course upload limit (10KB)', $result['0']);
+        $this->assertEquals(2, count($result));
+
+        // Test site limit smallest.
+        $sitebytes = 10240;
+        $coursebytes = 102400;
+        $modulebytes = 51200;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
+
+        $this->assertEquals('Site upload limit (10KB)', $result['0']);
+        $this->assertEquals(2, count($result));
+
+        // Test site limit not set.
+        $sitebytes = 0;
+        $coursebytes = 102400;
+        $modulebytes = 51200;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
+
+        $this->assertEquals('Activity upload limit (50KB)', $result['0']);
+        $this->assertEquals(3, count($result));
+
+        $sitebytes = 0;
+        $coursebytes = 51200;
+        $modulebytes = 102400;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
+
+        $this->assertEquals('Course upload limit (50KB)', $result['0']);
+        $this->assertEquals(3, count($result));
+
+        // Test custom bytes in range.
+        $sitebytes = 102400;
+        $coursebytes = 51200;
+        $modulebytes = 51200;
+        $custombytes = 10240;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
+
+        $this->assertEquals(3, count($result));
+
+        // Test custom bytes in range but non-standard.
+        $sitebytes = 102400;
+        $coursebytes = 51200;
+        $modulebytes = 51200;
+        $custombytes = 25600;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
+
+        $this->assertEquals(4, count($result));
+
+        // Test custom bytes out of range.
+        $sitebytes = 102400;
+        $coursebytes = 51200;
+        $modulebytes = 51200;
+        $custombytes = 102400;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
+
+        $this->assertEquals(3, count($result));
+
+        // Test custom bytes out of range and non-standard.
+        $sitebytes = 102400;
+        $coursebytes = 51200;
+        $modulebytes = 51200;
+        $custombytes = 256000;
+        $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
+
+        $this->assertEquals(3, count($result));
+    }
+
+    /**
+     * Test function password_is_legacy_hash().
+     */
+    public function test_password_is_legacy_hash() {
+        // Well formed md5s should be matched.
+        foreach (array('some', 'strings', 'to_check!') as $string) {
+            $md5 = md5($string);
+            $this->assertTrue(password_is_legacy_hash($md5));
+        }
+        // Strings that are not md5s should not be matched.
+        foreach (array('', AUTH_PASSWORD_NOT_CACHED, 'IPW8WTcsWNgAWcUS1FBVHegzJnw5M2jOmYkmfc8z.xdBOyC4Caeum') as $notmd5) {
+            $this->assertFalse(password_is_legacy_hash($notmd5));
+        }
+    }
+
+    /**
+     * Test function validate_internal_user_password().
+     */
+    public function test_validate_internal_user_password() {
+        if (password_compat_not_supported()) {
+            // If bcrypt is not properly supported test legacy md5 hashes instead.
+            // Can't hardcode these as we don't know the site's password salt.
+            $validhashes = array(
+                'pw' => hash_internal_user_password('pw'),
+                'abc' => hash_internal_user_password('abc'),
+                'C0mP1eX_&}<?@*&%` |\"' => hash_internal_user_password('C0mP1eX_&}<?@*&%` |\"'),
+                'ĩńťėŕňăţĩōŋāĹ' => hash_internal_user_password('ĩńťėŕňăţĩōŋāĹ')
+            );
+        } else {
+            // Otherwise test bcrypt hashes.
+            $validhashes = array(
+                'pw' => '$2y$10$LOSDi5eaQJhutSRun.OVJ.ZSxQZabCMay7TO1KmzMkDMPvU40zGXK',
+                'abc' => '$2y$10$VWTOhVdsBbWwtdWNDRHSpewjd3aXBQlBQf5rBY/hVhw8hciarFhXa',
+                'C0mP1eX_&}<?@*&%` |\"' => '$2y$10$3PJf.q.9ywNJlsInPbqc8.IFeSsvXrGvQLKRFBIhVu1h1I3vpIry6',
+                'ĩńťėŕňăţĩōŋāĹ' => '$2y$10$3A2Y8WpfRAnP3czJiSv6N.6Xp0T8hW3QZz2hUCYhzyWr1kGP1yUve'
+            );
+        }
+
+        foreach ($validhashes as $password => $hash) {
+            $user = new stdClass();
+            $user->auth = 'manual';
+            $user->password = $hash;
+            // The correct password should be validated.
+            $this->assertTrue(validate_internal_user_password($user, $password));
+            // An incorrect password should not be validated.
+            $this->assertFalse(validate_internal_user_password($user, 'badpw'));
+        }
+    }
+
+    /**
+     * Test function hash_internal_user_password().
+     */
+    public function test_hash_internal_user_password() {
+        $passwords = array('pw', 'abc123', 'C0mP1eX_&}<?@*&%` |\"', 'ĩńťėŕňăţĩōŋāĹ');
+
+        // Check that some passwords that we convert to hashes can
+        // be validated.
+        foreach ($passwords as $password) {
+            $hash = hash_internal_user_password($password);
+            $fasthash = hash_internal_user_password($password, true);
+            $user = new stdClass();
+            $user->auth = 'manual';
+            $user->password = $hash;
+            $this->assertTrue(validate_internal_user_password($user, $password));
+
+            if (password_compat_not_supported()) {
+                // If bcrypt is not properly supported make sure the passwords are in md5 format.
+                $this->assertTrue(password_is_legacy_hash($hash));
+            } else {
+                // Otherwise they should not be in md5 format.
+                $this->assertFalse(password_is_legacy_hash($hash));
+
+                // Check that cost factor in hash is correctly set.
+                $this->assertRegExp('/\$10\$/', $hash);
+                $this->assertRegExp('/\$04\$/', $fasthash);
+            }
+        }
+    }
+
+    /**
+     * Test function update_internal_user_password().
+     */
+    public function test_update_internal_user_password() {
+        global $DB;
+        $this->resetAfterTest();
+        $passwords = array('password', '1234', 'changeme', '****');
+        foreach ($passwords as $password) {
+            $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
+            update_internal_user_password($user, $password);
+            // The user object should have been updated.
+            $this->assertTrue(validate_internal_user_password($user, $password));
+            // The database field for the user should also have been updated to the
+            // same value.
+            $this->assertEquals($user->password, $DB->get_field('user', 'password', array('id' => $user->id)));
+        }
+
+        $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
+        // Manually set the user's password to the md5 of the string 'password'.
+        $DB->set_field('user', 'password', '5f4dcc3b5aa765d61d8327deb882cf99', array('id' => $user->id));
+
+        // Update the password.
+        update_internal_user_password($user, 'password');
+
+        if (password_compat_not_supported()) {
+            // If bcrypt not properly supported the password should remain as an md5 hash.
+            $expected_hash = hash_internal_user_password('password', true);
+            $this->assertEquals($user->password, $expected_hash);
+            $this->assertTrue(password_is_legacy_hash($user->password));
+        } else {
+            // Otherwise password should have been updated to a bcrypt hash.
+            $this->assertFalse(password_is_legacy_hash($user->password));
+        }
+    }
 }
index ff30d90..703494d 100644 (file)
@@ -3,6 +3,10 @@ information provided here is intended especially for developers.
 
 === 2.5 ===
 
+* Function get_max_file_sizes now returns an option for (for example) "Course limit (500MB)" or
+  "Site limit (200MB)" when appropriate with the option set to 0. This function no longer returns
+  an option for 0 bytes. Existing code that was replacing the 0 option in the return
+  from this function with a more sensible message, can now use the return from this function directly.
 * Functions responsible for output in course/lib.php are deprecated, the code is moved to
   appropriate renderers: print_section_add_menus()
   See functions' phpdocs in lib/deprecatedlib.php
@@ -15,6 +19,11 @@ information provided here is intended especially for developers.
   perform the whole deletion process. The function course_delete_module now takes care
   of the whole process.
 
+YUI changes:
+* M.util.help_icon has been deprecated. Code should be updated to use moodle-core-popuphelp
+  instead. To do so, remove any existing JS calls to M.util.help_icon from your PHP and ensure
+  that your help link is placed in a span which has the class 'helplink'.
+
 === 2.4 ===
 
 * Pagelib: Numerous deprecated functions were removed as classes page_base, page_course
index 532db65..111026c 100644 (file)
@@ -825,8 +825,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
 
             if ($currblock->cron != $block->cron) {
                 // update cron flag if needed
-                $currblock->cron = $block->cron;
-                $DB->update_record('block', $currblock);
+                $DB->set_field('block', 'cron', $block->cron, array('id' => $currblock->id));
             }
 
             // Upgrade various components
diff --git a/lib/yui/popuphelp/popuphelp.js b/lib/yui/popuphelp/popuphelp.js
new file mode 100644 (file)
index 0000000..6ef2710
--- /dev/null
@@ -0,0 +1,86 @@
+YUI.add('moodle-core-popuphelp', function(Y) {
+    function POPUPHELP() {
+        POPUPHELP.superclass.constructor.apply(this, arguments);
+    }
+
+    var SELECTORS = {
+            CLICKABLELINKS: 'span.helptooltip > a',
+            FOOTER: 'div.moodle-dialogue-ft'
+        },
+
+        CSS = {
+            ICON: 'icon',
+            ICONPRE: 'icon-pre'
+        },
+        ATTRS = {};
+
+    // Set the modules base properties.
+    POPUPHELP.NAME = 'moodle-core-popuphelp';
+    POPUPHELP.ATTRS = ATTRS;
+
+    Y.extend(POPUPHELP, Y.Base, {
+        panel: null,
+
+        initializer: function() {
+            Y.one('body').delegate('click', this.display_panel, SELECTORS.CLICKABLELINKS, this);
+        },
+
+        display_panel: function(e) {
+            if (!this.panel) {
+                this.panel = new M.core.tooltip({
+                    bodyhandler: this.set_body_content,
+                    footerhandler: this.set_footer,
+                    initialheadertext: M.util.get_string('loadinghelp', 'moodle'),
+                    initialfootertext: ''
+                });
+            }
+
+            // Call the tooltip setup.
+            this.panel.display_panel(e);
+        },
+
+        /**
+         * Override the footer handler to add a 'More help' link where relevant.
+         *
+         * @param {Object} helpobject The object returned from the AJAX call.
+         */
+        set_footer: function(helpobject) {
+            // Check for an optional link to documentation on moodle.org.
+            if (helpobject.doclink) {
+                // Wrap a help icon and the morehelp text in an anchor. The class of the anchor should
+                // determine whether it's opened in a new window or not.
+                doclink = Y.Node.create('<a />')
+                    .setAttrs({
+                        'href': helpobject.doclink.link
+                    })
+                    .addClass(helpobject.doclink['class']);
+                helpicon = Y.Node.create('<img />')
+                    .setAttrs({
+                        'src': M.util.image_url('docs', 'core')
+                    })
+                    .addClass(CSS.ICON)
+                    .addClass(CSS.ICONPRE);
+                doclink.appendChild(helpicon);
+                doclink.appendChild(helpobject.doclink.linktext);
+
+                // Set the footerContent to the contents of the doclink.
+                this.set('footerContent', doclink);
+                this.bb.one(SELECTORS.FOOTER).show();
+            } else {
+                this.bb.one(SELECTORS.FOOTER).hide();
+            }
+        }
+    });
+    M.core = M.core || {};
+    M.core.popuphelp = M.core.popuphelp || null;
+    M.core.init_popuphelp = M.core.init_popuphelp || function(config) {
+        // Only set up a single instance of the popuphelp.
+        if (!M.core.popuphelp) {
+            M.core.popuphelp = new POPUPHELP(config);
+        }
+        return M.core.popuphelp;
+    };
+},
+'@VERSION@', {
+    requires: ['moodle-core-tooltip']
+});
diff --git a/lib/yui/tooltip/tooltip.js b/lib/yui/tooltip/tooltip.js
new file mode 100644 (file)
index 0000000..e2680e5
--- /dev/null
@@ -0,0 +1,434 @@
+YUI.add('moodle-core-tooltip', function(Y) {
+    /**
+     * Provides the base tooltip class.
+     *
+     * @module moodle-core-tooltip
+     */
+
+    /**
+     * A base class for a tooltip.
+     *
+     * @param {Object} config Object literal specifying tooltip configuration properties.
+     * @class M.core.tooltip
+     * @constructor
+     * @extends M.core.dialogue
+     */
+    function TOOLTIP(config) {
+        if (!config) {
+            config = {};
+        }
+
+        // Override the default options provided by the parent class.
+        if (typeof config.draggable === 'undefined') {
+            config.draggable = true;
+        }
+
+        if (typeof config.constrain === 'undefined') {
+            config.constrain = true;
+        }
+
+        if (typeof config.lightbox === 'undefined') {
+            config.lightbox = false;
+        }
+
+        TOOLTIP.superclass.constructor.apply(this, [config]);
+    }
+
+    var SELECTORS = {
+            CLOSEBUTTON: '.closebutton'
+        },
+
+        CSS = {
+            PANELTEXT: 'tooltiptext'
+        },
+        RESOURCES = {
+            WAITICON: {
+                pix: 'i/loading_small',
+                component: 'moodle'
+            }
+        },
+        ATTRS = {};
+
+    /**
+     * Static property provides a string to identify the JavaScript class.
+     *
+     * @property NAME
+     * @type String
+     * @static
+     */
+    TOOLTIP.NAME = 'moodle-core-tooltip';
+
+    /**
+     * Static property used to define the CSS prefix applied to tooltip dialogues.
+     *
+     * @property CSS_PREFIX
+     * @type String
+     * @static
+     */
+    TOOLTIP.CSS_PREFIX = 'moodle-dialogue';
+
+    /**
+     * Static property used to define the default attribute configuration for the Tooltip.
+     *
+     * @property ATTRS
+     * @type String
+     * @static
+     */
+    TOOLTIP.ATTRS = ATTRS;
+
+    /**
+     * The initial value of the header region before the content finishes loading.
+     *
+     * @attribute initialheadertext
+     * @type String
+     * @default ''
+     * @writeOnce
+     */
+    ATTRS.initialheadertext = {
+        value: ''
+    };
+
+    /**
+      * The initial value of the body region before the content finishes loading.
+      *
+      * The supplid string will be wrapped in a div with the CSS.PANELTEXT class and a standard Moodle spinner
+      * appended.
+      *
+      * @attribute initialbodytext
+      * @type String
+      * @default ''
+      * @writeOnce
+      */
+    ATTRS.initialbodytext = {
+        value: '',
+        setter: function(content) {
+            var parentnode,
+                spinner;
+            parentnode = Y.Node.create('<div />')
+                .addClass(CSS.PANELTEXT);
+
+            spinner = Y.Node.create('<img />')
+                .setAttribute('src', M.util.image_url(RESOURCES.WAITICON.pix, RESOURCES.WAITICON.component))
+                .addClass('spinner');
+
+            if (content) {
+                // If we have been provided with content, add it to the parent and make
+                // the spinner appear correctly inline
+                parentnode.set('text', content);
+                spinner.addClass('iconsmall');
+            } else {
+                // If there is no loading message, just make the parent node a lightbox
+                parentnode.addClass('content-lightbox');
+            }
+
+            parentnode.append(spinner);
+            return parentnode;
+        }
+    };
+
+    /**
+     * The initial value of the footer region before the content finishes loading.
+     *
+     * If a value is supplied, it will be wrapped in a <div> first.
+     *
+     * @attribute initialfootertext
+     * @type String
+     * @default ''
+     * @writeOnce
+     */
+    ATTRS.initialfootertext = {
+        value: null,
+        setter: function(content) {
+            if (content) {
+                return Y.Node.create('<div />')
+                    .set('text', content);
+            }
+        }
+    };
+
+    /**
+     * The function which handles setting the content of the title region.
+     * The specified function will be called with a context of the tooltip instance.
+     *
+     * The default function will simply set the value of the title to object.heading as returned by the AJAX call.
+     *
+     * @attribute headerhandler
+     * @type Function|String|null
+     * @default set_header_content
+     */
+    ATTRS.headerhandler = {
+        value: 'set_header_content'
+    };
+
+    /**
+     * The function which handles setting the content of the body region.
+     * The specified function will be called with a context of the tooltip instance.
+     *
+     * The default function will simply set the value of the body area to a div containing object.text as returned
+     * by the AJAX call.
+     *
+     * @attribute bodyhandler
+     * @type Function|String|null
+     * @default set_body_content
+     */
+    ATTRS.bodyhandler = {
+        value: 'set_body_content'
+    };
+
+    /**
+     * The function which handles setting the content of the footer region.
+     * The specified function will be called with a context of the tooltip instance.
+     *
+     * By default, the footer is not set.
+     *
+     * @attribute footerhandler
+     * @type Function|String|null
+     * @default null
+     */
+    ATTRS.footerhandler = {
+        value: null
+    };
+
+    /**
+     * Set the Y.Cache object to use.
+     *
+     * By default a new Y.Cache object will be created for each instance of the tooltip.
+     *
+     * In certain situations, where multiple tooltips may share the same cache, it may be preferable to
+     * seed this cache from the calling method.
+     *
+     * @attribute textcache
+     * @type Y.Cache|null
+     * @default null
+     */
+    ATTRS.textcache = {
+        value: null
+    };
+
+    /**
+     * Set the default size of the Y.Cache object.
+     *
+     * This is only used if no textcache is specified.
+     *
+     * @attribute textcachesize
+     * @type Number
+     * @default 10
+     */
+    ATTRS.textcachesize = {
+        value: 10
+    };
+
+    Y.extend(TOOLTIP, M.core.dialogue, {
+        // The bounding box.
+        bb: null,
+
+        // Any event listeners we may need to cancel later.
+        listenevents: [],
+
+        // Cache of objects we've already retrieved.
+        textcache: null,
+
+        // The align position. This differs for RTL languages so we calculate once and store.
+        alignpoints: [
+            Y.WidgetPositionAlign.TL,
+            Y.WidgetPositionAlign.RC
+        ],
+
+        initializer: function() {
+            // Set the initial values for the handlers.
+            // These cannot be set in the attributes section as context isn't present at that time.
+            if (!this.get('headerhandler')) {
+                this.set('headerhandler', this.set_header_content);
+            }
+            if (!this.get('bodyhandler')) {
+                this.set('bodyhandler', this.set_body_content);
+            }
+            if (!this.get('footerhandler')) {
+                this.set('footerhandler', function() {});
+            }
+
+            // Set up the dialogue with initial content.
+            this.setAttrs({
+                headerContent: this.get('initialheadertext'),
+                bodyContent: this.get('initialbodytext'),
+                footerContent: this.get('initialfootertext'),
+                zIndex: 150
+            });
+
+            // Hide and then render the dialogue.
+            this.hide();
+            this.render();
+
+            // Hook into a few useful areas.
+            this.bb = this.get('boundingBox');
+
+            // Change the alignment if this is an RTL language.
+            if (right_to_left()) {
+                this.alignpoints = [
+                    Y.WidgetPositionAlign.TR,
+                    Y.WidgetPositionAlign.LC
+                ];
+            }
+
+            // Set up the text cache if it's not set up already.
+            if (!this.get('textcache')) {
+                this.set('textcache', new Y.Cache({
+                    // Set a reasonable maximum cache size to prevent memory growth.
+                    max: this.get('textcachesize')
+                }));
+            }
+
+            // Disable the textcache when in developerdebug.
+            if (M.cfg.developerdebug) {
+                this.get('textcache').set('max', 0);
+            }
+
+            return this;
+        },
+
+        /**
+         * Display the tooltip for the clicked link.
+         *
+         * The anchor for the clicked link is used, additionally appending ajax=1 to the parameters.
+         *
+         * @method display_panel
+         * @param {EventFacade} e The event from the clicked link. This is used to determine the clicked URL.
+         */
+        display_panel: function(e) {
+            var clickedlink, thisevent, ajaxurl, config, cacheentry;
+
+            // Prevent the default click action and prevent the event triggering anything else.
+            e.preventDefault();
+            e.stopPropagation();
+
+            // Cancel any existing listeners and close the panel if it's already open.
+            this.cancel_events();
+
+            // Grab the clickedlink - this contains the URL we fetch and we align the panel to it.
+            clickedlink = e.target.ancestor('a', true);
+
+            // Align with the link that was clicked.
+            this.align(clickedlink, this.alignpoints);
+
+            // Reset the initial text to a spinner while we retrieve the text.
+            this.setAttrs({
+                headerContent: this.get('initialheadertext'),
+                bodyContent: this.get('initialbodytext'),
+                footerContent: this.get('initialfootertext')
+            });
+
+            // Now that initial setup has begun, show the panel.
+            this.show();
+
+            // Add some listen events to close on.
+            thisevent = this.bb.delegate('click', this.close_panel, SELECTORS.CLOSEBUTTON, this);
+            this.listenevents.push(thisevent);
+
+            thisevent = Y.one('body').on('key', this.close_panel, 'esc', this);
+            this.listenevents.push(thisevent);
+
+            // Listen for mousedownoutside events - clickoutside is broken on IE.
+            thisevent = this.bb.on('mousedownoutside', this.close_panel, this);
+            this.listenevents.push(thisevent);
+
+            ajaxurl = clickedlink.get('href');
+
+            cacheentry = this.get('textcache').retrieve(ajaxurl);
+            if (cacheentry) {
+                // The data from this help call was already cached so use that and avoid an AJAX call.
+                this._set_panel_contents(cacheentry.response);
+            } else {
+                // Retrieve the actual help text we should use.
+                config = {
+                    method: 'get',
+                    context: this,
+                    sync: false,
+                    data: {
+                        // We use a slightly different AJAX URL to the one on the anchor to allow non-JS fallback.
+                        ajax: 1
+                    },
+                    on: {
+                        complete: function(tid, response) {
+                            this._set_panel_contents(response.responseText, ajaxurl);
+                        }
+                    }
+                };
+
+                Y.io(clickedlink.get('href'), config);
+            }
+        },
+
+        _set_panel_contents: function(response,&