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);
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']) {
}
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) {
// 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);
} 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)) {
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
}
$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);
}
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;
// 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
// 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() . "', ";
// 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() . "', ";
// 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;
// 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;
// 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
// 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;
// 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;
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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;
// 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;
// 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
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;
*/
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);
}
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));
*/
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);
}
*/
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);
}
*/
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);
}
*/
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();
}
$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;
$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());
// 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) {
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();
}
// 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';
//=========================================================================
-// 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
//=========================================================================
-// 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
//=========================================================================
-// 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
//=========================================================================
-// 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
//=========================================================================
-// 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.
// $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
// $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.
// '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
//=========================================================================
* @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 = {
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);
}
// 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) {
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
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
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
$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);
if (!$minstance) {
// This should never happen unless adding of default instance fails unexpectedly.
+ debugging('Failed to find manual enrolment instance', DEBUG_DEVELOPER);
continue;
}
$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;
$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;
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;
$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();
}
$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';
$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';
$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';
$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';
$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';
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
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
// 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 {
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;
}
<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"/>
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;
}
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();
// 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.
*/
--- /dev/null
+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']});
--- /dev/null
+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']});
$mform->setType($elementname, $params);
}
break;
+ case 'expanded' :
+ $mform->setExpanded($realelementname, $params);
+ break;
}
}
}
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')
);
}
}
/** @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;
*/
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
*
} 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;
}
/**
*/
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];
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);
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);
}
/** @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>";
/** @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
$this->_advancedElements = $elements;
}
+ /**
+ * Setting collapsible elements
+ *
+ * @param array $elements
+ */
+ function setCollapsibleElements($elements) {
+ $this->_collapsibleElements = $elements;
+ }
+
/**
* What to do when starting the form
*
$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>";
$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)));
+ }
}
/**
$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);
}
parent::startGroup($group, $required, $error);
}
+
/**
* Renders element
*
}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);
}
$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++;
}
* 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();
}
$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;
}
}
+/**
+ * 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;
}
};
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');
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 ////////////////////////////////////////////////////
/**
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
$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);
}
/**
- * 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
}
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);
}
}
/**
- * 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;
}
* @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,
$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);
* 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
}
$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);
}
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;
}
// 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;
$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'));
}
/**
// 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'));
}
/**
$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',
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+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.
--- /dev/null
+<?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));
+ }
+
+}
--- /dev/null
+<?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'));
+ }
+
+}
--- /dev/null
+<?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));
+ }
+
+}
--- /dev/null
+<?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'));
+ }
+
+}
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
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);
}
}
*/
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();
$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();
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));
+ }
+ }
}
=== 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
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
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
--- /dev/null
+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']
+});
--- /dev/null
+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, ajaxurl) {
+ var responseobject;
+
+ // Attempt to parse the response into an object.
+ try {
+ responseobject = Y.JSON.parse(response);
+ if (responseobject.error) {
+ this.close_panel();
+ return new M.core.ajaxException(responseobject);
+ }
+ } catch (error) {
+ this.close_panel();
+ return new M.core.exception({
+ name: error.name,
+ message: "Unable to retrieve the requested content. The following error was returned: " + error.message
+ });
+ }
+
+ // Set the contents using various handlers.
+ // We must use Y.bind to ensure that the correct context is used when the default handlers are overridden.
+ Y.bind(this.get('headerhandler'), this, responseobject)();
+ Y.bind(this.get('bodyhandler'), this, responseobject)();
+ Y.bind(this.get('footerhandler'), this, responseobject)();
+
+ if (ajaxurl) {
+ // Ensure that this data is added to the cache.
+ this.get('textcache').add(ajaxurl, response);
+ }
+
+ this.get('buttons').header[0].focus();
+ },
+
+ set_header_content: function(responseobject) {
+ this.set('headerContent', responseobject.heading);
+ },
+
+ set_body_content: function(responseobject) {
+ var bodycontent = Y.Node.create('<div />')
+ .set('innerHTML', responseobject.text)
+ .setAttribute('role', 'alert')
+ .addClass(CSS.PANELTEXT);
+ this.set('bodyContent', bodycontent);
+ },
+
+ close_panel: function(e) {
+ // Hide the panel first.
+ this.hide();
+
+ // Cancel the listeners that we added in display_panel.
+ this.cancel_events();
+
+ // Prevent any default click that the close button may have.
+ if (e) {
+ e.preventDefault();
+ }
+ },
+
+ cancel_events: function() {
+ // Detach all listen events to prevent duplicate triggers.
+ var thisevent;
+ while (this.listenevents.length) {
+ thisevent = this.listenevents.shift();
+ thisevent.detach();
+ }
+ }
+ });
+ M.core = M.core || {};
+ M.core.tooltip = M.core.tooltip = TOOLTIP;
+},
+'@VERSION@', {
+ requires: ['base', 'io-base', 'moodle-core-notification', 'json-parse',
+ 'widget-position', 'widget-position-align', 'event-outside', 'cache']
+}
+);
$where = 'u.id ' . $userwhere;
$params = array_merge($params, $userparams);
- if ($filter == ASSIGN_FILTER_SUBMITTED) {
- $where .= ' AND s.timecreated > 0 ';
- }
- if ($filter == ASSIGN_FILTER_REQUIRE_GRADING) {
- $where .= ' AND (s.timemodified IS NOT NULL AND
- s.status = :submitted AND
- (s.timemodified > g.timemodified OR g.timemodified IS NULL))';
- $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
- }
- if (strpos($filter, ASSIGN_FILTER_SINGLE_USER) === 0) {
- $userfilter = (int) array_pop(explode('=', $filter));
- $where .= ' AND (u.id = :userid)';
- $params['userid'] = $userfilter;
+ // The filters do not make sense when there are no submissions, so do not apply them.
+ if ($this->assignment->is_any_submission_plugin_enabled()) {
+ if ($filter == ASSIGN_FILTER_SUBMITTED) {
+ $where .= ' AND s.timecreated > 0 ';
+ }
+ if ($filter == ASSIGN_FILTER_REQUIRE_GRADING) {
+ $where .= ' AND (s.timemodified IS NOT NULL AND
+ s.status = :submitted AND
+ (s.timemodified > g.timemodified OR g.timemodified IS NULL))';
+ $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+ }
+ if (strpos($filter, ASSIGN_FILTER_SINGLE_USER) === 0) {
+ $userfilter = (int) array_pop(explode('=', $filter));
+ $where .= ' AND (u.id = :userid)';
+ $params['userid'] = $userfilter;
+ }
}
$this->set_sql($fields, $from, $where, $params);
$COURSE->maxbytes,
get_config('assignsubmission_file', 'maxbytes'));
- // Remove the option for 0 bytes.
- unset($choices[0]);
-
- if ($COURSE->maxbytes == 0) {
- $choices = array(0=>get_string('siteuploadlimit', 'assignsubmission_file')) + $choices;
- } else {
- $choices = array(0=>get_string('courseuploadlimit') . ' (' . display_size($COURSE->maxbytes) . ')') + $choices;
- }
$settings[] = array('type' => 'select',
'name' => 'maxsubmissionsizebytes',
'description' => get_string('maximumsubmissionsize', 'assignsubmission_file'),
$ynoptions = array( 0 => get_string('no'), 1 => get_string('yes'));
$choices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes);
- $choices[0] = get_string('courseuploadlimit') . ' ('.display_size($COURSE->maxbytes).')';
$mform->addElement('select', 'maxbytes', get_string('maximumsize', 'assignment'), $choices);
$mform->setDefault('maxbytes', $CFG->assignment_maxbytes);
$mform->setDefault('emailteachers', 0);
$choices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes);
- $choices[0] = get_string('courseuploadlimit') . ' ('.display_size($COURSE->maxbytes).')';
$mform->addElement('select', 'maxbytes', get_string('maximumsize', 'assignment'), $choices);
$mform->setDefault('maxbytes', $CFG->assignment_maxbytes);
$menuoptions = array();
$menuoptions[0] = get_string('disable');
$menuoptions[1] = get_string('enable');
- $mform->addElement('header', 'timerestricthdr', get_string('limit', 'choice'));
+ $mform->addElement('header', 'limithdr', get_string('limit', 'choice'));
$mform->addElement('select', 'limitanswers', get_string('limitanswers', 'choice'), $menuoptions);
$mform->addHelpButton('limitanswers', 'limitanswers', 'choice');
$this->repeat_elements($repeatarray, $repeatno,
$repeateloptions, 'option_repeats', 'option_add_fields', 3);
-
-
+ // Make the first option required
+ if ($mform->elementExists('option[0]')) {
+ $mform->addRule('option[0]', get_string('atleastoneoption', 'choice'), 'required', null, 'client');
+ }
//-------------------------------------------------------------------------------
$mform->addElement('header', 'timerestricthdr', get_string('timerestrict', 'choice'));
}
- function validation($data, $files) {
- $errors = parent::validation($data, $files);
-
- $choices = 0;
- foreach ($data['option'] as $option){
- if (trim($option) != ''){
- $choices++;
- }
- }
-
- if ($choices < 1) {
- $errors['option[0]'] = get_string('atleastoneoption', 'choice');
- }
-
- return $errors;
- }
-
function get_data() {
$data = parent::get_data();
if (!$data) {
// migrate the folder files
$this->fileman->filearea = 'content';
$this->fileman->itemid = 0;
- $this->fileman->migrate_directory('course_files/'.$data['reference']);
+ if (empty($data['reference'])) {
+ $this->fileman->migrate_directory('course_files');
+ } else {
+ $this->fileman->migrate_directory('course_files/'.$data['reference']);
+ }
// write folder.xml
$this->open_xml_writer("activities/folder_{$moduleid}/folder.xml");
//-------------------------------------------------------
$mform->addElement('header', 'content', get_string('contentheader', 'folder'));
$mform->addElement('filemanager', 'files', get_string('files'), null, array('subdirs'=>1, 'accepted_types'=>'*'));
+ $mform->setExpanded('content');
+
//-------------------------------------------------------
$this->standard_coursemodule_elements();
$choices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes);
$choices[1] = get_string('uploadnotallowed');
- $choices[0] = get_string('courseuploadlimit') . ' ('.display_size($COURSE->maxbytes).')';
$mform->addElement('select', 'maxbytes', get_string('maxattachmentsize', 'forum'), $choices);
$mform->addHelpButton('maxbytes', 'maxattachmentsize', 'forum');
$mform->setDefault('maxbytes', $CFG->forum_maxbytes);
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['configdndmedia'] = 'Offer to create a label when media files are dragged & dropped onto a course';
+$string['configdndresizeheight'] = 'When a label is created from a dragged & dropped image, resize it if it is higher than this many pixels (0 for no resize)';
+$string['configdndresizewidth'] = 'When a label is created from a dragged & dropped image, resize it if it is wider than this many pixels (0 for no resize)';
+$string['dndmedia'] = 'Media drag and drop';
+$string['dndresizeheight'] = 'Resize drag and drop height';
+$string['dndresizewidth'] = 'Resize drag and drop width';
+$string['dnduploadlabel'] = 'Add image to course page';
$string['label:addinstance'] = 'Add a new label';
$string['labeltext'] = 'Label text';
$string['modulename'] = 'Label';
}
}
+/**
+ * Register the ability to handle drag and drop file uploads
+ * @return array containing details of the files / types the mod can handle
+ */
+function label_dndupload_register() {
+ if (get_config('label', 'dndmedia')) {
+ $mediaextensions = file_get_typegroup('extension', 'web_image');
+ $strdnd = get_string('dnduploadlabel', 'mod_label');
+ $files = array();
+ foreach ($mediaextensions as $extn) {
+ $extn = trim($extn, '.');
+ $files[] = array('extension' => $extn, 'message' => $strdnd);
+ }
+ return array('files' => $files);
+ } else {
+ return array();
+ }
+}
+
+/**
+ * Handle a file that has been uploaded
+ * @param object $uploadinfo details of the file / content that has been uploaded
+ * @return int instance id of the newly created mod
+ */
+function label_dndupload_handle($uploadinfo) {
+ global $USER;
+
+ // Gather the required info.
+ $data = new stdClass();
+ $data->course = $uploadinfo->course->id;
+ $data->name = $uploadinfo->displayname;
+ $data->intro = '';
+ $data->introformat = FORMAT_HTML;
+ $data->coursemodule = $uploadinfo->coursemodule;
+
+ // Extract the first (and only) file from the file area and add it to the label as an img tag.
+ if (!empty($uploadinfo->draftitemid)) {
+ $fs = get_file_storage();
+ $draftcontext = context_user::instance($USER->id);
+ $context = context_module::instance($uploadinfo->coursemodule);
+ $files = $fs->get_area_files($draftcontext->id, 'user', 'draft', $uploadinfo->draftitemid, '', false);
+ if ($file = reset($files)) {
+ if (file_mimetype_in_typegroup($file->get_mimetype(), 'web_image')) {
+ // It is an image - resize it, if too big, then insert the img tag.
+ $config = get_config('label');
+ $data->intro = label_generate_resized_image($file, $config->dndresizewidth, $config->dndresizeheight);
+ } else {
+ // We aren't supposed to be supporting non-image types here, but fallback to adding a link, just in case.
+ $url = moodle_url::make_draftfile_url($file->get_itemid(), $file->get_filepath(), $file->get_filename());
+ $data->intro = html_writer::link($url, $file->get_filename());
+ }
+ $data->intro = file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_label', 'intro', 0,
+ null, $data->intro);
+ }
+ }
+
+ return label_add_instance($data, null);
+}
+
+/**
+ * Resize the image, if required, then generate an img tag and, if required, a link to the full-size image
+ * @param stored_file $file the image file to process
+ * @param int $maxwidth the maximum width allowed for the image
+ * @param int $maxheight the maximum height allowed for the image
+ * @return string HTML fragment to add to the label
+ */
+function label_generate_resized_image(stored_file $file, $maxwidth, $maxheight) {
+ global $CFG;
+
+ $fullurl = moodle_url::make_draftfile_url($file->get_itemid(), $file->get_filepath(), $file->get_filename());
+ $link = null;
+ $attrib = array('alt' => $file->get_filename(), 'src' => $fullurl);
+
+ if ($imginfo = $file->get_imageinfo()) {
+ // Work out the new width / height, bounded by maxwidth / maxheight
+ $width = $imginfo['width'];
+ $height = $imginfo['height'];
+ if (!empty($maxwidth) && $width > $maxwidth) {
+ $height *= (float)$maxwidth / $width;
+ $width = $maxwidth;
+ }
+ if (!empty($maxheight) && $height > $maxheight) {
+ $width *= (float)$maxheight / $height;
+ $height = $maxheight;
+ }
+
+ $attrib['width'] = $width;
+ $attrib['height'] = $height;
+
+ // If the size has changed and the image is of a suitable mime type, generate a smaller version
+ if ($width != $imginfo['width']) {
+ $mimetype = $file->get_mimetype();
+ if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
+ require_once($CFG->libdir.'/gdlib.php');
+ $tmproot = make_temp_directory('mod_label');
+ $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
+ $file->copy_content_to($tmpfilepath);
+ $data = generate_image_thumbnail($tmpfilepath, $width, $height);
+ unlink($tmpfilepath);
+
+ if (!empty($data)) {
+ $fs = get_file_storage();
+ $record = array(
+ 'contextid' => $file->get_contextid(),
+ 'component' => $file->get_component(),
+ 'filearea' => $file->get_filearea(),
+ 'itemid' => $file->get_itemid(),
+ 'filepath' => '/',
+ 'filename' => 's_'.$file->get_filename(),
+ );
+ $smallfile = $fs->create_file_from_string($record, $data);
+
+ // Replace the image 'src' with the resized file and link to the original
+ $attrib['src'] = moodle_url::make_draftfile_url($smallfile->get_itemid(), $smallfile->get_filepath(),
+ $smallfile->get_filename());
+ $link = $fullurl;
+ }
+ }
+ }
+
+ } else {
+ // Assume this is an image type that get_imageinfo cannot handle (e.g. SVG)
+ $attrib['width'] = $maxwidth;
+ }
+
+ $img = html_writer::empty_tag('img', $attrib);
+ if ($link) {
+ return html_writer::link($link, $img);
+ } else {
+ return $img;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Resource module admin settings and defaults
+ *
+ * @package mod_label
+ * @copyright 2013 Davo Smith, Synergy Learning
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+ $settings->add(new admin_setting_configcheckbox('label/dndmedia',
+ get_string('dndmedia', 'mod_label'), get_string('configdndmedia', 'mod_label'), 1));
+
+ $settings->add(new admin_setting_configtext('label/dndresizewidth',
+ get_string('dndresizewidth', 'mod_label'), get_string('configdndresizewidth', 'mod_label'), 400, PARAM_INT, 6));
+
+ $settings->add(new admin_setting_configtext('label/dndresizeheight',
+ get_string('dndresizeheight', 'mod_label'), get_string('configdndresizeheight', 'mod_label'), 400, PARAM_INT, 6));
+}
//-------------------------------------------------------
$mform->addElement('header', 'contentsection', get_string('contentheader', 'resource'));
+ $mform->setExpanded('contentsection');
$filemanager_options = array();
$filemanager_options['accepted_types'] = '*';
$mform->setDefault('nattachments', 1);
$options = get_max_upload_sizes($CFG->maxbytes, $this->course->maxbytes);
- $options[0] = get_string('courseuploadlimit') . ' ('.display_size($this->course->maxbytes).')';
$mform->addElement('select', 'maxbytes', get_string('maxbytes', 'workshop'), $options);
$mform->setDefault('maxbytes', $workshopconfig->maxbytes);
if (isset($CFG->maxbytes)) {
$options = get_max_upload_sizes($CFG->maxbytes);
- $options[0] = get_string('courseuploadlimit');
$settings->add(new admin_setting_configselect('workshop/maxbytes', get_string('maxbytes', 'workshop'),
get_string('configmaxbytes', 'workshop'), 0, $options));
}
<directory suffix="_test.php">lib/tests</directory>
<directory suffix="_test.php">lib/ajax/tests</directory>
<directory suffix="_test.php">lib/form/tests</directory>
+ <directory>lib/password_compat/tests</directory>
</testsuite>
<testsuite name="core_files">
<directory suffix="_test.php">lib/filestorage/tests</directory>
* @var string special value to indicate a response variable that is uploaded
* files.
*/
- const PARAM_CLEANHTML_FILES = 'paramcleanhtmlfiles';
+ const PARAM_RAW_FILES = 'paramrawfiles';
/** @var integer if this attempts is stored in the question_attempts table, the id of that row. */
protected $id = null;
case self::PARAM_FILES:
return $this->process_response_files($name, $name, $postdata);
- case self::PARAM_CLEANHTML_FILES:
- $var = $this->get_submitted_var($name, PARAM_CLEANHTML, $postdata);
+ case self::PARAM_RAW_FILES:
+ $var = $this->get_submitted_var($name, PARAM_RAW, $postdata);
return $this->process_response_files($name, $name . ':itemid', $postdata, $var);
default:
public function get_expected_data() {
if ($this->responseformat == 'editorfilepicker') {
- $expecteddata = array('answer' => question_attempt::PARAM_CLEANHTML_FILES);
- } else if ($this->responseformat == 'editor') {
- $expecteddata = array('answer' => PARAM_CLEANHTML);
+ $expecteddata = array('answer' => question_attempt::PARAM_RAW_FILES);
} else {
$expecteddata = array('answer' => PARAM_RAW);
}
$string['check_passwordpolicy_error'] = 'Password policy not set.';
$string['check_passwordpolicy_name'] = 'Password policy';
$string['check_passwordpolicy_ok'] = 'Password policy enabled.';
-$string['check_passwordsaltmain_details'] = '<p>Setting a password salt greatly reduces the risk of password theft.</p>
-<p>To set a password salt, add the following line to your config.php file:</p>
-<code>$CFG->passwordsaltmain = \'some long random string here with lots of characters\';</code>
-<p>The random string of characters should be a mix of letters, numbers and other characters. A string length of at least 40 characters is recommended.</p>
-<p>Please refer to the <a href="{$a}" target="_blank">password salting documentation</a> if you wish to change the password salt. Once set, do NOT delete your password salt otherwise you will no longer be able to login to your site!</p>';
-$string['check_passwordsaltmain_name'] = 'Password salt';
-$string['check_passwordsaltmain_ok'] = 'Password salt is OK';
-$string['check_passwordsaltmain_warning'] = 'No password salt has been set';
-$string['check_passwordsaltmain_weak'] = 'Password salt is weak';
$string['check_riskadmin_detailsok'] = '<p>Please verify the following list of system administrators:</p>{$a}';
$string['check_riskadmin_detailswarning'] = '<p>Please verify the following list of system administrators:</p>{$a->admins}
<p>It is recommended to assign administrator role in the system context only. The following users have (unsupported) admin role assignments in other contexts:</p>{$a->unsupported}';
'report_security_check_openprofiles',
'report_security_check_google',
'report_security_check_passwordpolicy',
- 'report_security_check_passwordsaltmain',
'report_security_check_emailchangeconfirmation',
'report_security_check_cookiesecure',
'report_security_check_configrw',
return $result;
}
-function report_security_check_passwordsaltmain($detailed=false) {
- global $CFG;
-
- $result = new stdClass();
- $result->issue = 'report_security_check_passwordsaltmain';
- $result->name = get_string('check_passwordsaltmain_name', 'report_security');
- $result->info = null;
- $result->details = null;
- $result->status = null;
- $result->link = null;
-
- if (empty($CFG->passwordsaltmain)) {
- $result->status = REPORT_SECURITY_WARNING;
- $result->info = get_string('check_passwordsaltmain_warning', 'report_security');
- } else if ($CFG->passwordsaltmain === 'some long random string here with lots of characters'
- || trim($CFG->passwordsaltmain) === '' || preg_match('/^([a-z0-9]{0,10})$/i', $CFG->passwordsaltmain)) {
- $result->status = REPORT_SECURITY_WARNING;
- $result->info = get_string('check_passwordsaltmain_weak', 'report_security');
- } else {
- $result->status = REPORT_SECURITY_OK;
- $result->info = get_string('check_passwordsaltmain_ok', 'report_security');
- }
-
- if ($detailed) {
- $result->details = get_string('check_passwordsaltmain_details', 'report_security', get_docs_url('report/security/report_security_check_passwordsaltmain'));
- }
-
- return $result;
-}
/**
* Lists all users with XSS risk, it would be great to combine this with risk trusts in user table,
this.pathbar.removeChild(this.pathnode);
}
// assign callbacks for view mode switch buttons
- this.fpnode.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details').
- on('click', this.viewbar_clicked, this);
+ this.fpnode.one('.fp-vb-icons').on('click', this.viewbar_clicked, this);
+ this.fpnode.one('.fp-vb-tree').on('click', this.viewbar_clicked, this);
+ this.fpnode.one('.fp-vb-details').on('click', this.viewbar_clicked, this);
+
// assign callbacks for toolbar links
this.setup_toolbar();
this.setup_select_file();
require_once($CFG->dirroot.'/tag/coursetagslib.php');
$courses = coursetag_get_tagged_courses($tag->id);
-if (has_capability('moodle/blog:view', $systemcontext)) {
+if (!empty($CFG->enableblogs) && has_capability('moodle/blog:view', $systemcontext)) {
require_once($CFG->dirroot.'/blog/lib.php');
require_once($CFG->dirroot.'/blog/locallib.php');
.mform fieldset.hidden {border-width:0;}
.mform fieldset.group {margin-bottom: 0}
.mform fieldset.error {border: 1px solid #A00;}
+.mform fieldset.collapsible legend a.fheader {padding: 0 5px 0 15px; background: url([[pix:t/expanded]]) 2px center no-repeat;}
+.mform fieldset.collapsed legend a.fheader {background: url([[pix:t/collapsed]]) 2px center no-repeat;}
+.mform fieldset.collapsed.jsprocessed {border-width: 1px 0 0 0; padding: 0;}
+.mform fieldset.collapsed.jsprocessed div.fcontainer {display: none;}
.mform .fitem {width:100%;overflow:hidden;margin-top:5px;margin-bottom:1px;clear:right;}
.mform .fitem .fitemtitle {width:15%;text-align:right;float:left;}
.mform .fitem .fitemtitle div {display: inline;}
*/
#webservice-doc-generator td {text-align: left;border: 0px solid black;}
-/**
- * Help Content (pop-up)
- */
-#helppopupbox {background-color: #eee; border: 1px solid #848484;z-index: 10000 !important;}
-#helppopupbox .yui3-widget-hd {float:right;margin:3px 3px 0 0;}
-#helppopupbox .yui3-widget-bd {margin:0 1em 1em 1em;border-top:1px solid #eee;}
-#helppopupbox .yui3-widget-ft {text-align: center;}
-#helppopupbox .yui3-widget-ft .closebtn {margin:0 1em 1em 1em;}
-#helppopupbox .helpheading {font-size: 1em;}
-#helppopupbox .spinner {margin:1em;}
-.dir-rtl #helppopupbox .yui3-widget-hd {float:left;margin:3px 0 0 3px;}
-
/**
* Custom menu
*/
padding-bottom:4px;
}
+.moodle-dialogue .moodle-dialogue-bd .content-lightbox {
+ opacity: .75;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: white;
+ text-align: center;
+ padding: 10% 0;
+}
+
+/* Apply a default max-height on tooltip text */
+.moodle-dialogue .tooltiptext {
+ max-height: 300px;
+}
+
/* Question Bank - Question Chooser "Close" button */
#page-question-edit.dir-rtl a.container-close {right:auto;left:6px;}
padding: 0 0 1em;
}
-/* YUI overlays
-------------------------*/
-#helppopupbox {
- z-index: 99999 !important;
-}
-
-
/* Embedded Pages
------------------------*/
#page-footer .logininfo {padding:0.3em 0 0.7em 0;}
#page-footer .moodledocs {text-align:center;background-color:#EFEFEF;padding:0.7em 0 0.8em 0;}
-/* js help messages */
-#helppopupbox .helpheading {margin-top:1em;}
-
/* pre, code, tt */
pre, code, tt {
font: 1em/1.3 monospace;
.helplink img {
margin-left:5px;
}
-#helppopupbox {
- padding:10px 0;
-}
-#helppopupbox p {
- padding:0 0 5px;
- margin:0;
- line-height:1.3em;
-}
-#helppopupbox .helpheading {
- font-size:1.2em;
- padding-bottom:10px;
-}
/* Icons
-------------------------*/
img.icon {
// Add button
$mform->addElement('submit', 'addfilter', get_string('addfilter','filters'));
-
- // Don't use last advanced state
- $mform->setShowAdvanced(false);
}
}
defined('MOODLE_INTERNAL') || die();
-$version = 2013020800.01; // YYYYMMDD = weekly release date of this DEV branch
+$version = 2013021200.01; // YYYYMMDD = weekly release date of this DEV branch
// RR = release increments - 00 in DEV branches
// .XX = incremental changes
} else {
switch($keydesc->type) {
case PARAM_BOOL:
- $paramanddefault .= '='.$keydesc->default; break;
+ $paramanddefault .= '='. (int) $keydesc->default; break;
case PARAM_INT:
$paramanddefault .= '='.$keydesc->default; break;
case PARAM_FLOAT;