Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52bd62f2be | |||
| 53392df219 | |||
| 344386f873 | |||
| 8a2e9542c9 | |||
| 1f01b678df | |||
| 90740716be | |||
| 2cebb87682 | |||
| 907b377e51 | |||
| b77dcd23d8 | |||
| 1a3612bb5b | |||
| fba7dcd90a | |||
| e836242e1c | |||
| 6f05d22622 | |||
| 141271b726 | |||
| aae3a2fdd4 | |||
| abd634d64c | |||
| e24f8994b5 | |||
| d16e730116 | |||
| 3cc11668c8 | |||
| 3c72d44b18 | |||
| 4546269170 | |||
| ad4dfb1e8c | |||
| 4f6373bf05 | |||
| 31c05704b8 | |||
| 384c4c29db | |||
| 2282d6932b | |||
| 47265ccf62 | |||
| 2d0d9a4748 | |||
| 0cbb46b944 | |||
| 3dcf5f9930 | |||
| 3456505930 | |||
| 18c80ae8a5 | |||
| 4fccbafa46 | |||
| 11fc8b3d02 | |||
| 4e190d7eae | |||
| acd27d90c8 | |||
| d81447751e | |||
| eb3df7ecad | |||
| a9d153e61d | |||
| c2c8ce4b6d | |||
| 27f608a5c9 | |||
| 181cf34341 | |||
| 56c15f5c99 | |||
| 87711249c3 | |||
| 0a03b2fd63 | |||
| 398c4c107d | |||
| 5789961754 | |||
| 587a8a9732 | |||
| 53d0efba1f | |||
| 16045c4846 | |||
| 57d2a28425 | |||
| 33cbe249dd | |||
| 82e23b9443 | |||
| ea1d1cf9e1 | |||
| 7bd7754ad0 | |||
| 734ce33b99 | |||
| 4f19471b9e | |||
| 78275d44f0 | |||
| cfde0b8d38 | |||
| 74746ce412 | |||
| 5dd590a3bd | |||
| 7b97be9f25 | |||
| aaf4b2b489 | |||
| 5b1d246758 | |||
| 3aef542a35 | |||
| 07b8bb1ae7 | |||
| 18c55facc0 | |||
| e8a62dc4c2 | |||
| b63c41d762 | |||
| 1aa56ceaca | |||
| fdf3d59a00 | |||
| f648211569 | |||
| f8de60ce57 | |||
| 48b2284317 | |||
| 1720738ca9 | |||
| f6d48eeb87 | |||
| 66774319b3 | |||
| c3238ac39b | |||
| a922416edb | |||
| fef96d285e | |||
| bd38b68684 | |||
| 8a5095d453 | |||
| c9cd95223a | |||
| 80221982fa | |||
| 03edaa4290 | |||
| 0401ae362c | |||
| f5fcf31947 | |||
| e5e3cb4577 | |||
| 999c187559 | |||
| b5fa0b3893 | |||
| 9e75d8d7fc | |||
| 0f908de1e3 | |||
| bbd79a469a | |||
| a67ed33ed3 | |||
| f757055b9f | |||
| 1b1465210b | |||
| f06b51262f | |||
| e4a6f46527 | |||
| ec12901c8c | |||
| b871a7a7e1 | |||
| 876302149f | |||
| 1164fa8370 | |||
| e087069258 | |||
| 8275b2d502 | |||
| c49c373209 | |||
| 2e80552524 | |||
| d9cab9de67 | |||
| 6546fd1043 | |||
| 4c39ccf8a3 | |||
| 270ba66408 | |||
| 8fdf8a4ed5 | |||
| 9d7e968130 | |||
| 668e1a2ade | |||
| 8e3bb05807 | |||
| 6d67532717 | |||
| 529d402ca6 | |||
| 6c9d494f35 | |||
| 327acfdadb | |||
| 3758b9c6ab | |||
| db1002a36a | |||
| e48cc26687 | |||
| a5f813e039 | |||
| 622be7e630 |
@@ -2,6 +2,7 @@
|
||||
theme/bootstrapbase/style/
|
||||
theme/clean/style/custom.css
|
||||
theme/more/style/custom.css
|
||||
theme/boost/style/moodle.css
|
||||
node_modules/
|
||||
vendor/
|
||||
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
|
||||
|
||||
+2
-1
@@ -214,7 +214,8 @@ module.exports = function(grunt) {
|
||||
'# Generated by "grunt ignorefiles"',
|
||||
'theme/bootstrapbase/style/',
|
||||
'theme/clean/style/custom.css',
|
||||
'theme/more/style/custom.css'
|
||||
'theme/more/style/custom.css',
|
||||
'theme/boost/style/moodle.css'
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
|
||||
};
|
||||
|
||||
@@ -2618,4 +2618,185 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="3.7" requires="3.2">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="5.5.31" />
|
||||
<VENDOR name="mysql" version="5.6" />
|
||||
<VENDOR name="postgres" version="9.4" />
|
||||
<VENDOR name="mssql" version="10.0" />
|
||||
<VENDOR name="oracle" version="11.2" />
|
||||
</DATABASE>
|
||||
<PHP version="7.1.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="mbstringrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -109,6 +109,17 @@ require_once(__DIR__ . '/../../../../lib/behat/lib.php');
|
||||
require_once(__DIR__ . '/../../../../lib/behat/classes/behat_command.php');
|
||||
require_once(__DIR__ . '/../../../../lib/behat/classes/behat_config_manager.php');
|
||||
|
||||
// Remove error handling overrides done in config.php. This is consistent with admin/tool/behat/cli/util_single_run.php.
|
||||
$CFG->debug = (E_ALL | E_STRICT);
|
||||
$CFG->debugdisplay = 1;
|
||||
error_reporting($CFG->debug);
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('log_errors', '1');
|
||||
|
||||
// Import the necessary libraries.
|
||||
require_once($CFG->libdir . '/setuplib.php');
|
||||
require_once($CFG->libdir . '/behat/classes/util.php');
|
||||
|
||||
// For drop option check if parallel site.
|
||||
if ((empty($options['parallel'])) && ($options['drop']) || $options['updatesteps']) {
|
||||
$options['parallel'] = behat_config_manager::get_behat_run_config_value('parallel');
|
||||
|
||||
@@ -1075,6 +1075,7 @@ class api {
|
||||
public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
|
||||
$request = new data_request($requestid);
|
||||
$user = \core_user::get_user($request->get('userid'));
|
||||
$isconfigured = data_registry::defaults_set();
|
||||
foreach ($clcollection as $contextlist) {
|
||||
// Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
|
||||
$clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
|
||||
@@ -1083,7 +1084,11 @@ class api {
|
||||
|
||||
// Store the associated contexts in the contextlist.
|
||||
foreach ($contextlist->get_contextids() as $contextid) {
|
||||
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
|
||||
if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $request->get('type')) {
|
||||
// Data can only be deleted from it if the context is either expired, or unprotected.
|
||||
// Note: We can only check whether a context is expired or unprotected if the site is configured and
|
||||
// defaults are set appropriately. If they are not, we treat all contexts as though they are
|
||||
// unprotected.
|
||||
$context = \context::instance_by_id($contextid);
|
||||
$purpose = static::get_effective_context_purpose($context);
|
||||
|
||||
@@ -1163,10 +1168,11 @@ class api {
|
||||
* @return contextlist_collection the collection of approved_contextlist objects.
|
||||
*/
|
||||
public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
|
||||
global $DB;
|
||||
$foruser = core_user::get_user($request->get('userid'));
|
||||
$isconfigured = data_registry::defaults_set();
|
||||
|
||||
// Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
|
||||
global $DB;
|
||||
$sql = "SELECT cl.component, ctx.contextid
|
||||
FROM {" . request_contextlist::TABLE . "} rcl
|
||||
JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
|
||||
@@ -1190,10 +1196,13 @@ class api {
|
||||
$contexts = [];
|
||||
}
|
||||
|
||||
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
|
||||
if ($isconfigured && $request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
|
||||
$context = \context::instance_by_id($record->contextid);
|
||||
$purpose = static::get_effective_context_purpose($context);
|
||||
// Data can only be deleted from it if the context is either expired, or unprotected.
|
||||
// Note: We can only check whether a context is expired or unprotected if the site is configured and
|
||||
// defaults are set appropriately. If they are not, we treat all contexts as though they are
|
||||
// unprotected.
|
||||
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -920,9 +920,17 @@ class expired_contexts_manager {
|
||||
* @return bool
|
||||
*/
|
||||
protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) {
|
||||
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
|
||||
|
||||
$info = $expiryrecords[$context->path]->info;
|
||||
if ($context->get_course_context()->instanceid == SITEID) {
|
||||
// The is an activity in the site course (front page).
|
||||
$purpose = data_registry::get_effective_contextlevel_value(CONTEXT_SYSTEM, 'purpose');
|
||||
$info = static::get_expiry_info($purpose);
|
||||
|
||||
} else {
|
||||
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
|
||||
$info = $expiryrecords[$context->path]->info;
|
||||
}
|
||||
|
||||
if ($info->is_fully_expired()) {
|
||||
// This context is fully expired.
|
||||
return true;
|
||||
|
||||
@@ -74,11 +74,9 @@ class process_data_request_task extends adhoc_task {
|
||||
return;
|
||||
}
|
||||
|
||||
// If no site purpose is defined, reject requests since they cannot be processed.
|
||||
if (!\tool_dataprivacy\data_registry::defaults_set()) {
|
||||
api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED);
|
||||
mtrace('No site purpose defined. Request ' . $requestid . ' rejected.');
|
||||
return;
|
||||
// Warn if no site purpose is defined.
|
||||
mtrace('Warning: No purpose is defined at the system level. Deletion will delete all.');
|
||||
}
|
||||
|
||||
// Get the user details now. We might not be able to retrieve it later if it's a deletion processing.
|
||||
|
||||
@@ -38,6 +38,10 @@ $title = get_string('datarequests', 'tool_dataprivacy');
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($title);
|
||||
|
||||
if (!\tool_dataprivacy\data_registry::defaults_set()) {
|
||||
\core\notification::error(get_string('systemconfignotsetwarning', 'tool_dataprivacy'));
|
||||
}
|
||||
|
||||
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
|
||||
$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
|
||||
$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
|
||||
|
||||
@@ -279,5 +279,19 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
|
||||
upgrade_plugin_savepoint(true, 2017111356, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2017111358) {
|
||||
// Delete orphaned data privacy requests.
|
||||
$sql = "SELECT r.id
|
||||
FROM {tool_dataprivacy_request} r LEFT JOIN {user} u ON r.userid = u.id
|
||||
WHERE u.id IS NULL";
|
||||
$orphaned = $DB->get_fieldset_sql($sql);
|
||||
|
||||
if ($orphaned) {
|
||||
$DB->delete_records_list('tool_dataprivacy_request', 'id', $orphaned);
|
||||
}
|
||||
|
||||
upgrade_plugin_savepoint(true, 2017111358, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -308,6 +308,7 @@ $string['statusrejected'] = 'Rejected';
|
||||
$string['subjectscope'] = 'Subject scope';
|
||||
$string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
|
||||
$string['summary'] = 'Registry configuration summary';
|
||||
$string['systemconfignotsetwarning'] = 'A site purpose and category have not been defined. When these are not defined, all data will be removed when processing deletion requests.';
|
||||
$string['user'] = 'User';
|
||||
$string['userlistnoncompliant'] = 'Userlist provider missing';
|
||||
$string['userlistexplanation'] = 'This plugin has the base provider but should also implement the userlist provider for full support of privacy functionality.';
|
||||
|
||||
@@ -1711,6 +1711,48 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
$this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out protected purpose contexts if the the site is properly configured.
|
||||
*/
|
||||
public function test_add_request_contexts_with_status_delete_course_no_site_config() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
|
||||
$contextforum = \context_module::instance($forumcm->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
// Create the initial contextlist.
|
||||
$initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
|
||||
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
$contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]);
|
||||
$contextlist->set_component('tool_dataprivacy');
|
||||
$initialcollection->add_contextlist($contextlist);
|
||||
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
$contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $contextforum->id]);
|
||||
$contextlist->set_component('mod_forum');
|
||||
$initialcollection->add_contextlist($contextlist);
|
||||
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
api::add_request_contexts_with_status(
|
||||
$initialcollection, $request->get('id'), contextlist_context::STATUS_APPROVED);
|
||||
|
||||
$newcollection = \tool_dataprivacy\request_contextlist::get_records();
|
||||
$this->assertCount(2, $newcollection);
|
||||
foreach ($newcollection as $contextlist) {
|
||||
$requests = contextlist_context::get_records(['contextlistid' => $contextlist->get('contextlistid')]);
|
||||
$this->assertCount(1, $requests);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out protected purpose contexts if they are already expired.
|
||||
*/
|
||||
@@ -1807,6 +1849,64 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
$this->assertCount(1, $requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out protected purpose contexts if the the site is properly configured.
|
||||
*/
|
||||
public function test_get_approved_contextlist_collection_for_request_delete_course_no_config() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
|
||||
$contextforum = \context_module::instance($forumcm->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
// Create the initial contextlist.
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
|
||||
|
||||
$contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
|
||||
$contextlist->save();
|
||||
$clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
|
||||
'contextid' => $coursecontext->id,
|
||||
'status' => contextlist_context::STATUS_APPROVED,
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$clcontext->save();
|
||||
$rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
|
||||
'requestid' => $request->get('id'),
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$rcl->save();
|
||||
|
||||
$contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'mod_forum']);
|
||||
$contextlist->save();
|
||||
$clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
|
||||
'contextid' => $contextforum->id,
|
||||
'status' => contextlist_context::STATUS_APPROVED,
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$clcontext->save();
|
||||
$rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
|
||||
'requestid' => $request->get('id'),
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$rcl->save();
|
||||
|
||||
$collection = api::get_approved_contextlist_collection_for_request($request);
|
||||
$this->assertCount(2, $collection);
|
||||
|
||||
$list = $collection->get_contextlist_for_component('tool_dataprivacy');
|
||||
$this->assertCount(1, $list);
|
||||
|
||||
$list = $collection->get_contextlist_for_component('mod_forum');
|
||||
$this->assertCount(1, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out protected purpose contexts if they are already expired.
|
||||
*/
|
||||
|
||||
@@ -49,7 +49,7 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "100000" bytes
|
||||
And following "Download" should download between "1" and "130000" bytes
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
And I wait "1" seconds
|
||||
@@ -87,7 +87,7 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Export all of my personal data" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "100000" bytes
|
||||
And following "Download" should download between "1" and "130000" bytes
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
@@ -127,7 +127,7 @@ Feature: Data export from the privacy API
|
||||
And I reload the page
|
||||
And I should see "Download ready" in the "Victim User 1" "table_row"
|
||||
And I open the action menu in "Victim User 1" "table_row"
|
||||
And following "Download" should download between "1" and "100000" bytes
|
||||
And following "Download" should download between "1" and "130000" bytes
|
||||
|
||||
And the following config values are set as admin:
|
||||
| privacyrequestexpiry | 1 | tool_dataprivacy |
|
||||
|
||||
@@ -2222,6 +2222,40 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
|
||||
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the is_context_expired functions when supplied with the front page course.
|
||||
*/
|
||||
public function test_is_context_expired_frontpage() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
|
||||
|
||||
$frontcourse = get_site();
|
||||
$frontcoursecontext = \context_course::instance($frontcourse->id);
|
||||
|
||||
$sitenews = $this->getDataGenerator()->create_module('forum', ['course' => $frontcourse->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $sitenews->id);
|
||||
$sitenewscontext = \context_module::instance($cm->id);
|
||||
|
||||
$user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
|
||||
|
||||
$this->assertFalse(expired_contexts_manager::is_context_expired($frontcoursecontext));
|
||||
$this->assertFalse(expired_contexts_manager::is_context_expired($sitenewscontext));
|
||||
|
||||
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
|
||||
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
|
||||
|
||||
// Protecting the course contextlevel does not impact the front page.
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
|
||||
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
|
||||
|
||||
// Protecting the system contextlevel affects the front page, too.
|
||||
$purposes->system->set('protected', 1)->save();
|
||||
$this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
|
||||
$this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the is_context_expired functions when supplied with an expired course.
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
$plugin->version = 2017111357;
|
||||
$plugin->version = 2017111358;
|
||||
$plugin->requires = 2017111304.00; // Moodle 3.4.4 (Build: 20180517) and upwards.
|
||||
$plugin->component = 'tool_dataprivacy';
|
||||
|
||||
@@ -139,6 +139,36 @@
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree/>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="View login page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">${sitepath}/login/index.php</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<boolProp name="HTTPSampler.monitor">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Get logintoken" enabled="true">
|
||||
<stringProp name="RegexExtractor.useHeaders">false</stringProp>
|
||||
<stringProp name="RegexExtractor.refname">LOGIN_TOKEN</stringProp>
|
||||
<stringProp name="RegexExtractor.regex">type="hidden"\sname="logintoken"\svalue="(\w+)"</stringProp>
|
||||
<stringProp name="RegexExtractor.template">$1$</stringProp>
|
||||
<stringProp name="RegexExtractor.default">0</stringProp>
|
||||
<stringProp name="RegexExtractor.match_number">1</stringProp>
|
||||
</RegexExtractor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
@@ -156,6 +186,13 @@
|
||||
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
<stringProp name="Argument.name">password</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="logintoken" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">${LOGIN_TOKEN}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
<stringProp name="Argument.name">logintoken</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
@@ -173,7 +210,17 @@
|
||||
<boolProp name="HTTPSampler.monitor">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree/>
|
||||
<hashTree>
|
||||
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Logged in" enabled="true">
|
||||
<collectionProp name="Asserion.test_strings">
|
||||
<stringProp name="615717117"><div class="logininfo">You are logged in as</stringProp>
|
||||
</collectionProp>
|
||||
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
|
||||
<boolProp name="Assertion.assume_success">false</boolProp>
|
||||
<intProp name="Assertion.test_type">2</intProp>
|
||||
</ResponseAssertion>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Frontpage logged" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
@@ -335,6 +382,36 @@
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree/>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="View login page" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
<stringProp name="HTTPSampler.port"></stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||
<stringProp name="HTTPSampler.protocol"></stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||
<stringProp name="HTTPSampler.path">${sitepath}/login/index.php</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||
<boolProp name="HTTPSampler.monitor">false</boolProp>
|
||||
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Get logintoken" enabled="true">
|
||||
<stringProp name="RegexExtractor.useHeaders">false</stringProp>
|
||||
<stringProp name="RegexExtractor.refname">LOGIN_TOKEN</stringProp>
|
||||
<stringProp name="RegexExtractor.regex">type="hidden"\sname="logintoken"\svalue="(\w+)"</stringProp>
|
||||
<stringProp name="RegexExtractor.template">$1$</stringProp>
|
||||
<stringProp name="RegexExtractor.default">0</stringProp>
|
||||
<stringProp name="RegexExtractor.match_number">1</stringProp>
|
||||
</RegexExtractor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
@@ -352,6 +429,13 @@
|
||||
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
<stringProp name="Argument.name">password</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="logintoken" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||
<stringProp name="Argument.value">${LOGIN_TOKEN}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
<stringProp name="Argument.name">logintoken</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain"></stringProp>
|
||||
|
||||
@@ -116,7 +116,7 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
|
||||
$this->userrole = create_role('User role', 'lpuserrole', 'learning plan user role description');
|
||||
|
||||
assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* @copyright 2016 Damyon Wiese
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define('NO_OUTPUT_BUFFERING', true);
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->libdir.'/adminlib.php');
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
|
||||
$frm = new stdClass();
|
||||
$frm->username = phpCAS::getUser();
|
||||
$frm->password = 'passwdCas';
|
||||
$frm->logintoken = \core\session\manager::get_login_token();
|
||||
|
||||
// Redirect to a course if multi-auth is activated, authCAS is set to CAS and the courseid is specified.
|
||||
if ($this->config->multiauth && !empty($courseid)) {
|
||||
@@ -167,6 +168,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
|
||||
$frm = new stdClass();
|
||||
$frm->username = 'guest';
|
||||
$frm->password = 'guest';
|
||||
$frm->logintoken = \core\session\manager::get_login_token();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4446,6 +4446,12 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
|
||||
// With newitemid = 0, let's create the question
|
||||
if (!$questionmapping->newitemid) {
|
||||
if ($data->qtype === 'random') {
|
||||
// Ensure that this newly created question is considered by
|
||||
// \qtype_random\task\remove_unused_questions.
|
||||
$data->hidden = 0;
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('question', $data);
|
||||
$this->set_mapping('question', $oldid, $newitemid);
|
||||
// Also annotate them as question_created, we need
|
||||
|
||||
@@ -50,9 +50,6 @@ class edit_backpack_form extends moodleform {
|
||||
$mform->addElement('hidden', 'userid', $USER->id);
|
||||
$mform->setType('userid', PARAM_INT);
|
||||
|
||||
$mform->addElement('hidden', 'backpackurl', BADGE_BACKPACKURL);
|
||||
$mform->setType('backpackurl', PARAM_URL);
|
||||
|
||||
if (isset($this->_customdata['email'])) {
|
||||
// Email will be passed in when we're in the process of verifying the user's email address,
|
||||
// so set the connection status, lock the email field, and provide options to resend the verification
|
||||
@@ -94,7 +91,7 @@ class edit_backpack_form extends moodleform {
|
||||
// We don't need to verify the email address if we're clearing a pending email verification attempt.
|
||||
if (!isset($data['revertbutton'])) {
|
||||
$check = new stdClass();
|
||||
$check->backpackurl = $data['backpackurl'];
|
||||
$check->backpackurl = BADGE_BACKPACKURL;
|
||||
$check->email = $data['email'];
|
||||
|
||||
$bp = new OpenBadgesBackpackHandler($check);
|
||||
|
||||
@@ -40,6 +40,7 @@ class managesubscriptions extends \moodleform {
|
||||
* Defines the form used to add calendar subscriptions.
|
||||
*/
|
||||
public function definition() {
|
||||
global $PAGE;
|
||||
$mform = $this->_form;
|
||||
$eventtypes = calendar_get_allowed_event_types();
|
||||
if (in_array(true, $eventtypes, true) === false) {
|
||||
@@ -86,6 +87,9 @@ class managesubscriptions extends \moodleform {
|
||||
|
||||
// Eventtype: 0 = user, 1 = global, anything else = course ID.
|
||||
$mform->addElement('submit', 'add', get_string('add'));
|
||||
|
||||
// Add the javascript required to enhance this mform.
|
||||
$PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id')]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+5
-8
@@ -2769,14 +2769,6 @@ function calendar_add_subscription($sub) {
|
||||
unset($sub->groupcourseid);
|
||||
}
|
||||
|
||||
// Pull the group id back out of the value. The form saves the value
|
||||
// as "<courseid>-<groupid>" to allow the javascript to work correctly.
|
||||
if (!empty($sub->groupid)) {
|
||||
list($courseid, $groupid) = explode('-', $sub->groupid);
|
||||
$sub->courseid = $courseid;
|
||||
$sub->groupid = $groupid;
|
||||
}
|
||||
|
||||
// Default course id if none is set.
|
||||
if (empty($sub->courseid)) {
|
||||
if ($sub->eventtype === 'site') {
|
||||
@@ -3569,6 +3561,11 @@ function calendar_output_fragment_event_form($args) {
|
||||
$mform->set_data($data);
|
||||
} else {
|
||||
$event = calendar_event::load($eventid);
|
||||
|
||||
if (!calendar_edit_event_allowed($event)) {
|
||||
print_error('nopermissiontoupdatecalendar');
|
||||
}
|
||||
|
||||
$mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
|
||||
$eventdata = $mapper->from_legacy_event_to_data($event);
|
||||
$data = array_merge((array) $eventdata, $data);
|
||||
|
||||
@@ -33,6 +33,7 @@ $categoryid = optional_param('category', null, PARAM_INT);
|
||||
// Used for processing subscription actions.
|
||||
$subscriptionid = optional_param('id', 0, PARAM_INT);
|
||||
$pollinterval = optional_param('pollinterval', 0, PARAM_INT);
|
||||
$groupcourseid = optional_param('groupcourseid', 0, PARAM_INT);
|
||||
$action = optional_param('action', '', PARAM_INT);
|
||||
|
||||
$url = new moodle_url('/calendar/managesubscriptions.php');
|
||||
@@ -61,7 +62,22 @@ if (!calendar_user_can_add_event($course)) {
|
||||
print_error('errorcannotimport', 'calendar');
|
||||
}
|
||||
|
||||
$form = new \core_calendar\local\event\forms\managesubscriptions(null, ['courseid' => $course->id]);
|
||||
// Populate the 'group' select box based on the given 'groupcourseid', if necessary.
|
||||
$groups = [];
|
||||
if (!empty($groupcourseid)) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($groupcourseid);
|
||||
if (!empty($groupcoursedata->groups)) {
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
$groups[$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
$customdata = [
|
||||
'courseid' => $course->id,
|
||||
'groups' => $groups,
|
||||
];
|
||||
$form = new \core_calendar\local\event\forms\managesubscriptions(null, $customdata);
|
||||
$form->set_data(array(
|
||||
'course' => $course->id
|
||||
));
|
||||
|
||||
@@ -533,7 +533,8 @@ class core_calendar_events_testcase extends advanced_testcase {
|
||||
$subscription = new stdClass();
|
||||
$subscription->eventtype = 'group';
|
||||
$subscription->name = 'test';
|
||||
$subscription->groupid = "{$courseid}-{$groupid}";
|
||||
$subscription->courseid = $courseid;
|
||||
$subscription->groupid = $groupid;
|
||||
|
||||
// Trigger and capture the event.
|
||||
$sink = $this->redirectEvents();
|
||||
@@ -668,7 +669,9 @@ class core_calendar_events_testcase extends advanced_testcase {
|
||||
$subscription = new stdClass();
|
||||
$subscription->eventtype = 'group';
|
||||
$subscription->name = 'test';
|
||||
$subscription->groupid = "{$courseid}-{$groupid}";
|
||||
$subscription->courseid = $courseid;
|
||||
$subscription->groupid = $groupid;
|
||||
|
||||
$subscription->id = calendar_add_subscription($subscription);
|
||||
// Now edit it.
|
||||
$subscription->name = 'awesome';
|
||||
@@ -797,7 +800,8 @@ class core_calendar_events_testcase extends advanced_testcase {
|
||||
$subscription = new stdClass();
|
||||
$subscription->eventtype = 'group';
|
||||
$subscription->name = 'test';
|
||||
$subscription->groupid = "{$courseid}-{$groupid}";
|
||||
$subscription->groupid = $groupid;
|
||||
$subscription->courseid = $courseid;
|
||||
$subscription->id = calendar_add_subscription($subscription);
|
||||
|
||||
// Trigger and capture the event.
|
||||
|
||||
@@ -102,6 +102,7 @@ class core_comment_external extends external_api {
|
||||
if ($comments === false) {
|
||||
throw new moodle_exception('nopermissions', 'error', '', 'view comments');
|
||||
}
|
||||
$options = array('blanktarget' => true);
|
||||
|
||||
foreach ($comments as $key => $comment) {
|
||||
|
||||
@@ -110,7 +111,8 @@ class core_comment_external extends external_api {
|
||||
$context->id,
|
||||
$params['component'],
|
||||
'',
|
||||
0);
|
||||
0,
|
||||
$options);
|
||||
}
|
||||
|
||||
$results = array(
|
||||
|
||||
+3
-2
@@ -570,7 +570,7 @@ class comment {
|
||||
$params['itemid'] = $this->itemid;
|
||||
|
||||
$comments = array();
|
||||
$formatoptions = array('overflowdiv' => true);
|
||||
$formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
|
||||
$rs = $DB->get_recordset_sql($sql, $params, $start, $perpage);
|
||||
foreach ($rs as $u) {
|
||||
$c = new stdClass();
|
||||
@@ -717,7 +717,8 @@ class comment {
|
||||
$newcmt->fullname = fullname($USER);
|
||||
$url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid));
|
||||
$newcmt->profileurl = $url->out();
|
||||
$newcmt->content = format_text($newcmt->content, $newcmt->format, array('overflowdiv'=>true));
|
||||
$formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
|
||||
$newcmt->content = format_text($newcmt->content, $newcmt->format, $formatoptions);
|
||||
$newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
|
||||
|
||||
$commentlist = array($newcmt);
|
||||
|
||||
@@ -68,7 +68,7 @@ class comment_manager {
|
||||
ON u.id=c.userid
|
||||
ORDER BY c.timecreated ASC";
|
||||
$rs = $DB->get_recordset_sql($sql, null, $start, $this->perpage);
|
||||
$formatoptions = array('overflowdiv' => true);
|
||||
$formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
|
||||
foreach ($rs as $item) {
|
||||
// Set calculated fields
|
||||
$item->fullname = fullname($item);
|
||||
|
||||
@@ -139,7 +139,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
|
||||
$this->userrole = create_role('User role', 'userrole', 'learning plan user role description');
|
||||
|
||||
assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:competencyview', CAP_ALLOW, $this->userrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id);
|
||||
|
||||
@@ -161,47 +161,55 @@ class provider implements
|
||||
$completioninfo = new \completion_info($course);
|
||||
$completion = $completioninfo->is_enabled();
|
||||
|
||||
if ($completion == COMPLETION_ENABLED) {
|
||||
if ($completion != COMPLETION_ENABLED) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$coursecomplete = $completioninfo->is_course_complete($user->id);
|
||||
$coursecomplete = $completioninfo->is_course_complete($user->id);
|
||||
|
||||
if ($coursecomplete) {
|
||||
$status = get_string('complete');
|
||||
} else {
|
||||
$criteriacomplete = $completioninfo->count_course_user_data($user->id);
|
||||
$ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]);
|
||||
|
||||
$status = ($coursecomplete) ? get_string('complete') : '';
|
||||
$status = (!$criteriacomplete && !$ccompletion->timestarted) ? get_string('notyetstarted', 'completion') :
|
||||
get_string('inprogress', 'completion');
|
||||
|
||||
$completions = $completioninfo->get_completions($user->id);
|
||||
$overall = get_string('nocriteriaset', 'completion');
|
||||
if (!empty($completions)) {
|
||||
if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
|
||||
$overall = get_string('criteriarequiredall', 'completion');
|
||||
} else {
|
||||
$overall = get_string('criteriarequiredany', 'completion');
|
||||
}
|
||||
if (!$criteriacomplete && !$ccompletion->timestarted) {
|
||||
$status = get_string('notyetstarted', 'completion');
|
||||
} else {
|
||||
$status = get_string('inprogress', 'completion');
|
||||
}
|
||||
|
||||
$coursecompletiondata = [
|
||||
'status' => $status,
|
||||
'required' => $overall,
|
||||
];
|
||||
|
||||
$coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
|
||||
$criteria = $completion->get_criteria();
|
||||
$aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
|
||||
$required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
|
||||
get_string('any', 'completion');
|
||||
$data = [
|
||||
'required' => $required,
|
||||
'completed' => transform::yesno($completion->is_complete()),
|
||||
'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
|
||||
];
|
||||
$details = $criteria->get_details($completion);
|
||||
$data = array_merge($data, $details);
|
||||
return $data;
|
||||
}, $completions);
|
||||
return $coursecompletiondata;
|
||||
}
|
||||
|
||||
$completions = $completioninfo->get_completions($user->id);
|
||||
$overall = get_string('nocriteriaset', 'completion');
|
||||
if (!empty($completions)) {
|
||||
if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
|
||||
$overall = get_string('criteriarequiredall', 'completion');
|
||||
} else {
|
||||
$overall = get_string('criteriarequiredany', 'completion');
|
||||
}
|
||||
}
|
||||
|
||||
$coursecompletiondata = [
|
||||
'status' => $status,
|
||||
'required' => $overall,
|
||||
];
|
||||
|
||||
$coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
|
||||
$criteria = $completion->get_criteria();
|
||||
$aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
|
||||
$required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
|
||||
get_string('any', 'completion');
|
||||
$data = [
|
||||
'required' => $required,
|
||||
'completed' => transform::yesno($completion->is_complete()),
|
||||
'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
|
||||
];
|
||||
$details = $criteria->get_details($completion);
|
||||
$data = array_merge($data, $details);
|
||||
return $data;
|
||||
}, $completions);
|
||||
return $coursecompletiondata;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -193,13 +193,31 @@ class core_completion_privacy_test extends \core_privacy\tests\provider_testcase
|
||||
$hasno = array_search('No', $coursecompletion1['criteria'], true);
|
||||
$this->assertFalse($hasno);
|
||||
$coursecompletion2 = \core_completion\privacy\provider::get_course_completion_info($user2, $this->course);
|
||||
$hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
|
||||
$hasyes = array_search('Yes', $coursecompletion2['criteria'], true);
|
||||
$this->assertFalse($hasyes);
|
||||
$coursecompletion3 = \core_completion\privacy\provider::get_course_completion_info($user3, $this->course);
|
||||
$hasno = array_search('No', $coursecompletion1['criteria'], true);
|
||||
$hasno = array_search('No', $coursecompletion3['criteria'], true);
|
||||
$this->assertFalse($hasno);
|
||||
$coursecompletion4 = \core_completion\privacy\provider::get_course_completion_info($user4, $this->course);
|
||||
$hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
|
||||
$hasyes = array_search('Yes', $coursecompletion4['criteria'], true);
|
||||
$this->assertFalse($hasyes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting course completion information with completion disabled.
|
||||
*/
|
||||
public function test_get_course_completion_info_completion_disabled() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 0]);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
$coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $course);
|
||||
|
||||
$this->assertTrue(is_array($coursecompletion));
|
||||
$this->assertEmpty($coursecompletion);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"homepage": "https://moodle.org",
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "6.5.*",
|
||||
"phpunit/dbUnit": "3.0.*",
|
||||
"phpunit/dbunit": "3.0.*",
|
||||
"moodlehq/behat-extension": "3.34.2",
|
||||
"mikey179/vfsStream": "^1.6"
|
||||
"mikey179/vfsstream": "^1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests.
|
||||
*
|
||||
* @package filter_activitynames
|
||||
* @category test
|
||||
* @copyright 2018 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/filter/activitynames/filter.php'); // Include the code to test.
|
||||
|
||||
/**
|
||||
* Test case for the activity names auto-linking filter.
|
||||
*
|
||||
* @copyright 2018 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class filter_activitynames_filter_testcase extends advanced_testcase {
|
||||
|
||||
public function test_links() {
|
||||
global $CFG;
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Create a test course.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
|
||||
// Create two pages that will be linked to.
|
||||
$page1 = $this->getDataGenerator()->create_module('page',
|
||||
['course' => $course->id, 'name' => 'Test 1']);
|
||||
$page2 = $this->getDataGenerator()->create_module('page',
|
||||
['course' => $course->id, 'name' => 'Test (2)']);
|
||||
|
||||
// Format text with all three entries in HTML.
|
||||
$html = '<p>Please read the two pages Test 1 and <i>Test (2)</i>.</p>';
|
||||
$filtered = format_text($html, FORMAT_HTML, array('context' => $context));
|
||||
|
||||
// Find all the glossary links in the result.
|
||||
$matches = [];
|
||||
preg_match_all('~<a class="autolink" title="([^"]*)" href="[^"]*/mod/page/view.php\?id=([0-9]+)">([^<]*)</a>~',
|
||||
$filtered, $matches);
|
||||
|
||||
// There should be 3 links links.
|
||||
$this->assertEquals(2, count($matches[1]));
|
||||
|
||||
// Check text of title attribute.
|
||||
$this->assertEquals($page1->name, $matches[1][0]);
|
||||
$this->assertEquals($page2->name, $matches[1][1]);
|
||||
|
||||
// Check the ids in the links.
|
||||
$this->assertEquals($page1->cmid, $matches[2][0]);
|
||||
$this->assertEquals($page2->cmid, $matches[2][1]);
|
||||
|
||||
// Check the link text.
|
||||
$this->assertEquals($page1->name, $matches[3][0]);
|
||||
$this->assertEquals($page2->name, $matches[3][1]);
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,52 @@ class filter_glossary_filter_testcase extends advanced_testcase {
|
||||
$this->assertEquals($glossary->name . ': normal', $matches[2][2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test brackets.
|
||||
*/
|
||||
public function test_brackets() {
|
||||
global $CFG;
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Enable glossary filter at top level.
|
||||
filter_set_global_state('glossary', TEXTFILTER_ON);
|
||||
$CFG->glossary_linkentries = 1;
|
||||
|
||||
// Create a test course.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
|
||||
// Create a glossary.
|
||||
$glossary = $this->getDataGenerator()->create_module('glossary',
|
||||
array('course' => $course->id, 'mainglossary' => 1));
|
||||
|
||||
// Create two entries with ampersands and one normal entry.
|
||||
/** @var mod_glossary_generator $generator */
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
|
||||
$simple = $generator->create_content($glossary, array('concept' => 'simple'));
|
||||
$withbrackets = $generator->create_content($glossary, array('concept' => 'more complex (perhaps)'));
|
||||
$test2 = $generator->create_content($glossary, array('concept' => 'Test (2)'));
|
||||
|
||||
// Format text with all three entries in HTML.
|
||||
$html = '<p>Some thigns are simple. Others are more complex (perhaps). Test (2).</p>';
|
||||
$filtered = format_text($html, FORMAT_HTML, array('context' => $context));
|
||||
|
||||
// Find all the glossary links in the result.
|
||||
$matches = array();
|
||||
preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches);
|
||||
|
||||
// There should be 3 glossary links.
|
||||
$this->assertEquals(3, count($matches[1]));
|
||||
$this->assertEquals($simple->id, $matches[1][0]);
|
||||
$this->assertEquals($withbrackets->id, $matches[1][1]);
|
||||
$this->assertEquals($test2->id, $matches[1][2]);
|
||||
|
||||
// Check text and escaping of title attribute.
|
||||
$this->assertEquals($glossary->name . ': simple', $matches[2][0]);
|
||||
$this->assertEquals($glossary->name . ': more complex (perhaps)', $matches[2][1]);
|
||||
$this->assertEquals($glossary->name . ': Test (2)', $matches[2][2]);
|
||||
}
|
||||
|
||||
public function test_exclude_excludes_link_to_entry_with_alias() {
|
||||
global $CFG, $GLOSSARY_EXCLUDEENTRY;
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Automatically generated strings for Moodle installer
|
||||
*
|
||||
* Do not edit this file manually! It contains just a subset of strings
|
||||
* needed during the very first steps of installation. This file was
|
||||
* generated automatically by export-installer.php (which is part of AMOS
|
||||
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
|
||||
* list of strings defined in /install/stringnames.txt.
|
||||
*
|
||||
* @package installer
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['thislanguage'] = 'Bamanankan';
|
||||
@@ -43,7 +43,7 @@ $string['cannotunzipfile'] = 'No s\'ha pogut descomprimir el fitxer';
|
||||
$string['componentisuptodate'] = 'El component està actualitzat';
|
||||
$string['dmlexceptiononinstall'] = '<p>S\'ha produït un error de la base de dades [{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'Ha fallat la comprovació del fitxer baixat';
|
||||
$string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu-ho a provar';
|
||||
$string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu a provar-ho';
|
||||
$string['missingrequiredfield'] = 'Falta algun camp necessari';
|
||||
$string['remotedownloaderror'] = '<p>No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els paràmetres del servidor intermediari. Es recomana vivament l\'extensió cURL de PHP.</p>
|
||||
<p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació «{$a->dest}» del vostre servidor i descomprimir-lo allí.</p>';
|
||||
|
||||
@@ -38,7 +38,7 @@ $string['clitypevalue'] = 'πληκτρολογήστε μια τιμή';
|
||||
$string['clitypevaluedefault'] = 'πληκτρολογήστε μια τιμή· πατήστε Enter για να χρησιμοποιήσετε τηνπροεπιλεγμένη τιμή ({$a})';
|
||||
$string['cliunknowoption'] = 'Μη αναγνωρίσιμες επιλογές: {$a}<br />
|
||||
Παρακαλούμε χρησιμοποιήστε την επιλογή --βοήθεια';
|
||||
$string['cliyesnoprompt'] = 'πατώντας ν (σημαίνει ναι) αλλιώς πατώντας ο (σημαίνει όχι)';
|
||||
$string['cliyesnoprompt'] = 'πατώντας y (σημαίνει yes=ναι) αλλιώς πατώντας n (σημαίνει no=όχι)';
|
||||
$string['environmentrequireinstall'] = 'απαιτείται να εγκατασταθεί/ ενεργοποιηθεί';
|
||||
$string['environmentrequireversion'] = 'απαιτείται η έκδοση {$a->needed} ενώ εσείς έχετε την {$a->current}';
|
||||
$string['upgradekeyset'] = 'Κλειδί αναβάθμισης (αφήστε κενό για να μην το ορίσετε)';
|
||||
|
||||
@@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['admindirname'] = 'Φάκελος διαχειριστή';
|
||||
$string['availablelangs'] = 'Λίστα διαθέσιμων πακέτων γλωσσών';
|
||||
$string['chooselanguagehead'] = 'Επιλογή γλώσσας';
|
||||
$string['chooselanguagesub'] = 'Παρακαλούμε, επιλέξτε γλώσσα για την εγκατάσταση ΜΟΝΟ. Θα μπορείτε να επιλέξετε γλώσσα ιστοσελίδας και χρηστών σε μια μετέπειτα οθόνη.';
|
||||
$string['chooselanguagesub'] = 'Παρακαλούμε, επιλέξτε γλώσσα για την εγκατάσταση. Αυτή η γλώσσα θα χρησιμοποιηθεί επίσης ως προεπιλεγμένη γλώσσα για τον ιστότοπο, αν και μπορεί να αλλάξει αργότερα.';
|
||||
$string['clialreadyconfigured'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να εγκαταστήσετε το Moodle για αυτόν τον ιστότοπο.';
|
||||
$string['clialreadyinstalled'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να αναβαθμίσετε το Moodle για αυτόν τον ιστότοπο.';
|
||||
$string['cliinstallheader'] = 'Moodle {$a} πρόγραμμα εγκατάστασης γραμμής εντολών';
|
||||
@@ -75,4 +75,4 @@ $string['welcomep40'] = 'Το πακέτο περιλαμβάνει επίσης
|
||||
$string['welcomep50'] = 'Η χρήση όλων των εφαρμογών σε αυτό το πακέτο υπόκειται στις αντίστοιχες άδειες. Ολόκληρο το πακέτο <strong>{$a->installername}</strong> είναι <a href="http://www.opensource.org/docs/definition_plain.html">λογισμικό ανοικτού κώδικα</a> και διανέμεται με την <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a> άδεια.';
|
||||
$string['welcomep60'] = 'Οι παρακάτω σελίδες θα σας καθοδηγήσουν με εύκολα βήματα στην εγκατάσταση και ρύθμιση του <strong>Moodle</strong> στον υπολογιστή σας. Μπορείτε να δεχθείτε τις προεπιλεγμένες ρυθμίσεις ή προαιρετικά, να τις τροποποιήσετε ανάλογα με τις ανάγκες σας.';
|
||||
$string['welcomep70'] = 'Πατήστε το κουμπί "Συνέχεια" για να συνεχίσετε με την εκγατάσταση του <strong>Moodle</strong>.';
|
||||
$string['wwwroot'] = 'Διεύθυνση ιστοσελίδας';
|
||||
$string['wwwroot'] = 'Διεύθυνση ιστού';
|
||||
|
||||
@@ -30,5 +30,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['parentlanguage'] = '';
|
||||
$string['thisdirection'] = 'ltr';
|
||||
$string['thislanguage'] = 'Ελληνικά';
|
||||
|
||||
@@ -32,12 +32,13 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['clianswerno'] = 'e';
|
||||
$string['cliansweryes'] = 'b';
|
||||
$string['cliincorrectvalueerror'] = 'Errorea, "{$a->value}" balorea ez da egokia "{$a->option}"-rako';
|
||||
$string['cliincorrectvalueretry'] = 'Balore ezegokia, mesedez saiatu berriz';
|
||||
$string['clitypevalue'] = 'Sartu balorea';
|
||||
$string['clitypevaluedefault'] = 'sartu balorea, sakatu Enter-i berezko balorea erabiltzeko ({$a})';
|
||||
$string['cliunknowoption'] = 'Aukera ezezagunak:{$a}
|
||||
Mesedez, erabili --laguntza aukera.';
|
||||
$string['cliincorrectvalueerror'] = 'Errorea, "{$a->value}" balioa ez da egokia "{$a->option}"-rako';
|
||||
$string['cliincorrectvalueretry'] = 'Balio ezegokia, mesedez saiatu berriz';
|
||||
$string['clitypevalue'] = 'Sartu balioa';
|
||||
$string['clitypevaluedefault'] = 'sartu balioa, sakatu Enter-i lehenetsitako balioa erabiltzeko ({$a})';
|
||||
$string['cliunknowoption'] = 'Aukera ezezagunak:
|
||||
{$a}
|
||||
Mesedez, erabili --help aukera.';
|
||||
$string['cliyesnoprompt'] = 'idatzi b (bai esateko) edo e (ez esateko)';
|
||||
$string['environmentrequireinstall'] = 'derrigorrezkoa da instalatuta eta gaituta izatea';
|
||||
$string['environmentrequireversion'] = '{$a->needed} bertsioa beharrezkoa da eta zu {$a->current} ari zara egikaritzen';
|
||||
|
||||
@@ -43,7 +43,7 @@ $string['cannotsavezipfile'] = 'Ezin da ZIP fitxategia gorde';
|
||||
$string['cannotunzipfile'] = 'Ezin da fitxategia deskonprimatu';
|
||||
$string['componentisuptodate'] = 'Osagaia eguneratu da';
|
||||
$string['dmlexceptiononinstall'] = '<p>Datu-basearen errorea gertatu da [{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'Jaitsitako fitxategiaren konprobazioak kale egin du.';
|
||||
$string['downloadedfilecheckfailed'] = 'Jaitsitako fitxategiaren egiaztatzeak huts egin du.';
|
||||
$string['invalidmd5'] = 'Kontrolerako aldagaia gaizki zegoen - saiatu berriz ere';
|
||||
$string['missingrequiredfield'] = 'Beharrezko eremuren bat falta da.';
|
||||
$string['remotedownloaderror'] = '<p>Errorea osagaia zure zerbitzarian jaistean, mesedez egiaztatu proxy-ezarpenak, PHP cURL luzapena erabat gomendatzen da.</p>
|
||||
|
||||
@@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['admindirname'] = 'Admin direktorioa';
|
||||
$string['availablelangs'] = 'Hizkuntza-pakete eskuragarriak';
|
||||
$string['chooselanguagehead'] = 'Aukeratu hizkuntza bat';
|
||||
$string['chooselanguagesub'] = 'Mesedez, aukeratu instalaziorako hizkuntza bat. Hizkuntza hori erabiliko da gunearen berezko hizkuntza gisa, baina aurrerago alda daiteke.';
|
||||
$string['chooselanguagesub'] = 'Mesedez, aukeratu instalaziorako hizkuntza bat. Hizkuntza hori erabiliko da gunearen hizkuntza lehenetsi gisa, baina aurrerago alda daiteke.';
|
||||
$string['clialreadyconfigured'] = 'Dagoeneko badago config.php konfigurazio-fitxategia. Mesedez erabili admin/cli/install_database.php Moodle gune honetan instalatu nahi baduzu.';
|
||||
$string['clialreadyinstalled'] = 'Dagoeneko badago config.php konfigurazio-fitxategia. Mesedez erabili admin/cli/upgrade.php zure Moodle gunea eguneratu nahi baduzu.';
|
||||
$string['cliinstallheader'] = 'Moodle {$a} komando-lerro bidezko instalaziorako programa';
|
||||
@@ -71,7 +71,7 @@ $string['pathserrcreatedataroot'] = 'Instalatzaileak ezin du datu-karpeta ({$a->
|
||||
$string['pathshead'] = 'Egiaztatu bideak';
|
||||
$string['pathsrodataroot'] = 'Dataroot direktorioa ez da idazteko modukoa.';
|
||||
$string['pathsroparentdataroot'] = 'Goragoko karpeta ({$a->parent}) ez da idazteko modukoa. Instalatzaileak ezin du datu-karpeta ({$a->dataroot}) sortu.';
|
||||
$string['pathssubadmindir'] = 'Web ostalari gutxi batzuk /admin URL berezi gisa erabiltzen dute kontrol-panel edo antzekora sarbidea emateko. Zoritxarrez, honek Moodle-ren kudeatze-orrien berezko kokapenarekin gatazka sortzen du. Hau konpondu dezakezu zure instalazioko admin karpeta berrizendatuz, eta izen berria hemen sartuta. Adibidez <em>moodleadmin</em>. Honek Moodle-ko admin estekak konponduko du.';
|
||||
$string['pathssubadmindir'] = 'Web ostalari gutxi batzuk /admin URL berezi gisa erabiltzen dute kontrol-panel edo antzekora sarbidea emateko. Zoritxarrez, honek Moodle-ren kudeatze-orrien lehenetsitako kokapenarekin gatazka sortzen du. Hau konpondu dezakezu zure instalazioko admin karpeta berrizendatuz, eta izen berria hemen sartuta. Adibidez <em>moodleadmin</em>. Honek Moodle-ko admin estekak konponduko du.';
|
||||
$string['pathssubdataroot'] = '<p>Moodle-k erabiltzaileek igotako fitxategien edukiak bilduko dituen direktorio bat.</p>
|
||||
<p>Direktorio honetan web-zerbitzariaren erabiltzaileak irakurtzeko eta idazteko baimena izan beharko ditu (normalean \'www-data\', \'nobody\', edo \'apache\').</p>
|
||||
<p>Ez litzateke web bidez eskuragarri egon beharko.</p>
|
||||
@@ -90,14 +90,13 @@ $string['phpversionhelp'] = '<p>Moodle-k PHP 5.6.5 edo 7.1 bertsioetako bat beha
|
||||
<p>PHP eguneratu edo PHP bertsio berriagoa duen zerbitzari batera jo.</p>';
|
||||
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
|
||||
$string['welcomep20'] = 'Orri hau ikusten baduzu <strong>{$a->packname} {$a->packversion}</strong> paketea zure ordenagailuan instalatu ahal izan duzu. Zorionak!';
|
||||
$string['welcomep30'] = '<strong>{$a->installername}</strong>ren bertsio honek <strong>Moodle</strong>k
|
||||
zure ordenadorean funtzionatzeko behar diren aplikazioak dauzka,
|
||||
bereziki:';
|
||||
$string['welcomep30'] = '<strong>{$a->installername}</strong>ren bertsio honek <strong>Moodle</strong>-k
|
||||
zure ordenagailuan funtzionatzeko behar diren aplikazioak dauzka, zehazki hurrengoak:';
|
||||
$string['welcomep40'] = 'Paketeak ere zera dauka: <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
|
||||
$string['welcomep50'] = 'Paketeko aplikazio guztien erabilpena dagozkien lizentziek
|
||||
arautzen dute. <strong>{$a->installername}</strong> aplikazioak
|
||||
<a href="http://www.opensource.org/docs/definition_plain.html">kode irekia</a> dauka eta
|
||||
<a href="http://www.gnu.org/copyleft/gpl.html">GPL</a> lizentziapean banatzen da.';
|
||||
$string['welcomep60'] = 'Datozen orriek urrats erraz batzuen bidez gidatuko zaituzte <strong>Moodle</strong> zure ordenagailuan instalatu eta konfiguratzeko. Aholkatzen diren lehentsitako baloreak mantendu edo, nahi izanez gero, alda ditzakezu zure beharrei erantzun diezaieten.';
|
||||
$string['welcomep70'] = '"Hurrengoa" botoia sakatu <strong>Moodle</strong>ren konfigurazioarekin jarraitzeko.';
|
||||
$string['welcomep60'] = 'Datozen orriek urrats erraz batzuen bidez gidatuko zaituzte <strong>Moodle</strong> zure ordenagailuan instalatu eta konfiguratzeko. Aholkatzen diren lehenetsitako balioak mantendu edo, nahi izanez gero, alda ditzakezu zure beharrei erantzun diezaieten.';
|
||||
$string['welcomep70'] = 'Egin klik "Hurrengoa" botoian <strong>Moodle</strong>ren konfigurazioarekin jarraitzeko.';
|
||||
$string['wwwroot'] = 'Web helbidea';
|
||||
|
||||
@@ -32,3 +32,4 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['language'] = 'भाषा';
|
||||
$string['next'] = 'अगला';
|
||||
$string['reload'] = 'सीमा से अधिक लादना';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['clianswerno'] = 'n';
|
||||
$string['cliansweryes'] = 'y';
|
||||
$string['cliansweryes'] = 'd';
|
||||
$string['cliincorrectvalueerror'] = 'Pogreška, netočna vrijednost "{$a->value}" za "{$a->option}"';
|
||||
$string['cliincorrectvalueretry'] = 'Netočna vrijednost, pokušajte ponovno';
|
||||
$string['clitypevalue'] = 'unesite vrijednost';
|
||||
@@ -40,3 +40,4 @@ $string['cliunknowoption'] = 'Nepoznate opcije: {$a} Molimo koristite --help opc
|
||||
$string['cliyesnoprompt'] = 'unesite y (znači da) ili n (znači ne)';
|
||||
$string['environmentrequireinstall'] = 'je neophodno instalirati/omogućiti';
|
||||
$string['environmentrequireversion'] = 'neophodna inačica je {$a->needed}, a vi trenutačno koristite inačicu {$a->current}';
|
||||
$string['upgradekeyset'] = 'Ključ za ažuriranje (ostavite prazno kako ga ne bi zadali)';
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cannotcreatedboninstall'] = '<p>Ne mogu stvoriti bazu podataka.</p>
|
||||
<p>Navedena baza podataka ne postoji i korisnik nema prava stvaranja baze podataka.</p>
|
||||
<p>Administrator treba provjeriti postavke baze podataka.</p>';
|
||||
$string['cannotcreatelangdir'] = 'Nije moguće stvoriti lang mapu';
|
||||
$string['cannotcreatetempdir'] = 'Nije moguće stvoriti privremenu (TEMP) mapu';
|
||||
$string['cannotdownloadcomponents'] = 'Nije moguće preuzimanje komponenti';
|
||||
@@ -39,6 +42,7 @@ $string['cannotsavemd5file'] = 'Nije moguće pohraniti md5 datoteku';
|
||||
$string['cannotsavezipfile'] = 'Nije moguće pohraniti ZIP datoteku';
|
||||
$string['cannotunzipfile'] = 'Nije moguće otpakirati datoteku';
|
||||
$string['componentisuptodate'] = 'Komponenta je dostupna u svojoj najnovijoj inačici.';
|
||||
$string['dmlexceptiononinstall'] = '<p>Dogodila se pogreška baze podataka [{$a->errorcode}].<br />{$a->debuginfo}';
|
||||
$string['downloadedfilecheckfailed'] = 'Došlo je do pogreške pri provjeri preuzete datoteke';
|
||||
$string['invalidmd5'] = 'Neispravna md5 datoteka';
|
||||
$string['missingrequiredfield'] = 'Nedostaje neko obvezatno polje';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['admindirname'] = 'Admin mapa';
|
||||
$string['availablelangs'] = 'Popis dostupnih jezika';
|
||||
$string['availablelangs'] = 'Popis dostupnih jezičnih paketa';
|
||||
$string['chooselanguagehead'] = 'Odaberite jezik';
|
||||
$string['chooselanguagesub'] = 'Molimo odaberite jezik instalacije. Ovaj jezik će biti korišten kao zadani jezik sustava, a kasnije to možete lagano promijeniti.';
|
||||
$string['clialreadyconfigured'] = 'Datoteka config.php već postoji. Upotrijebite naredbu admin/cli/install_database.php ako želite nastaviti instalaciju.';
|
||||
@@ -68,7 +68,7 @@ $string['pathsroparentdataroot'] = 'Nije moguće zapisivati podatke u nadređenu
|
||||
$string['pathssubadmindir'] = 'Manji broj webhosting tvrtki koristi /admin kao posebni URL za Vaš pristup upravljanju vašim hosting paketom. Nažalost, to rezultira konfliktom sa standardnom lokacijom za Moodle administratorsku stranicu. Navedenu lokaciju unutar Moodle sustava možete preimenovati. Na primjer: <br /> <br /><em>moodleadmin</em><br /> <br />
|
||||
Ovo će promijeniti administratorsku poveznicu na Moodle sustavu u novu vrijednost.';
|
||||
$string['pathssubdataroot'] = 'Mora postojati mapa u koju Moodle može pohraniti prenešene datoteke. Korisnik pod kojim je pokrenut web server (obično \'nobody\' ili \'apache\') bi morao imati mogućnost čitanja/pisanja podataka u toj mapi, ali oni ne bi trebali biti dostupni direktno preko weba. Instalacijska skripta će pokušati stvoriti navedenu mapu ako ista ne postoji.';
|
||||
$string['pathssubdirroot'] = 'Puna putanja (PATH) do Moodle instalacije.';
|
||||
$string['pathssubdirroot'] = '<p>Puna putanja (PATH) do Moodle instalacije.</p>';
|
||||
$string['pathssubwwwroot'] = 'Unesite punu web adresu putem koje će se pristupati vašem Moodle sustavu.
|
||||
Moodle sustavu NIJE MOGUĆE pristupiti preko više URL-ova, odaberite onaj koji vam najviše odgovara.
|
||||
Ako vaš poslužitelj ima višestruke javne adrese, onda morate postaviti tzv. permanent redirect na sve osim ove adrese.
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['language'] = 'Tungumál';
|
||||
$string['moodlelogo'] = 'Moodle merkið';
|
||||
$string['next'] = 'Áfram';
|
||||
$string['previous'] = 'Til baka';
|
||||
$string['reload'] = 'Endurhlaða';
|
||||
|
||||
@@ -73,9 +73,9 @@ $string['pathssubdataroot'] = '<p>ユーザによってアップロードされ
|
||||
<p>ウェブからは直接アクセスできないようにしてください。</p>
|
||||
<p>現在ディレクトリが存在しない場合、インストレーションプロセスは作成を試みます。</p';
|
||||
$string['pathssubdirroot'] = '<p>Moodleコードを含むディレクトリに関するフルパスです。</p>';
|
||||
$string['pathssubwwwroot'] = '<p>Moodleにアクセスすることのできるフルウェブアドレスです。例えばユーザがブラウザのアドレスバーに入力してMoodleにアクセスするためのアドレスです。</p>
|
||||
$string['pathssubwwwroot'] = '<p>Moodleにアクセスできるフルウェブアドレスです。例えばユーザがブラウザのアドレスバーに入力してMoodleにアクセスするためのアドレスです。</p>
|
||||
|
||||
<p>複数アドレスを使用してMoodleにアクセスすることはできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
|
||||
<p>複数アドレスを使用したMoodleへのアクセスはできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
|
||||
|
||||
<p>あなたのサイトにインターネットおよび内部ネットワーク (イントラネットと呼ばれます) からアクセスできる場合、ここではパブリックアドレスを使用してください。</p>
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['clianswerno'] = 'n';
|
||||
$string['cliansweryes'] = 's';
|
||||
$string['cliincorrectvalueerror'] = 'Erro: o valor "{$a->value}" não é permitido para a opção "{$a->option}"';
|
||||
$string['cliincorrectvalueretry'] = 'Valor incorreto, por favor tente novamente';
|
||||
$string['cliincorrectvalueretry'] = 'Valor incorreto, por favor, tente novamente';
|
||||
$string['clitypevalue'] = 'introduza valor';
|
||||
$string['clitypevaluedefault'] = 'introduza valor, pressione a tecla Enter para usar o valor predefinido ({$a})';
|
||||
$string['cliunknowoption'] = 'Opções desconhecidas: {$a}. Por favor use a opção --help';
|
||||
$string['cliunknowoption'] = 'Opções desconhecidas: {$a}. Por favor, use a opção --help';
|
||||
$string['cliyesnoprompt'] = 'digite s (para sim) ou n (para não)';
|
||||
$string['environmentrequireinstall'] = 'deve estar instalada e ativa';
|
||||
$string['environmentrequireversion'] = 'é requerida a versão {$a->needed} e está a correr a versão {$a->current}';
|
||||
|
||||
@@ -54,6 +54,7 @@ $string['paths'] = 'Căi';
|
||||
$string['pathserrcreatedataroot'] = 'Data directory ({$a->dataroot}) nu poate fi creat de către installer.';
|
||||
$string['pathshead'] = 'Confirmare căi';
|
||||
$string['pathsrodataroot'] = 'Directorul dataroot nu poate fi scris.';
|
||||
$string['pathsroparentdataroot'] = 'Directorul parent ({$a->parent}) nu poate fi scris. Directorul data ({$a->dataroot}) nu poate fi creat de persoana care îl instalează.';
|
||||
$string['pathssubdataroot'] = '<p>Un director unde Moodle va stoca tot conținutul unui fișier încărcat de către utilizatori.</p>
|
||||
<p>Acest director trebuie să poată fi citit și scris de către utilizatrii serverului web (de obicei \'www-data\', \'nobody\', or \'apache\').</p>
|
||||
<p>Nu trebuie să fie direct accesibil de pe web.</p>
|
||||
|
||||
@@ -30,9 +30,15 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cannotcreatedboninstall'] = '<p> డేటాబేస్ను సృష్టించలేరు. </ p>
|
||||
<p> పేర్కొన్న డేటాబేస్ ఉనికిలో లేదు మరియు ఇచ్చిన యూజర్కు డాటాబేస్ను సృష్టించడానికి అనుమతి లేదు. </ p>
|
||||
<p> సైట్ నిర్వాహకుడు డేటాబేస్ కాన్ఫిగరేషన్ను ధృవీకరించాలి. </ p>';
|
||||
$string['cannotcreatelangdir'] = 'భాష డైరెక్టరీని సృష్టించలేం';
|
||||
$string['cannotcreatetempdir'] = 'టెంప్ డైరెక్టరీని సృష్టించలేం';
|
||||
$string['cannotdownloadcomponents'] = 'కాంపొనెంట్లను దించుకోలేరు';
|
||||
$string['cannotdownloadzipfile'] = 'ZIP దస్త్రాలను దించుకోలేరు';
|
||||
$string['cannotfindcomponent'] = 'కాంపొనెంటు కనబడలేదు';
|
||||
$string['cannotsavemd5file'] = 'Md5 ఫైల్ను సేవ్ చేయలేరు';
|
||||
$string['cannotsavezipfile'] = 'జిప్ ఫైల్ను సేవ్ చేయడం సాధ్యపడదు';
|
||||
$string['cannotunzipfile'] = 'ఫైల్ అన్జిప్ చేయలేరు';
|
||||
$string['componentisuptodate'] = 'కాంపొనెంటు తాజాగా ఉంది';
|
||||
|
||||
@@ -39,3 +39,4 @@ $string['reseteveryonesprofile'] = 'Reset profile for all users';
|
||||
$string['resetpage'] = 'Reset page to default';
|
||||
$string['reseterror'] = 'There was an error resetting your page';
|
||||
$string['privacy:metadata:core_my:preference:user_home_page_preference'] = 'The user home page preference configured for the Dashboard page.';
|
||||
$string['unabletoaccess'] = 'As a security precaution, you may not access another user\'s dashboard';
|
||||
|
||||
+43
-6
@@ -330,6 +330,7 @@ function get_role_definitions_uncached(array $roleids) {
|
||||
$sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {context} ctx ON rc.contextid = ctx.id
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
WHERE rc.roleid $sql
|
||||
ORDER BY ctx.path, rc.roleid, rc.capability";
|
||||
$rs = $DB->get_recordset_sql($sql, $params);
|
||||
@@ -1144,7 +1145,17 @@ function is_safe_capability($capability) {
|
||||
*/
|
||||
function get_local_override($roleid, $contextid, $capability) {
|
||||
global $DB;
|
||||
return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
|
||||
|
||||
return $DB->get_record_sql("
|
||||
SELECT rc.*
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {capability} cap ON rc.capability = cap.name
|
||||
WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
|
||||
'roleid' => $roleid,
|
||||
'contextid' => $contextid,
|
||||
'capability' => $capability,
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1290,6 +1301,11 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
|
||||
$context = context::instance_by_id($contextid);
|
||||
}
|
||||
|
||||
// Capability must exist.
|
||||
if (!$capinfo = get_capability_info($capability)) {
|
||||
throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
|
||||
}
|
||||
|
||||
if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
|
||||
unassign_capability($capability, $roleid, $context->id);
|
||||
return true;
|
||||
@@ -1337,6 +1353,11 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
|
||||
function unassign_capability($capability, $roleid, $contextid = null) {
|
||||
global $DB;
|
||||
|
||||
// Capability must exist.
|
||||
if (!$capinfo = get_capability_info($capability)) {
|
||||
throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
|
||||
}
|
||||
|
||||
if (!empty($contextid)) {
|
||||
if ($contextid instanceof context) {
|
||||
$context = $contextid;
|
||||
@@ -1391,6 +1412,7 @@ function get_roles_with_capability($capability, $permission = null, $context = n
|
||||
FROM {role} r
|
||||
WHERE r.id IN (SELECT rc.roleid
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
WHERE rc.capability = :capname
|
||||
$contextsql
|
||||
$permissionsql)";
|
||||
@@ -2177,6 +2199,9 @@ function update_capabilities($component = 'moodle') {
|
||||
|
||||
$DB->insert_record('capabilities', $capability, false);
|
||||
|
||||
// Flush the cached, as we have changed DB.
|
||||
cache::make('core', 'capabilities')->delete('core_capabilities');
|
||||
|
||||
if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
|
||||
if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
|
||||
foreach ($rolecapabilities as $rolecapability){
|
||||
@@ -2230,9 +2255,6 @@ function capabilities_cleanup($component, $newcapdef = null) {
|
||||
if (empty($newcapdef) ||
|
||||
array_key_exists($cachedcap->name, $newcapdef) === false) {
|
||||
|
||||
// Remove from capabilities cache.
|
||||
$DB->delete_records('capabilities', array('name'=>$cachedcap->name));
|
||||
$removedcount++;
|
||||
// Delete from roles.
|
||||
if ($roles = get_roles_with_capability($cachedcap->name)) {
|
||||
foreach($roles as $role) {
|
||||
@@ -2241,6 +2263,13 @@ function capabilities_cleanup($component, $newcapdef = null) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from role_capabilities for any old ones.
|
||||
$DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
|
||||
|
||||
// Remove from capabilities cache.
|
||||
$DB->delete_records('capabilities', array('name' => $cachedcap->name));
|
||||
$removedcount++;
|
||||
} // End if.
|
||||
}
|
||||
}
|
||||
@@ -2305,10 +2334,12 @@ function role_context_capabilities($roleid, context $context, $cap = '') {
|
||||
}
|
||||
|
||||
$sql = "SELECT rc.*
|
||||
FROM {role_capabilities} rc, {context} c
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {context} c ON rc.contextid = c.id
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
WHERE rc.contextid in $contexts
|
||||
AND rc.roleid = ?
|
||||
AND rc.contextid = c.id $search
|
||||
$search
|
||||
ORDER BY c.contextlevel DESC, rc.capability DESC";
|
||||
|
||||
$capabilities = array();
|
||||
@@ -3024,6 +3055,7 @@ function get_switchable_roles(context $context) {
|
||||
SELECT r.id, r.name, r.shortname, rn.name AS coursealias
|
||||
FROM (SELECT DISTINCT rc.roleid
|
||||
FROM {role_capabilities} rc
|
||||
|
||||
$extrajoins
|
||||
$extrawhere) idlist
|
||||
JOIN {role} r ON r.id = idlist.roleid
|
||||
@@ -3286,6 +3318,7 @@ function get_users_by_capability(context $context, $capability, $fields = '', $s
|
||||
$params = array_merge($params, $params2);
|
||||
$sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
JOIN {context} ctx on rc.contextid = ctx.id
|
||||
WHERE rc.contextid $incontexts AND rc.capability $incaps";
|
||||
|
||||
@@ -4381,6 +4414,7 @@ function get_roles_with_cap_in_context($context, $capability) {
|
||||
$sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {context} ctx ON ctx.id = rc.contextid
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
|
||||
ORDER BY rc.roleid ASC, ctx.depth DESC";
|
||||
$params = array('cap'=>$capability);
|
||||
@@ -4500,6 +4534,7 @@ function prohibit_is_removable($roleid, context $context, $capability) {
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {context} ctx ON ctx.id = rc.contextid
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
|
||||
ORDER BY ctx.depth DESC";
|
||||
|
||||
@@ -4543,6 +4578,7 @@ function role_change_permission($roleid, $context, $capname, $permission) {
|
||||
$sql = "SELECT ctx.id, rc.permission, ctx.depth
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {context} ctx ON ctx.id = rc.contextid
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
|
||||
ORDER BY ctx.depth DESC";
|
||||
|
||||
@@ -7224,6 +7260,7 @@ function get_with_capability_join(context $context, $capability, $useridcolumn)
|
||||
$defs = array();
|
||||
$sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.path
|
||||
FROM {role_capabilities} rc
|
||||
JOIN {capabilities} cap ON rc.capability = cap.name
|
||||
JOIN {context} ctx on rc.contextid = ctx.id
|
||||
WHERE rc.contextid $incontexts AND rc.capability $incaps";
|
||||
$rcs = $DB->get_records_sql($sql, array_merge($cparams, $capsparams));
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
define(["core/config"],function(a){var b=function(b){this.storage=b,this.supported=this.detectSupport(),this.hashSource=a.wwwroot+"/"+a.jsrev,this.hash=this.hashString(this.hashSource),this.prefix=this.hash+"/",this.jsrevPrefix=this.hash+"/jsrev"};return b.prototype.detectSupport=function(){if(a.jsrev==-1)return!1;if("undefined"==typeof this.storage)return!1;var b="test";try{return null!==this.storage&&(this.storage.setItem(b,"1"),this.storage.removeItem(b),!0)}catch(c){return!1}},b.prototype.prefixKey=function(a){return this.prefix+a},b.prototype.validateCache=function(){var b=this.storage.getItem(this.jsrevPrefix);if(null===b)return void this.storage.setItem(this.jsrevPrefix,a.jsrev);var c=a.jsrev;c!=b&&(this.storage.clear(),this.storage.setItem(this.jsrevPrefix,a.jsrev))},b.prototype.hashString=function(a){var b,c,d,e=0;if(0===a.length)return e;for(b=0,d=a.length;b<d;b++)c=a.charCodeAt(b),e=(e<<5)-e+c,e|=0;return e},b.prototype.get=function(a){return!!this.supported&&(this.validateCache(),a=this.prefixKey(a),this.storage.getItem(a))},b.prototype.set=function(a,b){if(!this.supported)return!1;this.validateCache(),a=this.prefixKey(a);try{this.storage.setItem(a,b)}catch(c){return!1}return!0},b});
|
||||
define(["core/config"],function(a){var b=function(b){this.storage=b,this.supported=this.detectSupport(),this.hashSource=a.wwwroot+"/"+a.jsrev,this.hash=this.hashString(this.hashSource),this.prefix=this.hash+"/",this.jsrevPrefix=this.hashString(a.wwwroot)+"/jsrev",this.validateCache()};return b.prototype.detectSupport=function(){if(a.jsrev==-1)return!1;if("undefined"==typeof this.storage)return!1;var b="test";try{return null!==this.storage&&(this.storage.setItem(b,"1"),this.storage.removeItem(b),!0)}catch(c){return!1}},b.prototype.prefixKey=function(a){return this.prefix+a},b.prototype.validateCache=function(){var b=this.storage.getItem(this.jsrevPrefix);if(null===b)return void this.storage.setItem(this.jsrevPrefix,a.jsrev);var c=a.jsrev;c!=b&&(this.storage.clear(),this.storage.setItem(this.jsrevPrefix,a.jsrev))},b.prototype.hashString=function(a){var b,c,d,e=0;if(0===a.length)return e;for(b=0,d=a.length;b<d;b++)c=a.charCodeAt(b),e=(e<<5)-e+c,e|=0;return e},b.prototype.get=function(a){return!!this.supported&&(a=this.prefixKey(a),this.storage.getItem(a))},b.prototype.set=function(a,b){if(!this.supported)return!1;a=this.prefixKey(a);try{this.storage.setItem(a,b)}catch(c){return!1}return!0},b});
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -35,7 +35,8 @@ define(['core/config'], function(config) {
|
||||
this.hashSource = config.wwwroot + '/' + config.jsrev;
|
||||
this.hash = this.hashString(this.hashSource);
|
||||
this.prefix = this.hash + '/';
|
||||
this.jsrevPrefix = this.hash + '/jsrev';
|
||||
this.jsrevPrefix = this.hashString(config.wwwroot) + '/jsrev';
|
||||
this.validateCache();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -89,8 +90,8 @@ define(['core/config'], function(config) {
|
||||
this.storage.setItem(this.jsrevPrefix, config.jsrev);
|
||||
return;
|
||||
}
|
||||
var moodleVersion = config.jsrev;
|
||||
|
||||
var moodleVersion = config.jsrev;
|
||||
if (moodleVersion != cacheVersion) {
|
||||
this.storage.clear();
|
||||
this.storage.setItem(this.jsrevPrefix, config.jsrev);
|
||||
@@ -132,7 +133,6 @@ define(['core/config'], function(config) {
|
||||
if (!this.supported) {
|
||||
return false;
|
||||
}
|
||||
this.validateCache();
|
||||
key = this.prefixKey(key);
|
||||
|
||||
return this.storage.getItem(key);
|
||||
@@ -150,7 +150,6 @@ define(['core/config'], function(config) {
|
||||
if (!this.supported) {
|
||||
return false;
|
||||
}
|
||||
this.validateCache();
|
||||
key = this.prefixKey(key);
|
||||
// This can throw exceptions when the storage limit is reached.
|
||||
try {
|
||||
|
||||
@@ -656,9 +656,10 @@ define([
|
||||
* @param {string} templateName - should consist of the component and the name of the template like this:
|
||||
* core/menu (lib/templates/menu.mustache) or
|
||||
* tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
|
||||
* @param {Array} parentage - A list of requested partials in this render chain.
|
||||
* @return {Promise} JQuery promise object resolved when all partials are in the cache.
|
||||
*/
|
||||
Renderer.prototype.cachePartials = function(templateName) {
|
||||
Renderer.prototype.cachePartials = function(templateName, parentage) {
|
||||
var searchKey = this.currentThemeName + '/' + templateName;
|
||||
|
||||
if (searchKey in cachePartialPromises) {
|
||||
@@ -667,6 +668,9 @@ define([
|
||||
|
||||
// This promise will not be resolved until all child partials are also resolved and ready.
|
||||
// We create it here to allow us to check for recursive inclusion of templates.
|
||||
// Keep track of the requested partials in this chain.
|
||||
parentage = parentage || [searchKey];
|
||||
|
||||
cachePartialPromises[searchKey] = $.Deferred();
|
||||
|
||||
this.getTemplate(templateName)
|
||||
@@ -675,8 +679,8 @@ define([
|
||||
var uniquePartials = partials.filter(function(partialName) {
|
||||
// Check for recursion.
|
||||
|
||||
if (typeof cachePartialPromises[this.currentThemeName + '/' + partialName] !== 'undefined') {
|
||||
// Ignore templates which include their parent.
|
||||
if (parentage.indexOf(this.currentThemeName + '/' + partialName) >= 0) {
|
||||
// Ignore templates which include a parent template already requested in the current chain.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -686,7 +690,8 @@ define([
|
||||
|
||||
// Fetch any partial which has not already been fetched.
|
||||
var fetchThemAll = uniquePartials.map(function(partialName) {
|
||||
return this.cachePartials(partialName);
|
||||
parentage.push(this.currentThemeName + '/' + partialName);
|
||||
return this.cachePartials(partialName, parentage);
|
||||
}.bind(this));
|
||||
|
||||
// Resolve the templateName promise when all of the children are resolved.
|
||||
|
||||
@@ -54,9 +54,8 @@ class delete_unconfirmed_users_task extends scheduled_task {
|
||||
WHERE confirmed = 0 AND timecreated > 0
|
||||
AND timecreated < ? AND deleted = 0", array($cuttime));
|
||||
foreach ($rs as $user) {
|
||||
delete_user($user); // We MUST delete user properly first.
|
||||
$DB->delete_records('user', array('id' => $user->id)); // This is a bloody hack, but it might work.
|
||||
mtrace(" Deleted unconfirmed user for ".fullname($user, true)." ($user->id)");
|
||||
delete_user($user);
|
||||
mtrace(" Deleted unconfirmed user ".fullname($user, true)." ($user->id)");
|
||||
}
|
||||
$rs->close();
|
||||
}
|
||||
|
||||
@@ -1103,6 +1103,7 @@ $capabilities = array(
|
||||
),
|
||||
|
||||
'moodle/course:managegroups' => array(
|
||||
'riskbitmask' => RISK_XSS,
|
||||
|
||||
'captype' => 'write',
|
||||
'contextlevel' => CONTEXT_COURSE,
|
||||
|
||||
@@ -118,23 +118,30 @@ class provider implements
|
||||
|
||||
$user = $contextlist->get_user();
|
||||
|
||||
// Firstly export all autosave records from all contexts in the list owned by the given user.
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
$contextparams['userid'] = $user->id;
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM {editor_atto_autosave}
|
||||
WHERE userid = :userid AND contextid {$contextsql}";
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
$contextparams['userid'] = $contextlist->get_user()->id;
|
||||
$autosaves = $DB->get_recordset_sql($sql, $contextparams);
|
||||
self::export_autosaves($user, $autosaves);
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM {editor_atto_autosave}
|
||||
JOIN {context} c ON c.id = eas.contextid
|
||||
WHERE c.id {$contextsql} AND contextlevel = :contextuser AND c.instanceid = :userid";
|
||||
// Additionally export all eventual records in the given user's context regardless the actual owner.
|
||||
// We still consider them to be the user's personal data even when edited by someone else.
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
$contextparams['userid'] = $contextlist->get_user()->id;
|
||||
$contextparams['userid'] = $user->id;
|
||||
$contextparams['contextuser'] = CONTEXT_USER;
|
||||
|
||||
$sql = "SELECT eas.*
|
||||
FROM {editor_atto_autosave} eas
|
||||
JOIN {context} c ON c.id = eas.contextid
|
||||
WHERE c.id {$contextsql} AND c.contextlevel = :contextuser AND c.instanceid = :userid";
|
||||
|
||||
$autosaves = $DB->get_recordset_sql($sql, $contextparams);
|
||||
self::export_autosaves($user, $autosaves);
|
||||
}
|
||||
|
||||
+9
-2
@@ -1278,7 +1278,7 @@ function filter_phrases($text, $linkarray, $ignoretagsopen = null, $ignoretagscl
|
||||
// for things that have already been matched on this page.
|
||||
static $usedphrases = [];
|
||||
|
||||
$ignoretags = array(); // To store all the enclosig tags to be completely ignored.
|
||||
$ignoretags = array(); // To store all the enclosing tags to be completely ignored.
|
||||
$tags = array(); // To store all the simple tags to be ignored.
|
||||
|
||||
if (!$linkarrayalreadyprepared) {
|
||||
@@ -1418,8 +1418,15 @@ function filter_prepare_phrases_for_filtering(array $linkarray) {
|
||||
// Quote any regular expression characters and the delimiter in the work phrase to be searched.
|
||||
$linkobject->workregexp = preg_quote($linkobject->workregexp, '/');
|
||||
|
||||
// If we ony want to match entire words then add \b assertions. However, only
|
||||
// do this if the first or last thing in the phrase to match is a word character.
|
||||
if ($linkobject->fullmatch) {
|
||||
$linkobject->workregexp = '\b' . $linkobject->workregexp . '\b';
|
||||
if (preg_match('~^\w~', $linkobject->workregexp)) {
|
||||
$linkobject->workregexp = '\b' . $linkobject->workregexp;
|
||||
}
|
||||
if (preg_match('~\w$~', $linkobject->workregexp)) {
|
||||
$linkobject->workregexp = $linkobject->workregexp . '\b';
|
||||
}
|
||||
}
|
||||
|
||||
$linkobject->workregexp = '/(' . $linkobject->workregexp . ')/s';
|
||||
|
||||
@@ -6,10 +6,12 @@ Some changes from the upstream version have been made:
|
||||
* Added context type property for Context class
|
||||
* Set context type if 'context_type' parameter was submitted through POST
|
||||
* Do not require tool_consumer_instance_guid
|
||||
These changes can be reverted once the following pull requests/issues have been integrated upstream:
|
||||
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/10/commits/a9a1641f1a593eba4638133245c21d9ad47d8680
|
||||
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/11/commits/0bae60389bd020a02be5554516b86336e651e237
|
||||
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/issues/19
|
||||
* Prevent modification of the request to the provider
|
||||
These changes can be reverted once the following pull requests have been integrated upstream:
|
||||
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/10
|
||||
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/11
|
||||
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/47
|
||||
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/48
|
||||
|
||||
It is recommended by upstream to install depdencies via composer - but the composer installation is bundled
|
||||
with an autoloader so it's better to do it manually.
|
||||
|
||||
@@ -17,7 +17,6 @@ class OAuthRequest {
|
||||
// for debug purposes
|
||||
public $base_string;
|
||||
public static $version = '1.0';
|
||||
public static $POST_INPUT = 'php://input';
|
||||
|
||||
function __construct($http_method, $http_url, $parameters = null) {
|
||||
|
||||
@@ -60,22 +59,15 @@ class OAuthRequest {
|
||||
$parameters = array();
|
||||
}
|
||||
|
||||
// It's a POST request of the proper content-type, so parse POST
|
||||
// parameters and add those overriding any duplicates from GET
|
||||
if ($http_method == "POST"
|
||||
&& isset($request_headers['Content-Type'])
|
||||
&& strstr($request_headers['Content-Type'], 'application/x-www-form-urlencoded')) {
|
||||
$post_data = OAuthUtil::parse_parameters(file_get_contents(self::$POST_INPUT));
|
||||
$parameters = array_merge($parameters, $post_data);
|
||||
}
|
||||
|
||||
// We have a Authorization-header with OAuth data. Parse the header
|
||||
// and add those overriding any duplicates from GET or POST
|
||||
// We have a Authorization-header with OAuth data. Parse the header and add those.
|
||||
if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') {
|
||||
$header_parameters = OAuthUtil::split_header($request_headers['Authorization']);
|
||||
$parameters = array_merge($parameters, $header_parameters);
|
||||
}
|
||||
|
||||
// If there are parameters in $_POST, these are likely what will be used. Therefore, they should be considered
|
||||
// the final value in the case of any duplicates from sources parsed above.
|
||||
$parameters = array_merge($parameters, $_POST);
|
||||
}
|
||||
|
||||
return new OAuthRequest($http_method, $http_url, $parameters);
|
||||
|
||||
@@ -2485,7 +2485,7 @@ class core_renderer extends renderer_base {
|
||||
global $CFG, $DB;
|
||||
|
||||
$user = $userpicture->user;
|
||||
$canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
|
||||
$canviewfullnames = has_capability('moodle/site:viewfullnames', $this->page->context);
|
||||
|
||||
if ($userpicture->alttext) {
|
||||
if (!empty($user->imagealt)) {
|
||||
|
||||
@@ -48,8 +48,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
/**
|
||||
* The reCAPTCHA URL's
|
||||
*/
|
||||
define('RECAPTCHA_API_URL', 'https://www.google.com/recaptcha/api.js');
|
||||
define('RECAPTCHA_VERIFY_URL', 'https://www.google.com/recaptcha/api/siteverify');
|
||||
define('RECAPTCHA_API_URL', 'https://www.recaptcha.net/recaptcha/api.js');
|
||||
define('RECAPTCHA_VERIFY_URL', 'https://www.recaptcha.net/recaptcha/api/siteverify');
|
||||
|
||||
/**
|
||||
* Returns the language code the reCAPTCHA element should use.
|
||||
|
||||
+2
-2
@@ -1212,7 +1212,7 @@ class flexible_table {
|
||||
* This function is not part of the public api.
|
||||
*/
|
||||
function print_headers() {
|
||||
global $CFG, $OUTPUT;
|
||||
global $CFG, $OUTPUT, $PAGE;
|
||||
|
||||
echo html_writer::start_tag('thead');
|
||||
echo html_writer::start_tag('tr');
|
||||
@@ -1234,7 +1234,7 @@ class flexible_table {
|
||||
|
||||
case 'fullname':
|
||||
// Check the full name display for sortable fields.
|
||||
if (has_capability('moodle/site:viewfullnames', context_system::instance())) {
|
||||
if (has_capability('moodle/site:viewfullnames', $PAGE->context)) {
|
||||
$nameformat = $CFG->alternativefullnameformat;
|
||||
} else {
|
||||
$nameformat = $CFG->fullnamedisplay;
|
||||
|
||||
@@ -1696,6 +1696,101 @@ class core_accesslib_testcase extends advanced_testcase {
|
||||
$this->assertFalse(has_all_capabilities($sca, $coursecontext, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that assigning a fake cap does not return.
|
||||
*/
|
||||
public function test_fake_capability() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST);
|
||||
$teacher = $this->getDataGenerator()->create_user();
|
||||
|
||||
$fakecapname = 'moodle/fake:capability';
|
||||
|
||||
role_assign($teacherrole->id, $teacher->id, $coursecontext);
|
||||
$admin = $DB->get_record('user', array('username' => 'admin'));
|
||||
|
||||
// Test a capability which does not exist.
|
||||
// Note: Do not use assign_capability because it will not allow fake caps.
|
||||
$DB->insert_record('role_capabilities', (object) [
|
||||
'contextid' => $coursecontext->id,
|
||||
'roleid' => $teacherrole->id,
|
||||
'capability' => $fakecapname,
|
||||
'permission' => CAP_ALLOW,
|
||||
'timemodified' => time(),
|
||||
'modifierid' => 0,
|
||||
]);
|
||||
|
||||
// Check `has_capability`.
|
||||
$this->assertFalse(has_capability($fakecapname, $coursecontext, $teacher));
|
||||
$this->assertDebuggingCalled("Capability \"{$fakecapname}\" was not found! This has to be fixed in code.");
|
||||
$this->assertFalse(has_capability($fakecapname, $coursecontext, $admin));
|
||||
$this->assertDebuggingCalled("Capability \"{$fakecapname}\" was not found! This has to be fixed in code.");
|
||||
|
||||
// Check `get_with_capability_sql` (with uses `get_with_capability_join`).
|
||||
list($sql, $params) = get_with_capability_sql($coursecontext, $fakecapname);
|
||||
$users = $DB->get_records_sql($sql, $params);
|
||||
|
||||
$this->assertFalse(array_key_exists($teacher->id, $users));
|
||||
$this->assertFalse(array_key_exists($admin->id, $users));
|
||||
|
||||
// Check `get_users_by_capability`.
|
||||
$users = get_users_by_capability($coursecontext, $fakecapname);
|
||||
|
||||
$this->assertFalse(array_key_exists($teacher->id, $users));
|
||||
$this->assertFalse(array_key_exists($admin->id, $users));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that assigning a fake cap does not return.
|
||||
*/
|
||||
public function test_fake_capability_assign() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST);
|
||||
$teacher = $this->getDataGenerator()->create_user();
|
||||
|
||||
$capability = 'moodle/fake:capability';
|
||||
|
||||
role_assign($teacherrole->id, $teacher->id, $coursecontext);
|
||||
$admin = $DB->get_record('user', array('username' => 'admin'));
|
||||
|
||||
$this->expectException('coding_exception');
|
||||
$this->expectExceptionMessage("Capability '{$capability}' was not found! This has to be fixed in code.");
|
||||
assign_capability($capability, CAP_ALLOW, $teacherrole->id, $coursecontext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that assigning a fake cap does not return.
|
||||
*/
|
||||
public function test_fake_capability_unassign() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST);
|
||||
$teacher = $this->getDataGenerator()->create_user();
|
||||
|
||||
$capability = 'moodle/fake:capability';
|
||||
|
||||
role_assign($teacherrole->id, $teacher->id, $coursecontext);
|
||||
$admin = $DB->get_record('user', array('username' => 'admin'));
|
||||
|
||||
$this->expectException('coding_exception');
|
||||
$this->expectExceptionMessage("Capability '{$capability}' was not found! This has to be fixed in code.");
|
||||
unassign_capability($capability, CAP_ALLOW, $teacherrole->id, $coursecontext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the caching in get_role_definitions() and get_role_definitions_uncached()
|
||||
* works as intended.
|
||||
@@ -2815,7 +2910,7 @@ class core_accesslib_testcase extends advanced_testcase {
|
||||
assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultfrontpageroleid, $frontpagepagecontext, true);
|
||||
assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $frontpagepagecontext, true);
|
||||
assign_capability('mod/page:view', CAP_ALLOW, $allroles['user'], $frontpagepagecontext, true);
|
||||
assign_capability('moodle/page:view', CAP_ALLOW, $allroles['student'], $frontpagepagecontext, true);
|
||||
assign_capability('mod/page:view', CAP_ALLOW, $allroles['student'], $frontpagepagecontext, true);
|
||||
|
||||
assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultuserroleid, $frontpagecontext, true);
|
||||
assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultfrontpageroleid, $frontpagecontext, true);
|
||||
@@ -3443,10 +3538,10 @@ class core_accesslib_testcase extends advanced_testcase {
|
||||
$this->assertFalse(array_key_exists($guest->id, $users));
|
||||
|
||||
// Test role override.
|
||||
assign_capability('moodle/site:backupcourse', CAP_PROHIBIT, $teacherrole->id, $coursecontext, true);
|
||||
assign_capability('moodle/site:backupcourse', CAP_ALLOW, $studentrole->id, $coursecontext, true);
|
||||
assign_capability('moodle/backup:backupcourse', CAP_PROHIBIT, $teacherrole->id, $coursecontext, true);
|
||||
assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $coursecontext, true);
|
||||
|
||||
list($sql, $params) = get_with_capability_sql($coursecontext, 'moodle/site:backupcourse');
|
||||
list($sql, $params) = get_with_capability_sql($coursecontext, 'moodle/backup:backupcourse');
|
||||
$users = $DB->get_records_sql($sql, $params);
|
||||
|
||||
$this->assertFalse(array_key_exists($teacher->id, $users));
|
||||
|
||||
@@ -32,36 +32,61 @@ defined('MOODLE_INTERNAL') || die();
|
||||
class core_environment_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test the environment.
|
||||
* Test the environment check status.
|
||||
*/
|
||||
public function test_environment() {
|
||||
public function test_environment_check_status() {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->libdir.'/environmentlib.php');
|
||||
list($envstatus, $environment_results) = check_moodle_environment(normalize_version($CFG->release), ENV_SELECT_RELEASE);
|
||||
|
||||
$results = check_moodle_environment(normalize_version($CFG->release), ENV_SELECT_RELEASE);
|
||||
|
||||
// The first element of the results array contains the environment check status.
|
||||
$status = reset($results);
|
||||
$this->assertTrue($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for Moodle environment check tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function environment_provider() {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir.'/environmentlib.php');
|
||||
|
||||
$results = check_moodle_environment(normalize_version($CFG->release), ENV_SELECT_RELEASE);
|
||||
// The second element of the results array contains the list of environment results.
|
||||
$environmentresults = end($results);
|
||||
return array_map(function($result) {
|
||||
return [$result];
|
||||
}, $environmentresults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the environment.
|
||||
*
|
||||
* @dataProvider environment_provider
|
||||
* @param environment_results $result
|
||||
*/
|
||||
public function test_environment($result) {
|
||||
$sslmessages = ['ssl/tls configuration not supported', 'invalid ssl/tls configuration'];
|
||||
|
||||
$this->assertNotEmpty($envstatus);
|
||||
foreach ($environment_results as $environment_result) {
|
||||
if ($environment_result->part === 'php_setting'
|
||||
and $environment_result->info === 'opcache.enable'
|
||||
and $environment_result->getLevel() === 'optional'
|
||||
and $environment_result->getStatus() === false
|
||||
) {
|
||||
$this->markTestSkipped('OPCache extension is not necessary for unit testing.');
|
||||
continue;
|
||||
}
|
||||
if ($environment_result->part === 'custom_check'
|
||||
and in_array($environment_result->info, $sslmessages)
|
||||
and $environment_result->getLevel() === 'optional'
|
||||
and $environment_result->getStatus() === false
|
||||
) {
|
||||
$this->markTestSkipped('Up-to-date TLS libraries are not necessary for unit testing.');
|
||||
continue;
|
||||
}
|
||||
$this->assertTrue($environment_result->getStatus(), "Problem detected in environment ($environment_result->part:$environment_result->info), fix all warnings and errors!");
|
||||
if ($result->part === 'php_setting'
|
||||
&& $result->info === 'opcache.enable'
|
||||
&& $result->getLevel() === 'optional'
|
||||
&& $result->getStatus() === false) {
|
||||
$this->markTestSkipped('OPCache extension is not necessary for unit testing.');
|
||||
}
|
||||
|
||||
if ($result->part === 'custom_check'
|
||||
&& $result->getLevel() === 'optional'
|
||||
&& $result->getStatus() === false) {
|
||||
if (in_array($result->info, $sslmessages)) {
|
||||
$this->markTestSkipped('Up-to-date TLS libraries are not necessary for unit testing.');
|
||||
}
|
||||
}
|
||||
$info = "{$result->part}:{$result->info}";
|
||||
$this->assertTrue($result->getStatus(), "Problem detected in environment ($info), fix all warnings and errors!");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
This files describes API changes in core libraries and APIs,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.4.6 ===
|
||||
|
||||
* The \core_rating provider's get_sql_join function now accepts an optional $innerjoin parameter.
|
||||
It is recommended that privacy providers using this function call rewrite any long query into a number of separate
|
||||
calls to add_from_sql for improved performance, and that the new argument is used.
|
||||
This will allow queries to remain backwards-compatible with older versions of Moodle but will have significantly better performance in version supporting the innerjoin parameter.
|
||||
|
||||
=== 3.4.5 ===
|
||||
|
||||
* Custom AJAX handlers for the form autocomplete fields can now optionally return string in their processResults()
|
||||
|
||||
+3
-3
@@ -31,7 +31,6 @@ redirect_if_major_upgrade_required();
|
||||
|
||||
$testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
|
||||
$anchor = optional_param('anchor', '', PARAM_RAW); // Used to restore hash anchor to wantsurl.
|
||||
$logintoken = optional_param('logintoken', '', PARAM_RAW); // Used to validate the request.
|
||||
|
||||
$context = context_system::instance();
|
||||
$PAGE->set_url("$CFG->wwwroot/login/index.php");
|
||||
@@ -67,13 +66,13 @@ if (!empty($SESSION->has_timed_out)) {
|
||||
$session_has_timed_out = false;
|
||||
}
|
||||
|
||||
/// auth plugins may override these - SSO anyone?
|
||||
$frm = false;
|
||||
$user = false;
|
||||
|
||||
$authsequence = get_enabled_auth_plugins(true); // auths, in sequence
|
||||
foreach($authsequence as $authname) {
|
||||
$authplugin = get_auth_plugin($authname);
|
||||
// The auth plugin's loginpage_hook() can eventually set $frm and/or $user.
|
||||
$authplugin->loginpage_hook();
|
||||
}
|
||||
|
||||
@@ -132,12 +131,13 @@ if ($frm and isset($frm->username)) { // Login WITH
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
//user already supplied by aut plugin prelogin hook
|
||||
// The auth plugin has already provided the user via the loginpage_hook() called above.
|
||||
} else if (($frm->username == 'guest') and empty($CFG->guestloginbutton)) {
|
||||
$user = false; /// Can't log in as guest if guest button is disabled
|
||||
$frm = false;
|
||||
} else {
|
||||
if (empty($errormsg)) {
|
||||
$logintoken = isset($frm->logintoken) ? $frm->logintoken : '';
|
||||
$user = authenticate_user_login($frm->username, $frm->password, false, $errorcode, $logintoken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,8 @@ class helper {
|
||||
}
|
||||
}
|
||||
$data->isonline = null;
|
||||
if (self::show_online_status($userfields)) {
|
||||
$user = \core_user::get_user($data->userid);
|
||||
if (self::show_online_status($user)) {
|
||||
$data->isonline = self::is_online($userfields->lastaccess);
|
||||
}
|
||||
$data->isblocked = isset($contact->blocked) ? (bool) $contact->blocked : false;
|
||||
|
||||
@@ -103,29 +103,57 @@ class provider implements
|
||||
* @return contextlist the list of contexts containing user info for the user.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
// Fetch all data records.
|
||||
$contextlist = new contextlist();
|
||||
|
||||
// Fetch all data records that the user rote.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
INNER JOIN {data} d ON d.id = cm.instance
|
||||
INNER JOIN {data_records} dr ON dr.dataid = d.id
|
||||
LEFT JOIN {comments} com ON com.commentarea=:commentarea and com.itemid = dr.id AND com.userid = :userid1
|
||||
LEFT JOIN {rating} r ON r.contextid = c.id AND r.itemid = dr.id AND r.component = :moddata
|
||||
AND r.ratingarea = :ratingarea AND r.userid = :userid2
|
||||
WHERE dr.userid = :userid OR com.id IS NOT NULL OR r.id IS NOT NULL";
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {data} d ON d.id = cm.instance
|
||||
JOIN {data_records} dr ON dr.dataid = d.id
|
||||
WHERE dr.userid = :userid";
|
||||
|
||||
$params = [
|
||||
'modname' => 'data',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'data',
|
||||
'userid' => $userid,
|
||||
'userid1' => $userid,
|
||||
'userid2' => $userid,
|
||||
'commentarea' => 'database_entry',
|
||||
'moddata' => 'mod_data',
|
||||
'ratingarea' => 'entry',
|
||||
];
|
||||
$contextlist = new contextlist();
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Fetch contexts where the user commented.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {data} d ON d.id = cm.instance
|
||||
JOIN {data_records} dr ON dr.dataid = d.id
|
||||
JOIN {comments} com ON com.commentarea = :commentarea and com.itemid = dr.id
|
||||
WHERE com.userid = :userid";
|
||||
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'data',
|
||||
'commentarea' => 'database_entry',
|
||||
'userid' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Fetch all data records.
|
||||
$ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_data', 'entry', 'dr.id', $userid, true);
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {data} d ON d.id = cm.instance
|
||||
JOIN {data_records} dr ON dr.dataid = d.id
|
||||
{$ratingquery->join}
|
||||
WHERE {$ratingquery->userwhere}";
|
||||
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'data',
|
||||
] + $ratingquery->params;
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
return $contextlist;
|
||||
|
||||
@@ -274,7 +274,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
|
||||
public function test_view_database_no_capabilities() {
|
||||
// Test user with no capabilities.
|
||||
// We need a explicit prohibit since this capability is allowed for students by default.
|
||||
assign_capability('mod/data:viewpage', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
|
||||
assign_capability('mod/data:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
|
||||
accesslib_clear_all_caches_for_unit_testing();
|
||||
|
||||
$this->expectException('moodle_exception');
|
||||
|
||||
@@ -152,46 +152,105 @@ class provider implements
|
||||
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
|
||||
$ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
|
||||
// Fetch all forum discussions, and forum posts.
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
|
||||
$params = [
|
||||
'modname' => 'forum',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'userid' => $userid,
|
||||
];
|
||||
|
||||
// Discussion creators.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
LEFT JOIN {forum_discussions} d ON d.forum = f.id
|
||||
LEFT JOIN {forum_posts} p ON p.discussion = d.id
|
||||
LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
|
||||
LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
|
||||
LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
|
||||
LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id AND hasread.userid = :hasreaduserid
|
||||
LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id AND dsub.userid = :dsubuserid
|
||||
{$ratingsql->join}
|
||||
WHERE (
|
||||
p.userid = :postuserid OR
|
||||
d.userid = :discussionuserid OR
|
||||
dig.id IS NOT NULL OR
|
||||
sub.id IS NOT NULL OR
|
||||
pref.id IS NOT NULL OR
|
||||
hasread.id IS NOT NULL OR
|
||||
dsub.id IS NOT NULL OR
|
||||
{$ratingsql->userwhere}
|
||||
)
|
||||
JOIN {forum_discussions} d ON d.forum = f.id
|
||||
WHERE d.userid = :userid
|
||||
";
|
||||
$params = [
|
||||
'modname' => 'forum',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'postuserid' => $userid,
|
||||
'discussionuserid' => $userid,
|
||||
'digestuserid' => $userid,
|
||||
'subuserid' => $userid,
|
||||
'prefuserid' => $userid,
|
||||
'hasreaduserid' => $userid,
|
||||
'dsubuserid' => $userid,
|
||||
];
|
||||
$params += $ratingsql->params;
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
// Post authors.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_discussions} d ON d.forum = f.id
|
||||
JOIN {forum_posts} p ON p.discussion = d.id
|
||||
WHERE p.userid = :userid
|
||||
";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Forum digest records.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_digests} dig ON dig.forum = f.id
|
||||
WHERE dig.userid = :userid
|
||||
";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Forum subscriptions.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_subscriptions} sub ON sub.forum = f.id
|
||||
WHERE sub.userid = :userid
|
||||
";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Discussion subscriptions.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id
|
||||
WHERE dsub.userid = :userid
|
||||
";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Discussion tracking preferences.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_track_prefs} pref ON pref.forumid = f.id
|
||||
WHERE pref.userid = :userid
|
||||
";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Discussion read records.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_read} hasread ON hasread.forumid = f.id
|
||||
WHERE hasread.userid = :userid
|
||||
";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Rating authors.
|
||||
$ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid, true);
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_discussions} d ON d.forum = f.id
|
||||
JOIN {forum_posts} p ON p.discussion = d.id
|
||||
{$ratingsql->join}
|
||||
WHERE {$ratingsql->userwhere}
|
||||
";
|
||||
$params += $ratingsql->params;
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
return $contextlist;
|
||||
@@ -365,30 +424,68 @@ class provider implements
|
||||
$userid = $user->id;
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
$params = $contextparams;
|
||||
|
||||
// Digested forums.
|
||||
$sql = "SELECT
|
||||
c.id AS contextid,
|
||||
dig.maildigest AS maildigest
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_digests} dig ON dig.forum = f.id
|
||||
WHERE (
|
||||
dig.userid = :userid AND
|
||||
c.id {$contextsql}
|
||||
)
|
||||
";
|
||||
$params['userid'] = $userid;
|
||||
$digests = $DB->get_records_sql_menu($sql, $params);
|
||||
|
||||
// Forum subscriptions.
|
||||
$sql = "SELECT
|
||||
c.id AS contextid,
|
||||
sub.userid AS subscribed
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_subscriptions} sub ON sub.forum = f.id
|
||||
WHERE (
|
||||
sub.userid = :userid AND
|
||||
c.id {$contextsql}
|
||||
)
|
||||
";
|
||||
$params['userid'] = $userid;
|
||||
$subscriptions = $DB->get_records_sql_menu($sql, $params);
|
||||
|
||||
// Tracked forums.
|
||||
$sql = "SELECT
|
||||
c.id AS contextid,
|
||||
f.*,
|
||||
cm.id AS cmid,
|
||||
dig.maildigest,
|
||||
sub.userid AS subscribed,
|
||||
pref.userid AS tracked
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
|
||||
LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
|
||||
LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
|
||||
JOIN {forum_track_prefs} pref ON pref.forumid = f.id
|
||||
WHERE (
|
||||
pref.userid = :userid AND
|
||||
c.id {$contextsql}
|
||||
)
|
||||
";
|
||||
$params['userid'] = $userid;
|
||||
$tracked = $DB->get_records_sql_menu($sql, $params);
|
||||
|
||||
$sql = "SELECT
|
||||
c.id AS contextid,
|
||||
f.*,
|
||||
cm.id AS cmid
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
WHERE (
|
||||
c.id {$contextsql}
|
||||
)
|
||||
";
|
||||
|
||||
$params = [
|
||||
'digestuserid' => $userid,
|
||||
'subuserid' => $userid,
|
||||
'prefuserid' => $userid,
|
||||
];
|
||||
$params += $contextparams;
|
||||
|
||||
// Keep a mapping of forumid to contextid.
|
||||
@@ -407,9 +504,15 @@ class provider implements
|
||||
request_helper::export_context_files($context, $user);
|
||||
|
||||
// Store relevant metadata about this forum instance.
|
||||
static::export_digest_data($userid, $forum);
|
||||
static::export_subscription_data($userid, $forum);
|
||||
static::export_tracking_data($userid, $forum);
|
||||
if (isset($digests[$forum->contextid])) {
|
||||
static::export_digest_data($userid, $forum, $digests[$forum->contextid]);
|
||||
}
|
||||
if (isset($subscriptions[$forum->contextid])) {
|
||||
static::export_subscription_data($userid, $forum, $subscriptions[$forum->contextid]);
|
||||
}
|
||||
if (isset($tracked[$forum->contextid])) {
|
||||
static::export_tracking_data($userid, $forum, $tracked[$forum->contextid]);
|
||||
}
|
||||
}
|
||||
$forums->close();
|
||||
|
||||
@@ -704,16 +807,17 @@ class provider implements
|
||||
*
|
||||
* @param int $userid The userid of the user whose data is to be exported.
|
||||
* @param \stdClass $forum The forum whose data is being exported.
|
||||
* @param int $maildigest The mail digest setting for this forum.
|
||||
* @return bool Whether any data was stored.
|
||||
*/
|
||||
protected static function export_digest_data(int $userid, \stdClass $forum) {
|
||||
if (null !== $forum->maildigest) {
|
||||
protected static function export_digest_data(int $userid, \stdClass $forum, int $maildigest) {
|
||||
if (null !== $maildigest) {
|
||||
// The user has a specific maildigest preference for this forum.
|
||||
$a = (object) [
|
||||
'forum' => format_string($forum->name, true),
|
||||
];
|
||||
|
||||
switch ($forum->maildigest) {
|
||||
switch ($maildigest) {
|
||||
case 0:
|
||||
$a->type = get_string('emaildigestoffshort', 'mod_forum');
|
||||
break;
|
||||
@@ -726,7 +830,7 @@ class provider implements
|
||||
}
|
||||
|
||||
writer::with_context(\context_module::instance($forum->cmid))
|
||||
->export_metadata([], 'digestpreference', $forum->maildigest,
|
||||
->export_metadata([], 'digestpreference', $maildigest,
|
||||
get_string('privacy:digesttypepreference', 'mod_forum', $a));
|
||||
|
||||
return true;
|
||||
@@ -740,10 +844,11 @@ class provider implements
|
||||
*
|
||||
* @param int $userid The userid of the user whose data is to be exported.
|
||||
* @param \stdClass $forum The forum whose data is being exported.
|
||||
* @param int $subscribed if the user is subscribed
|
||||
* @return bool Whether any data was stored.
|
||||
*/
|
||||
protected static function export_subscription_data(int $userid, \stdClass $forum) {
|
||||
if (null !== $forum->subscribed) {
|
||||
protected static function export_subscription_data(int $userid, \stdClass $forum, int $subscribed) {
|
||||
if (null !== $subscribed) {
|
||||
// The user is subscribed to this forum.
|
||||
writer::with_context(\context_module::instance($forum->cmid))
|
||||
->export_metadata([], 'subscriptionpreference', 1, get_string('privacy:subscribedtoforum', 'mod_forum'));
|
||||
@@ -798,10 +903,11 @@ class provider implements
|
||||
*
|
||||
* @param int $userid The userid of the user whose data is to be exported.
|
||||
* @param \stdClass $forum The forum whose data is being exported.
|
||||
* @param int $tracke if the user is subscribed
|
||||
* @return bool Whether any data was stored.
|
||||
*/
|
||||
protected static function export_tracking_data(int $userid, \stdClass $forum) {
|
||||
if (null !== $forum->tracked) {
|
||||
protected static function export_tracking_data(int $userid, \stdClass $forum, int $tracked) {
|
||||
if (null !== $tracked) {
|
||||
// The user has a main preference to track all forums, but has opted out of this one.
|
||||
writer::with_context(\context_module::instance($forum->cmid))
|
||||
->export_metadata([], 'trackreadpreference', 0, get_string('privacy:readtrackingdisabled', 'mod_forum'));
|
||||
|
||||
+8
-5
@@ -658,6 +658,12 @@ function forum_cron() {
|
||||
}
|
||||
}
|
||||
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userto->id)) {
|
||||
// The course is hidden and the user does not have access to it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't send email if the forum is Q&A and the user has not posted.
|
||||
// Initial topics are still mailed.
|
||||
if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
|
||||
@@ -3919,7 +3925,7 @@ function forum_print_discussion_header(&$post, $forum, $group = -1, $datestring
|
||||
}
|
||||
|
||||
echo '<td class="lastpost">';
|
||||
$usedate = (empty($post->created)) ? $post->timemodified : $post->created;
|
||||
$usedate = (empty($post->timemodified)) ? $post->created : $post->timemodified;
|
||||
$parenturl = '';
|
||||
$usermodified = new stdClass();
|
||||
$usermodified->id = $post->usermodified;
|
||||
@@ -4552,10 +4558,6 @@ function forum_update_post($newpost, $mform, $unused = null) {
|
||||
}
|
||||
$post->modified = time();
|
||||
|
||||
// Last post modified tracking.
|
||||
$discussion->timemodified = $post->modified;
|
||||
$discussion->usermodified = $post->userid;
|
||||
|
||||
if (!$post->parent) { // Post is a discussion starter - update discussion title and times too
|
||||
$discussion->name = $post->subject;
|
||||
$discussion->timestart = $post->timestart;
|
||||
@@ -4568,6 +4570,7 @@ function forum_update_post($newpost, $mform, $unused = null) {
|
||||
$post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id,
|
||||
mod_forum_post_form::editor_options($context, $post->id), $post->message);
|
||||
$DB->update_record('forum_posts', $post);
|
||||
// Note: Discussion modified time/user are intentionally not updated, to enable them to track the latest new post.
|
||||
$DB->update_record('forum_discussions', $discussion);
|
||||
|
||||
forum_add_attachment($post, $forum, $cm, $mform);
|
||||
|
||||
+1
-1
@@ -574,7 +574,7 @@ file_prepare_draft_area($draftitemid, $modcontext->id, 'mod_forum', 'attachment'
|
||||
|
||||
if ($USER->id != $post->userid) { // Not the original author, so add a message to the end
|
||||
$data = new stdClass();
|
||||
$data->date = userdate($post->modified);
|
||||
$data->date = userdate($post->created);
|
||||
if ($post->messageformat == FORMAT_HTML) {
|
||||
$data->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$USER->id.'&course='.$post->course.'">'.
|
||||
fullname($USER).'</a>';
|
||||
|
||||
@@ -71,14 +71,14 @@ Feature: New discussions and discussions with recently added replies are display
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Course general forum"
|
||||
#
|
||||
# Make sure the order of the forum posts is as expected (most recently participated first).
|
||||
# Make sure the order of the forum posts is as expected, with most recent new participation first (ie excluding edits).
|
||||
#
|
||||
Then I should see "Forum post 3" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=3]" "xpath_element"
|
||||
And I should see "Edited forum post 2" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=2]" "xpath_element"
|
||||
And I should see "Forum post 1" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=1]" "xpath_element"
|
||||
Then I should see "Forum post 1" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=1]" "xpath_element"
|
||||
And I should see "Forum post 3" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=2]" "xpath_element"
|
||||
And I should see "Edited forum post 2" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=3]" "xpath_element"
|
||||
#
|
||||
# Make sure the next/prev navigation uses the same order of the posts.
|
||||
#
|
||||
And I follow "Edited forum post 2"
|
||||
And I follow "Forum post 3"
|
||||
And "//a[@aria-label='Next discussion: Forum post 1']" "xpath_element" should exist
|
||||
And "//a[@aria-label='Previous discussion: Forum post 3']" "xpath_element" should exist
|
||||
And "//a[@aria-label='Previous discussion: Edited forum post 2']" "xpath_element" should exist
|
||||
|
||||
@@ -3298,6 +3298,17 @@ class mod_forum_lib_testcase extends advanced_testcase {
|
||||
// On this freshly created discussion, the teacher is the author of the last post.
|
||||
$this->assertEquals($teacher->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
|
||||
|
||||
// Fetch modified timestamp of the discussion.
|
||||
$discussionmodified = $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]);
|
||||
$pasttime = $discussionmodified - 3600;
|
||||
|
||||
// Adjust the discussion modified timestamp back an hour, so it's in the past.
|
||||
$adjustment = (object)[
|
||||
'id' => $discussion->id,
|
||||
'timemodified' => $pasttime,
|
||||
];
|
||||
$DB->update_record('forum_discussions', $adjustment);
|
||||
|
||||
// Let the student reply to the teacher's post.
|
||||
$reply = $generator->create_post((object)[
|
||||
'course' => $course->id,
|
||||
@@ -3310,6 +3321,30 @@ class mod_forum_lib_testcase extends advanced_testcase {
|
||||
// The student should now be the last post's author.
|
||||
$this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
|
||||
|
||||
// Fetch modified timestamp of the discussion and student's post.
|
||||
$discussionmodified = $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]);
|
||||
$postmodified = $DB->get_field('forum_posts', 'modified', ['id' => $reply->id]);
|
||||
|
||||
// Discussion modified time should be updated to be equal to the newly created post's time.
|
||||
$this->assertEquals($discussionmodified, $postmodified);
|
||||
|
||||
// Adjust the discussion and post timestamps, so they are in the past.
|
||||
$adjustment = (object)[
|
||||
'id' => $discussion->id,
|
||||
'timemodified' => $pasttime,
|
||||
];
|
||||
$DB->update_record('forum_discussions', $adjustment);
|
||||
|
||||
$adjustment = (object)[
|
||||
'id' => $reply->id,
|
||||
'modified' => $pasttime,
|
||||
];
|
||||
$DB->update_record('forum_posts', $adjustment);
|
||||
|
||||
// The discussion and student's post time should now be an hour in the past.
|
||||
$this->assertEquals($pasttime, $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]));
|
||||
$this->assertEquals($pasttime, $DB->get_field('forum_posts', 'modified', ['id' => $reply->id]));
|
||||
|
||||
// Let the teacher edit the student's reply.
|
||||
$this->setUser($teacher->id);
|
||||
$newpost = (object)[
|
||||
@@ -3319,8 +3354,14 @@ class mod_forum_lib_testcase extends advanced_testcase {
|
||||
];
|
||||
forum_update_post($newpost, null);
|
||||
|
||||
// The student should be still the last post's author.
|
||||
// The student should still be the last post's author.
|
||||
$this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
|
||||
|
||||
// The discussion modified time should not have changed.
|
||||
$this->assertEquals($pasttime, $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]));
|
||||
|
||||
// The post time should be updated.
|
||||
$this->assertGreaterThan($pasttime, $DB->get_field('forum_posts', 'modified', ['id' => $reply->id]));
|
||||
}
|
||||
|
||||
public function test_forum_core_calendar_provide_event_action() {
|
||||
|
||||
@@ -80,26 +80,55 @@ class provider implements
|
||||
* @return contextlist the list of contexts containing user info for the user.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
$ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_glossary', 'entry', 'ge.id', $userid);
|
||||
$contextlist = new contextlist();
|
||||
|
||||
// Glossary entries.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
INNER JOIN {glossary} g ON g.id = cm.instance
|
||||
INNER JOIN {glossary_entries} ge ON ge.glossaryid = g.id
|
||||
LEFT JOIN {comments} com ON com.commentarea =:commentarea AND com.itemid = ge.id
|
||||
{$ratingquery->join}
|
||||
WHERE ge.userid = :glossaryentryuserid OR com.userid = :commentuserid OR {$ratingquery->userwhere}";
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {glossary} g ON g.id = cm.instance
|
||||
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
|
||||
WHERE ge.userid = :glossaryentryuserid";
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'glossary',
|
||||
'commentarea' => 'glossary_entry',
|
||||
'glossaryentryuserid' => $userid,
|
||||
'commentuserid' => $userid,
|
||||
] + $ratingquery->params;
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$contextlist = new contextlist();
|
||||
// Where the user has rated something.
|
||||
$ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_glossary', 'entry', 'ge.id', $userid, true);
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {glossary} g ON g.id = cm.instance
|
||||
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
|
||||
{$ratingquery->join}
|
||||
WHERE {$ratingquery->userwhere}";
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'glossary',
|
||||
] + $ratingquery->params;
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Comments.
|
||||
$sql = "SELECT c.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {glossary} g ON g.id = cm.instance
|
||||
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
|
||||
JOIN {comments} com ON com.commentarea =:commentarea AND com.itemid = ge.id
|
||||
WHERE com.userid = :commentuserid";
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'glossary',
|
||||
'commentarea' => 'glossary_entry',
|
||||
'commentuserid' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
return $contextlist;
|
||||
|
||||
@@ -45,6 +45,13 @@ $reset = optional_param('reset', null, PARAM_BOOL);
|
||||
|
||||
require_login();
|
||||
|
||||
if (\core\session\manager::is_loggedinas()) {
|
||||
// Disable access to the user's dashboard for "logged in as" sessions
|
||||
// to mitigate risks associated with loading other users' JavaScript.
|
||||
// See MDL-63786 for more information.
|
||||
redirect(new moodle_url('/', ['redirect' => 0]), get_string('unabletoaccess', 'core_my'));
|
||||
}
|
||||
|
||||
$hassiteconfig = has_capability('moodle/site:config', context_system::instance());
|
||||
if ($hassiteconfig && moodle_needs_upgrading()) {
|
||||
redirect(new moodle_url('/admin/index.php'));
|
||||
|
||||
@@ -453,7 +453,7 @@ class moodle_content_writer implements content_writer {
|
||||
|
||||
$this->write_data($newshortpath, $variablecontent);
|
||||
} else {
|
||||
$treekey[$shortpath] = 'No var';
|
||||
$treekey[clean_param($shortpath, PARAM_PATH)] = 'No var';
|
||||
}
|
||||
}
|
||||
return [$tree, $treekey, $allfiles];
|
||||
@@ -488,11 +488,11 @@ class moodle_content_writer implements content_writer {
|
||||
$url = clean_param($url, PARAM_PATH);
|
||||
$treeleaf->name = $file;
|
||||
$treeleaf->itemtype = 'item';
|
||||
$gokey = $url . DIRECTORY_SEPARATOR . $file;
|
||||
$gokey = clean_param($url . '/' . $file, PARAM_PATH);
|
||||
if (isset($treekey[$gokey]) && $treekey[$gokey] !== 'No var') {
|
||||
$treeleaf->datavar = $treekey[$gokey];
|
||||
} else {
|
||||
$treeleaf->url = new \moodle_url($url . DIRECTORY_SEPARATOR . $file);
|
||||
$treeleaf->url = new \moodle_url($url . '/' . $file);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -635,16 +635,9 @@ class moodle_content_writer implements content_writer {
|
||||
$this->copy_data($moodleimgpath, ['pix', 'moodlelogo.svg']);
|
||||
|
||||
// Additional required css.
|
||||
// Determine what direction to show the data export page according to the user preference.
|
||||
$rtl = right_to_left();
|
||||
if (!$rtl) {
|
||||
$bootstrapdestination = 'bootstrap.min.css';
|
||||
$this->write_url_content('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css',
|
||||
$bootstrapdestination);
|
||||
} else {
|
||||
$rtldestination = 'rtlbootstrap.min.css';
|
||||
$this->write_url_content('https://cdn.rtlcss.com/bootstrap/v4.0.0/css/bootstrap.min.css', $rtldestination);
|
||||
}
|
||||
$csspath = ['theme', 'boost', 'style', 'moodle.css'];
|
||||
$destination = ['moodle.css'];
|
||||
$this->copy_data($csspath, $destination);
|
||||
|
||||
$csspath = ['privacy', 'export_files', 'general.css'];
|
||||
$destination = ['general.css'];
|
||||
@@ -666,6 +659,7 @@ class moodle_content_writer implements content_writer {
|
||||
$siteurl = $CFG->wwwroot;
|
||||
|
||||
// Create custom index.html file.
|
||||
$rtl = right_to_left();
|
||||
$htmlpage = new \core_privacy\output\exported_html_page($navigationhtml, $systemname, $fullusername, $rtl, $siteurl);
|
||||
$outputpage = $output->render_html_page($htmlpage);
|
||||
$this->write_data('index.html', $outputpage);
|
||||
@@ -720,22 +714,4 @@ class moodle_content_writer implements content_writer {
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write url files to the export.
|
||||
*
|
||||
* @param string $url Url of the file.
|
||||
* @param string $path Path to store the file.
|
||||
*/
|
||||
protected function write_url_content(string $url, string $path) {
|
||||
$filepointer = fopen($url, 'r');
|
||||
$targetpath = $this->path . DIRECTORY_SEPARATOR . $path;
|
||||
check_dir_exists(dirname($targetpath), true, true);
|
||||
$status = file_put_contents($targetpath, $filepointer);
|
||||
if ($status === false) {
|
||||
// There was an error. Throw an exception to allow the download status to remain as requiring download.
|
||||
throw new \moodle_exception('Content download was incomplete');
|
||||
}
|
||||
$this->files[$path] = $targetpath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,17 +49,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Data export</title>
|
||||
{{^righttoleft}}
|
||||
<link rel="stylesheet" href="bootstrap.min.css">
|
||||
{{/righttoleft}}
|
||||
{{#righttoleft}}
|
||||
<link rel="stylesheet" href="rtlbootstrap.min.css">
|
||||
{{/righttoleft}}
|
||||
<link rel="stylesheet" type="text/css" href="moodle.css" />
|
||||
<link rel="stylesheet" type="text/css" href="general.css" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-light bg-light border-bottom">
|
||||
<a class="navbar-brand" href="https://www.moodle.org" title="Moodle">
|
||||
<a class="navbar-brand" href="https://moodle.org" title="Moodle">
|
||||
<img class="moodle-logo" src="pix/moodlelogo.svg" alt="Moodle logo" />
|
||||
</a>
|
||||
<span class="navbar-brand">{{#str}}broughtbymoodle, core_privacy{{/str}}</span>
|
||||
@@ -107,4 +102,4 @@
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* A scheduled task to remove unneeded random questions.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @category task
|
||||
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace qtype_random\task;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* A scheduled task to remove unneeded random questions.
|
||||
*
|
||||
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class remove_unused_questions extends \core\task\scheduled_task {
|
||||
|
||||
public function get_name() {
|
||||
return get_string('taskunusedrandomscleanup', 'qtype_random');
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
// Find potentially unused random questions (up to 10000).
|
||||
// Note, because we call question_delete_question below,
|
||||
// the question will not actually be deleted if something else
|
||||
// is using them, but nothing else in Moodle core uses qtype_random,
|
||||
// and not many third-party plugins do.
|
||||
$unusedrandomids = $DB->get_records_sql("
|
||||
SELECT q.id, 1
|
||||
FROM {question} q
|
||||
LEFT JOIN {quiz_slots} qslots ON q.id = qslots.questionid
|
||||
WHERE qslots.questionid IS NULL
|
||||
AND q.qtype = ? AND hidden = ?", ['random', 0], 0, 10000);
|
||||
|
||||
$count = 0;
|
||||
foreach ($unusedrandomids as $unusedrandomid => $notused) {
|
||||
question_delete_question($unusedrandomid);
|
||||
// In case the question was not actually deleted (because it was in use somehow
|
||||
// mark it as hidden so the query above will not return it again.
|
||||
$DB->set_field('question', 'hidden', 1, ['id' => $unusedrandomid]);
|
||||
$count += 1;
|
||||
}
|
||||
mtrace('Cleaned up ' . $count . ' unused random questions.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Definition of question/type/random scheduled tasks.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @category task
|
||||
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$tasks = array(
|
||||
array(
|
||||
'classname' => 'qtype_random\task\remove_unused_questions',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => '*',
|
||||
'day' => '*',
|
||||
'month' => '*',
|
||||
'dayofweek' => '*'
|
||||
)
|
||||
);
|
||||
@@ -33,3 +33,4 @@ $string['randomqname'] = 'Random ({$a})';
|
||||
$string['randomqplusname'] = 'Random ({$a} and subcategories)';
|
||||
$string['selectedby'] = '{$a->questionname} selected by {$a->randomname}';
|
||||
$string['selectmanualquestions'] = 'Random questions can use manually graded questions';
|
||||
$string['taskunusedrandomscleanup'] = 'Remove unused random questions';
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Tests of the scheduled task for cleaning up random questions.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @copyright 2018 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
|
||||
|
||||
/**
|
||||
* Tests of the scheduled task for cleaning up random questions.
|
||||
*
|
||||
* @copyright 2018 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_random_cleanup_task_testcase extends advanced_testcase {
|
||||
|
||||
public function test_cleanup_task_removes_unused_question() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$questiongenerator = $generator->get_plugin_generator('core_question');
|
||||
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$quiz = $quizgenerator->create_instance(['course' => SITEID]);
|
||||
|
||||
// Add two random questions.
|
||||
quiz_add_random_questions($quiz, 0, $cat->id, 2, false);
|
||||
$quizslots = $DB->get_records('quiz_slots', ['quizid' => $quiz->id],
|
||||
'slot', 'slot, id, questionid');
|
||||
|
||||
// Now remove the second from the quiz. (Do it manually,
|
||||
// because the API cleans up the random question, but we are trying to
|
||||
// create an orphaned random question.)
|
||||
$DB->delete_records('quiz_slots', array('id' => $quizslots[2]->id));
|
||||
|
||||
// Run the scheduled task.
|
||||
$task = new \qtype_random\task\remove_unused_questions();
|
||||
$this->expectOutputString("Cleaned up 1 unused random questions.\n");
|
||||
$task->execute();
|
||||
|
||||
// Verify.
|
||||
$this->assertTrue($DB->record_exists('question', ['id' => $quizslots[1]->questionid]));
|
||||
$this->assertFalse($DB->record_exists('question', ['id' => $quizslots[2]->questionid]));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'qtype_random';
|
||||
$plugin->version = 2017111300;
|
||||
$plugin->version = 2017111301;
|
||||
|
||||
$plugin->requires = 2017110800;
|
||||
|
||||
|
||||
@@ -129,26 +129,41 @@ class provider implements
|
||||
/**
|
||||
* Get the SQL required to find all submission items where this user has had any involvements.
|
||||
*
|
||||
* If possible an inner join should be used.
|
||||
*
|
||||
* @param string $alias The name of the table alias to use.
|
||||
* @param string $component The na eof the component to fetch ratings for.
|
||||
* @param string $ratingarea The rating area to fetch results for.
|
||||
* @param string $itemidjoin The right-hand-side of the JOIN ON clause.
|
||||
* @param int $userid The ID of the user being stored.
|
||||
* @param bool $innerjoin Whether to use an inner join (preferred)
|
||||
* @return \stdClass
|
||||
*/
|
||||
public static function get_sql_join($alias, $component, $ratingarea, $itemidjoin, $userid) {
|
||||
public static function get_sql_join($alias, $component, $ratingarea, $itemidjoin, $userid, $innerjoin = false) {
|
||||
static $count = 0;
|
||||
$count++;
|
||||
|
||||
// Join the rating table with the specified alias and the relevant join params.
|
||||
$join = "LEFT JOIN {rating} {$alias} ON ";
|
||||
$join .= "{$alias}.userid = :ratinguserid{$count} AND ";
|
||||
$join .= "{$alias}.component = :ratingcomponent{$count} AND ";
|
||||
$join .= "{$alias}.ratingarea = :ratingarea{$count} AND ";
|
||||
$join .= "{$alias}.itemid = {$itemidjoin}";
|
||||
$userwhere = '';
|
||||
|
||||
// Match against the specified user.
|
||||
$userwhere = "{$alias}.id IS NOT NULL";
|
||||
if ($innerjoin) {
|
||||
// Join the rating table with the specified alias and the relevant join params.
|
||||
$join = "JOIN {rating} {$alias} ON ";
|
||||
$join .= "{$alias}.itemid = {$itemidjoin}";
|
||||
|
||||
$userwhere .= "{$alias}.userid = :ratinguserid{$count} AND ";
|
||||
$userwhere .= "{$alias}.component = :ratingcomponent{$count} AND ";
|
||||
$userwhere .= "{$alias}.ratingarea = :ratingarea{$count}";
|
||||
} else {
|
||||
// Join the rating table with the specified alias and the relevant join params.
|
||||
$join = "LEFT JOIN {rating} {$alias} ON ";
|
||||
$join .= "{$alias}.userid = :ratinguserid{$count} AND ";
|
||||
$join .= "{$alias}.component = :ratingcomponent{$count} AND ";
|
||||
$join .= "{$alias}.ratingarea = :ratingarea{$count} AND ";
|
||||
$join .= "{$alias}.itemid = {$itemidjoin}";
|
||||
|
||||
// Match against the specified user.
|
||||
$userwhere = "{$alias}.id IS NOT NULL";
|
||||
}
|
||||
|
||||
$params = [
|
||||
'ratingcomponent' . $count => $component,
|
||||
|
||||
@@ -153,6 +153,94 @@ class core_rating_privacy_testcase extends \core_privacy\tests\provider_testcase
|
||||
$this->assertFalse(isset($courses[$course3->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the get_sql_join function returns valid SQL which returns the correct list of rated itemids.
|
||||
* This makes use of the optional inner join argument.
|
||||
*/
|
||||
public function test_get_sql_join_inner() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
$course3 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
$u2 = $this->getDataGenerator()->create_user();
|
||||
$u3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Rate the courses.
|
||||
$rm = new rating_manager();
|
||||
$ratingoptions = (object) [
|
||||
'component' => 'core_course',
|
||||
'ratingarea' => 'course',
|
||||
'scaleid' => 100,
|
||||
];
|
||||
|
||||
// Rate all courses as u1, and something else in the same context.
|
||||
$this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25);
|
||||
$this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50);
|
||||
$this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75);
|
||||
$this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99);
|
||||
|
||||
// Rate course2 as u2, and something else in a different context/component..
|
||||
$this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90);
|
||||
$this->rate_as_user($u2->id, 'user', 'user', $u3->id, \context_user::instance($u3->id), 10);
|
||||
|
||||
// Return any course which the u1 has rated.
|
||||
// u1 rated all three courses.
|
||||
$ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u1->id, true);
|
||||
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
|
||||
$courses = $DB->get_records_sql($sql, $ratingquery->params);
|
||||
|
||||
$this->assertCount(3, $courses);
|
||||
$this->assertTrue(isset($courses[$course1->id]));
|
||||
$this->assertTrue(isset($courses[$course2->id]));
|
||||
$this->assertTrue(isset($courses[$course3->id]));
|
||||
|
||||
// User u1 rated files in course 3 only.
|
||||
$ratingquery = provider::get_sql_join('r', 'core_course', 'files', 'c.id', $u1->id, true);
|
||||
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
|
||||
$courses = $DB->get_records_sql($sql, $ratingquery->params);
|
||||
|
||||
$this->assertCount(1, $courses);
|
||||
$this->assertFalse(isset($courses[$course1->id]));
|
||||
$this->assertFalse(isset($courses[$course2->id]));
|
||||
$this->assertTrue(isset($courses[$course3->id]));
|
||||
|
||||
// Return any course which the u2 has rated.
|
||||
// User u2 rated only course 2.
|
||||
$ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u2->id, true);
|
||||
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
|
||||
$courses = $DB->get_records_sql($sql, $ratingquery->params);
|
||||
|
||||
$this->assertCount(1, $courses);
|
||||
$this->assertFalse(isset($courses[$course1->id]));
|
||||
$this->assertTrue(isset($courses[$course2->id]));
|
||||
$this->assertFalse(isset($courses[$course3->id]));
|
||||
|
||||
// User u2 rated u3.
|
||||
$ratingquery = provider::get_sql_join('r', 'user', 'user', 'u.id', $u2->id, true);
|
||||
$sql = "SELECT u.id FROM {user} u {$ratingquery->join} WHERE {$ratingquery->userwhere}";
|
||||
$users = $DB->get_records_sql($sql, $ratingquery->params);
|
||||
|
||||
$this->assertCount(1, $users);
|
||||
$this->assertFalse(isset($users[$u1->id]));
|
||||
$this->assertFalse(isset($users[$u2->id]));
|
||||
$this->assertTrue(isset($users[$u3->id]));
|
||||
|
||||
// Return any course which the u3 has rated.
|
||||
// User u3 did not rate anything.
|
||||
$ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u3->id, true);
|
||||
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
|
||||
$courses = $DB->get_records_sql($sql, $ratingquery->params);
|
||||
|
||||
$this->assertCount(0, $courses);
|
||||
$this->assertFalse(isset($courses[$course1->id]));
|
||||
$this->assertFalse(isset($courses[$course2->id]));
|
||||
$this->assertFalse(isset($courses[$course3->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that export_area_ratings exports all ratings that a user has made, and all ratings for a users own content.
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -646,7 +646,6 @@ class core_userliblib_testcase extends advanced_testcase {
|
||||
$this->setUser($user8);
|
||||
$this->assertFalse(user_can_view_profile($user1));
|
||||
|
||||
$allroles = $DB->get_records_menu('role', array(), 'id', 'archetype, id');
|
||||
// Let us test with guest user.
|
||||
$this->setGuestUser();
|
||||
$CFG->forceloginforprofiles = 1;
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ echo $OUTPUT->header();
|
||||
|
||||
$rs = $DB->get_recordset_select("user", "deleted = 0 AND picture > 0", array(), "lastaccess DESC", user_picture::fields());
|
||||
foreach ($rs as $user) {
|
||||
$fullname = fullname($user);
|
||||
$fullname = s(fullname($user));
|
||||
echo "<a href=\"$CFG->wwwroot/user/view.php?id=$user->id&course=1\" ".
|
||||
"title=\"$fullname\">";
|
||||
echo $OUTPUT->user_picture($user);
|
||||
|
||||
+2
-2
@@ -29,11 +29,11 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2017111306.00; // 20171113 = branching date YYYYMMDD - do not modify!
|
||||
$version = 2017111308.00; // 20171113 = branching date YYYYMMDD - do not modify!
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
|
||||
$release = '3.4.6 (Build: 20181112)'; // Human-friendly version name
|
||||
$release = '3.4.8 (Build: 20190311)'; // Human-friendly version name
|
||||
|
||||
$branch = '34'; // This version's branch.
|
||||
$maturity = MATURITY_STABLE; // This version's maturity level.
|
||||
|
||||
Reference in New Issue
Block a user