}
// If necessary add files that should be ignored - such as in 3rd party plugins.
-$blacklist = array();
+$ignorelist = array();
$path = $options['path'];
if (!file_exists($path)) {
cli_error("Invalid path $path");
}
if ($options['ie9fix']) {
- core_admin_recurse_svgs($path, '', 'core_admin_svgtool_ie9fix', $blacklist);
+ core_admin_recurse_svgs($path, '', 'core_admin_svgtool_ie9fix', $ignorelist);
} else if ($options['noaspectratio']) {
- core_admin_recurse_svgs($path, '', 'core_admin_svgtool_noaspectratio', $blacklist);
+ core_admin_recurse_svgs($path, '', 'core_admin_svgtool_noaspectratio', $ignorelist);
} else {
$help =
* @param string $base
* @param string $sub
* @param string $filecallback
- * @param array $blacklist
+ * @param array $ignorelist List of files to be ignored and skipped.
*/
-function core_admin_recurse_svgs($base, $sub, $filecallback, $blacklist) {
+function core_admin_recurse_svgs($base, $sub, $filecallback, $ignorelist) {
if (is_dir("$base/$sub")) {
$items = new DirectoryIterator("$base/$sub");
foreach ($items as $item) {
continue;
}
$file = $item->getFilename();
- core_admin_recurse_svgs("$base/$sub", $file, $filecallback, $blacklist);
+ core_admin_recurse_svgs("$base/$sub", $file, $filecallback, $ignorelist);
}
unset($item);
unset($items);
return;
}
$file = realpath("$base/$sub");
- if (in_array($file, $blacklist)) {
+ if (in_array($file, $ignorelist)) {
return;
}
$filecallback($file);
new lang_string('configallowedemaildomains', 'admin'),
''));
+ $temp->add(new admin_setting_heading('divertallemailsheading', new lang_string('divertallemails', 'admin'),
+ new lang_string('divertallemailsdetail', 'admin')));
+ $temp->add(new admin_setting_configtext('divertallemailsto',
+ new lang_string('divertallemailsto', 'admin'),
+ new lang_string('divertallemailsto_desc', 'admin'),
+ ''));
+ $temp->add(new admin_setting_configtextarea('divertallemailsexcept',
+ new lang_string('divertallemailsexcept', 'admin'),
+ new lang_string('divertallemailsexcept_desc', 'admin'),
+ '', PARAM_RAW, '50', '4'));
+
$url = new moodle_url('/admin/testoutgoingmailconf.php');
$link = html_writer::link($url, get_string('testoutgoingmailconf', 'admin'));
$temp->add(new admin_setting_heading('testoutgoinmailc', new lang_string('testoutgoingmailconf', 'admin'),
*/
private static function get_tree_node_structure($allowchildbranches = true) {
$fields = [
- 'text' => new external_value(PARAM_TEXT, 'The node text', VALUE_REQUIRED),
+ 'text' => new external_value(PARAM_RAW, 'The node text', VALUE_REQUIRED),
'expandcontextid' => new external_value(PARAM_INT, 'The contextid this node expands', VALUE_REQUIRED),
'expandelement' => new external_value(PARAM_ALPHA, 'What element is this node expanded to', VALUE_REQUIRED),
'contextid' => new external_value(PARAM_INT, 'The node contextid', VALUE_REQUIRED),
$params = [
'data-action' => 'contactdpo',
'data-replytoemail' => $replytoemail,
- 'class' => 'contactdpo'
];
return html_writer::link('#', get_string('contactdataprotectionofficer', 'tool_dataprivacy'), $params);
}
$dblibrary = $matches[2];
if ($dbtype === 'sqlite3') {
- // Blacklist unfinished drivers.
+ // The sqlite3 driver is not fully working yet and should not be returned.
continue;
}
array(
'wwwroot' => new external_value(PARAM_RAW, 'Site URL.'),
'httpswwwroot' => new external_value(PARAM_RAW, 'Site https URL (if httpslogin is enabled).'),
- 'sitename' => new external_value(PARAM_TEXT, 'Site name.'),
+ 'sitename' => new external_value(PARAM_RAW, 'Site name.'),
'guestlogin' => new external_value(PARAM_INT, 'Whether guest login is enabled.'),
'rememberusername' => new external_value(PARAM_INT, 'Values: 0 for No, 1 for Yes, 2 for optional.'),
'authloginviaemail' => new external_value(PARAM_INT, 'Whether log in via email is enabled.'),
--- /dev/null
+mobileappconnected,tool_mobile
\ No newline at end of file
$string['minimumversion'] = 'If an app version is specified (3.8.0 or higher), any users using an older app version will be prompted to upgrade their app before being allowed access to the site.';
$string['minimumversion_key'] = 'Minimum app version required';
$string['mobileapp'] = 'Mobile app';
-$string['mobileappconnected'] = 'Mobile app connected';
$string['mobileappenabled'] = 'This site has mobile app access enabled.<br /><a href="{$a}">Download the mobile app</a>.';
$string['mobileappearance'] = 'Mobile appearance';
$string['mobileappsubscription'] = 'Moodle app subscription';
$string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
$string['responsivemainmenuitems'] = 'Responsive menu items';
$string['viewqrcode'] = 'View QR code';
+
+// Deprecated since Moodle 3.10.
+$string['mobileappconnected'] = 'Mobile app connected';
}
/**
- * Checks if the given user has a mobile token (has used recently the app).
+ * Return the user mobile app WebService access token.
*
- * @param int $userid the user to check
- * @return bool true if the user has a token, false otherwise.
+ * @param int $userid the user to return the token from
+ * @return stdClass|false the token or false if the token doesn't exists
+ * @since 3.10
*/
-function tool_mobile_user_has_token($userid) {
+function tool_mobile_get_token($userid) {
global $DB;
- $sql = "SELECT 1
+ $sql = "SELECT t.*
FROM {external_tokens} t, {external_services} s
WHERE t.externalserviceid = s.id
AND s.enabled = 1
AND s.shortname IN ('moodle_mobile_app', 'local_mobile')
AND t.userid = ?";
- return $DB->record_exists_sql($sql, [$userid]);
+ return $DB->get_record_sql($sql, [$userid], IGNORE_MULTIPLE);
+}
+
+/**
+ * Checks if the given user has a mobile token (has used recently the app).
+ *
+ * @param int $userid the user to check
+ * @return bool true if the user has a token, false otherwise.
+ */
+function tool_mobile_user_has_token($userid) {
+
+ return !empty(tool_mobile_get_token($userid));
}
/**
}
// Check if the user is using the app, encouraging him to use it otherwise.
- $userhastoken = tool_mobile_user_has_token($user->id);
+ $usertoken = tool_mobile_get_token($user->id);
$mobilestrconnected = null;
-
- if ($userhastoken) {
- $mobilestrconnected = get_string('mobileappconnected', 'tool_mobile');
+ $mobilelastaccess = null;
+
+ if ($usertoken) {
+ $mobilestrconnected = get_string('lastsiteaccess');
+ if ($usertoken->lastaccess) {
+ $mobilelastaccess = userdate($usertoken->lastaccess) . " (" . format_time(time() - $usertoken->lastaccess) . ")";
+ } else {
+ // We should not reach this point.
+ $mobilelastaccess = get_string("never");
+ }
} else if ($url = tool_mobile_create_app_download_url()) {
$mobilestrconnected = get_string('mobileappenabled', 'tool_mobile', $url->out());
}
if ($mobilestrconnected) {
- $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestrconnected, null);
+ $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestrconnected, null, null,
+ $mobilelastaccess);
}
// Add nodes, if any.
/**
* Returns meta data about this system.
*
- * @param collection $itemcollection The initialised item collection to add items to.
+ * @param collection $items The initialised item collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $items) : collection {
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
- $preferences = get_user_preferences();
+ $preferences = get_user_preferences(null, null, $userid);
foreach ($preferences as $name => $value) {
$descriptionidentifier = null;
$tourid = null;
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Unit tests for the block_html implementation of the privacy API.
+ * Unit tests for the tool_usertours implementation of the privacy API.
*
- * @package block_html
+ * @package tool_usertours
* @category test
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\writer;
-use \core_privacy\local\request\approved_contextlist;
-use \core_privacy\local\request\deletion_criteria;
use \tool_usertours\tour;
use \tool_usertours\privacy\provider;
/**
- * Unit tests for the block_html implementation of the privacy API.
+ * Unit tests for the tool_usertours implementation of the privacy API.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class tool_usertours_privacy_testcase extends \core_privacy\tests\provider_testcase {
+class tool_usertours_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
+ /**
+ * Helper method for creating a tour
+ *
+ * @return tour
+ */
protected function create_test_tour(): tour {
return (new tour())
->set_name('test_tour')
$this->assertCount(2, (array) $prefs);
}
+ /**
+ * Make sure we are exporting preferences for the correct user
+ */
+ public function test_export_user_preferences_correct_user(): void {
+ $this->resetAfterTest();
+
+ $tour = $this->create_test_tour();
+
+ // Create test user, mark them as having completed the tour.
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+ $tour->mark_user_completed();
+
+ // Switch to admin user, mark them as having reset the tour.
+ $this->setAdminUser();
+ $tour->request_user_reset();
+
+ // Export test users preferences.
+ provider::export_user_preferences($user->id);
+
+ $writer = writer::with_context(\context_system::instance());
+ $this->assertTrue($writer->has_any_data());
+
+ $prefs = $writer->get_user_preferences('tool_usertours');
+ $this->assertCount(1, (array) $prefs);
+
+ // We should have received back the "completed tour" preference of the test user.
+ $this->assertStringStartsWith('You last marked the "' . $tour->get_name() . '" user tour as completed on',
+ reset($prefs)->description);
+ }
+
/**
* Ensure that export_user_preferences excludes deleted tours.
*/
}
}
-// whitelisting security
+// Allow only functions available for testing.
if (!isset($functions[$function])) {
$function = '';
}
}
$protocols[$p] = get_string('pluginname', 'webservice_'.$p);
}
-if (!isset($protocols[$protocol])) { // whitelisting security
+
+// Allow only protocols supporting the test client.
+if (!isset($protocols[$protocol])) {
$protocol = '';
}
array(
'id' => new external_value(PARAM_INT, 'Profile field id', VALUE_OPTIONAL),
'shortname' => new external_value(PARAM_ALPHANUMEXT, 'Profile field shortname', VALUE_OPTIONAL),
- 'name' => new external_value(PARAM_TEXT, 'Profield field name', VALUE_OPTIONAL),
+ 'name' => new external_value(PARAM_RAW, 'Profield field name', VALUE_OPTIONAL),
'datatype' => new external_value(PARAM_ALPHANUMEXT, 'Profield field datatype', VALUE_OPTIONAL),
'description' => new external_value(PARAM_RAW, 'Profield field description', VALUE_OPTIONAL),
'descriptionformat' => new external_format_value('description'),
'categoryid' => new external_value(PARAM_INT, 'Profield field category id', VALUE_OPTIONAL),
- 'categoryname' => new external_value(PARAM_TEXT, 'Profield field category name', VALUE_OPTIONAL),
+ 'categoryname' => new external_value(PARAM_RAW, 'Profield field category name', VALUE_OPTIONAL),
'sortorder' => new external_value(PARAM_INT, 'Profield field sort order', VALUE_OPTIONAL),
'required' => new external_value(PARAM_INT, 'Profield field required', VALUE_OPTIONAL),
'locked' => new external_value(PARAM_INT, 'Profield field locked', VALUE_OPTIONAL),
$this->assertEquals('textarea', $namedarray['sometext']['datatype']);
}
+ /**
+ * Test get_signup_settings with mathjax in a profile field.
+ */
+ public function test_get_signup_settings_with_mathjax_in_profile_fields() {
+ global $CFG, $DB;
+
+ require_once($CFG->dirroot . '/lib/externallib.php');
+
+ // Enable MathJax filter in content and headings.
+ $this->configure_filters([
+ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+ ]);
+
+ // Create category with MathJax and a new field with MathJax.
+ $categoryname = 'Cat $$(a+b)=2$$';
+ $fieldname = 'Some text $$(a+b)=2$$';
+ $categoryid = $DB->insert_record('user_info_category', array('name' => $categoryname, 'sortorder' => 1));
+ $field3 = $DB->insert_record('user_info_field', array(
+ 'shortname' => 'mathjaxname', 'name' => $fieldname, 'categoryid' => $categoryid,
+ 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2));
+
+ $result = auth_email_external::get_signup_settings();
+ $result = external_api::clean_returnvalue(auth_email_external::get_signup_settings_returns(), $result);
+
+ // Format the original data.
+ $sitecontext = context_system::instance();
+ $categoryname = external_format_string($categoryname, $sitecontext->id);
+ $fieldname = external_format_string($fieldname, $sitecontext->id);
+
+ // Whip up a array with named entries to easily check against.
+ $namedarray = array();
+ foreach ($result['profilefields'] as $key => $value) {
+ $namedarray[$value['shortname']] = $value;
+ }
+
+ // Check the new profile field.
+ $this->assertArrayHasKey('mathjaxname', $namedarray);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+ $namedarray['mathjaxname']['categoryname']);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+ $namedarray['mathjaxname']['name']);
+ $this->assertEquals($categoryname, $namedarray['mathjaxname']['categoryname']);
+ $this->assertEquals($fieldname, $namedarray['mathjaxname']['name']);
+ }
+
public function test_signup_user() {
global $DB;
$this->assertFalse($badge->is_issued($this->user->id));
// Mark course as complete.
- $sink = $this->redirectEmails();
+ $sink = $this->redirectMessages();
$ccompletion->mark_complete();
- $this->assertCount(1, $sink->get_messages());
+ // Two messages are generated: One for the course completed and the other one for the badge awarded.
+ $messages = $sink->get_messages();
+ $this->assertCount(2, $messages);
+ $this->assertEquals('badgerecipientnotice', $messages[0]->eventtype);
+ $this->assertEquals('coursecompleted', $messages[1]->eventtype);
$sink->close();
// Check if badge is awarded.
'visible' => new external_value(PARAM_BOOL, 'Whether the block is visible.', VALUE_OPTIONAL),
'contents' => new external_single_structure(
array(
- 'title' => new external_value(PARAM_TEXT, 'Block title.'),
+ 'title' => new external_value(PARAM_RAW, 'Block title.'),
'content' => new external_value(PARAM_RAW, 'Block contents.'),
'contentformat' => new external_format_value('content'),
'footer' => new external_value(PARAM_RAW, 'Block footer.'),
$this->assertEquals(5, $configcounts);
}
+ /**
+ * Test get_course_blocks contents with mathjax.
+ */
+ public function test_get_course_blocks_contents_with_mathjax() {
+ global $DB, $CFG;
+
+ require_once($CFG->dirroot . '/lib/externallib.php');
+
+ $this->resetAfterTest(true);
+
+ // Enable MathJax filter in content and headings.
+ $this->configure_filters([
+ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+ ]);
+
+ // Create a few stuff to test with.
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course();
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+ $coursecontext = context_course::instance($course->id);
+
+ // Create a HTML block.
+ $title = 'My block $$(a+b)=2$$';
+ $body = 'My block contents $$(a+b)=2$$';
+ $bodyformat = FORMAT_MOODLE;
+ $page = new moodle_page();
+ $page->set_context($coursecontext);
+ $page->set_pagelayout('course');
+ $course->format = course_get_format($course)->get_format();
+ $page->set_pagetype('course-view-' . $course->format);
+ $page->blocks->load_blocks();
+ $newblock = 'html';
+ $page->blocks->add_block_at_end_of_default_region($newblock);
+
+ $this->setUser($user);
+ // Re-create the page.
+ $page = new moodle_page();
+ $page->set_context($coursecontext);
+ $page->set_pagelayout('course');
+ $course->format = course_get_format($course)->get_format();
+ $page->set_pagetype('course-view-' . $course->format);
+ $page->blocks->load_blocks();
+ $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
+ $block = end($blocks);
+ $block = block_instance('html', $block->instance);
+ $nonscalar = [
+ 'something' => true,
+ ];
+ $configdata = (object) [
+ 'title' => $title,
+ 'text' => [
+ 'itemid' => 0,
+ 'text' => $body,
+ 'format' => $bodyformat,
+ ],
+ 'nonscalar' => $nonscalar
+ ];
+ $block->instance_config_save((object) $configdata);
+
+ // Check for the new block.
+ $result = core_block_external::get_course_blocks($course->id, true);
+ $result = external_api::clean_returnvalue(core_block_external::get_course_blocks_returns(), $result);
+
+ // Format the original data.
+ $sitecontext = context_system::instance();
+ $title = external_format_string($title, $coursecontext->id);
+ list($body, $bodyformat) = external_format_text($body, $bodyformat, $coursecontext->id, 'block_html', 'content');
+
+ // Check that the block data is formatted.
+ $this->assertCount(1, $result['blocks']);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+ $result['blocks'][0]['contents']['title']);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+ $result['blocks'][0]['contents']['content']);
+ $this->assertEquals($title, $result['blocks'][0]['contents']['title']);
+ $this->assertEquals($body, $result['blocks'][0]['contents']['content']);
+ }
+
/**
* Test user get default dashboard blocks.
*/
$action = optional_param('action', null, PARAM_ALPHA);
admin_externalpage_setup('cacheconfig');
-$context = context_system::instance();
+$adminhelper = cache_factory::instance()->get_administration_display_helper();
-$storeinstancesummaries = cache_administration_helper::get_store_instance_summaries();
-$storepluginsummaries = cache_administration_helper::get_store_plugin_summaries();
-$definitionsummaries = cache_administration_helper::get_definition_summaries();
-$defaultmodestores = cache_administration_helper::get_default_mode_stores();
-$locks = cache_administration_helper::get_lock_summaries();
-
-$title = new lang_string('cacheadmin', 'cache');
-$mform = null;
$notifications = array();
-$notifysuccess = true;
+// Empty array to hold any form information returned from actions.
+$forminfo = [];
+// Handle page actions in admin helper class.
if (!empty($action) && confirm_sesskey()) {
- switch ($action) {
- case 'rescandefinitions' : // Rescan definitions.
- cache_config_writer::update_definitions();
- redirect($PAGE->url);
- break;
- case 'addstore' : // Add the requested store.
- $plugin = required_param('plugin', PARAM_PLUGIN);
- if (!$storepluginsummaries[$plugin]['canaddinstance']) {
- print_error('ex_unmetstorerequirements', 'cache');
- }
- $mform = cache_administration_helper::get_add_store_form($plugin);
- $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
- if ($mform->is_cancelled()) {
- redirect($PAGE->url);
- } else if ($data = $mform->get_data()) {
- $config = cache_administration_helper::get_store_configuration_from_data($data);
- $writer = cache_config_writer::instance();
- unset($config['lock']);
- foreach ($writer->get_locks() as $lock => $lockconfig) {
- if ($lock == $data->lock) {
- $config['lock'] = $data->lock;
- }
- }
- $writer->add_store_instance($data->name, $data->plugin, $config);
- redirect($PAGE->url, get_string('addstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
- }
- break;
- case 'editstore' : // Edit the requested store.
- $plugin = required_param('plugin', PARAM_PLUGIN);
- $store = required_param('store', PARAM_TEXT);
- $mform = cache_administration_helper::get_edit_store_form($plugin, $store);
- $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
- if ($mform->is_cancelled()) {
- redirect($PAGE->url);
- } else if ($data = $mform->get_data()) {
- $config = cache_administration_helper::get_store_configuration_from_data($data);
- $writer = cache_config_writer::instance();
-
- unset($config['lock']);
- foreach ($writer->get_locks() as $lock => $lockconfig) {
- if ($lock == $data->lock) {
- $config['lock'] = $data->lock;
- }
- }
- $writer->edit_store_instance($data->name, $data->plugin, $config);
- redirect($PAGE->url, get_string('editstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
- }
- break;
- case 'deletestore' : // Delete a given store.
- $store = required_param('store', PARAM_TEXT);
- $confirm = optional_param('confirm', false, PARAM_BOOL);
-
- if (!array_key_exists($store, $storeinstancesummaries)) {
- $notifysuccess = false;
- $notifications[] = array(get_string('invalidstore', 'cache'), false);
- } else if ($storeinstancesummaries[$store]['mappings'] > 0) {
- $notifysuccess = false;
- $notifications[] = array(get_string('deletestorehasmappings', 'cache'), false);
- }
-
- if ($notifysuccess) {
- if (!$confirm) {
- $title = get_string('confirmstoredeletion', 'cache');
- $params = array('store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
- $url = new moodle_url($PAGE->url, $params);
- $button = new single_button($url, get_string('deletestore', 'cache'));
-
- $PAGE->set_title($title);
- $PAGE->set_heading($SITE->fullname);
- echo $OUTPUT->header();
- echo $OUTPUT->heading($title);
- $confirmation = get_string('deletestoreconfirmation', 'cache', $storeinstancesummaries[$store]['name']);
- echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
- echo $OUTPUT->footer();
- exit;
- } else {
- $writer = cache_config_writer::instance();
- $writer->delete_store_instance($store);
- redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5);
- }
- }
- break;
- case 'editdefinitionmapping' : // Edit definition mappings.
- $definition = required_param('definition', PARAM_SAFEPATH);
- if (!array_key_exists($definition, $definitionsummaries)) {
- throw new cache_exception('Invalid cache definition requested');
- }
- $title = get_string('editdefinitionmappings', 'cache', $definition);
- $mform = new cache_definition_mappings_form($PAGE->url, array('definition' => $definition));
- if ($mform->is_cancelled()) {
- redirect($PAGE->url);
- } else if ($data = $mform->get_data()) {
- $writer = cache_config_writer::instance();
- $mappings = array();
- foreach ($data->mappings as $mapping) {
- if (!empty($mapping)) {
- $mappings[] = $mapping;
- }
- }
- $writer->set_definition_mappings($definition, $mappings);
- redirect($PAGE->url);
- }
- break;
- case 'editdefinitionsharing' :
- $definition = required_param('definition', PARAM_SAFEPATH);
- if (!array_key_exists($definition, $definitionsummaries)) {
- throw new cache_exception('Invalid cache definition requested');
- }
- $title = get_string('editdefinitionsharing', 'cache', $definition);
- $sharingoptions = $definitionsummaries[$definition]['sharingoptions'];
- $customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
- $mform = new cache_definition_sharing_form($PAGE->url, $customdata);
- $mform->set_data(array(
- 'sharing' => $definitionsummaries[$definition]['selectedsharingoption'],
- 'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey']
- ));
- if ($mform->is_cancelled()) {
- redirect($PAGE->url);
- } else if ($data = $mform->get_data()) {
- $component = $definitionsummaries[$definition]['component'];
- $area = $definitionsummaries[$definition]['area'];
- // Purge the stores removing stale data before we alter the sharing option.
- cache_helper::purge_stores_used_by_definition($component, $area);
- $writer = cache_config_writer::instance();
- $sharing = array_sum(array_keys($data->sharing));
- $userinputsharingkey = $data->userinputsharingkey;
- $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
- redirect($PAGE->url);
- }
- break;
- case 'editmodemappings': // Edit default mode mappings.
- $mform = new cache_mode_mappings_form(null, $storeinstancesummaries);
- $mform->set_data(array(
- 'mode_'.cache_store::MODE_APPLICATION => key($defaultmodestores[cache_store::MODE_APPLICATION]),
- 'mode_'.cache_store::MODE_SESSION => key($defaultmodestores[cache_store::MODE_SESSION]),
- 'mode_'.cache_store::MODE_REQUEST => key($defaultmodestores[cache_store::MODE_REQUEST]),
- ));
- if ($mform->is_cancelled()) {
- redirect($PAGE->url);
- } else if ($data = $mform->get_data()) {
- $mappings = array(
- cache_store::MODE_APPLICATION => array($data->{'mode_'.cache_store::MODE_APPLICATION}),
- cache_store::MODE_SESSION => array($data->{'mode_'.cache_store::MODE_SESSION}),
- cache_store::MODE_REQUEST => array($data->{'mode_'.cache_store::MODE_REQUEST}),
- );
- $writer = cache_config_writer::instance();
- $writer->set_mode_mappings($mappings);
- redirect($PAGE->url);
- }
- break;
-
- case 'purgedefinition': // Purge a specific definition.
- $id = required_param('definition', PARAM_SAFEPATH);
- list($component, $area) = explode('/', $id, 2);
- $factory = cache_factory::instance();
- $definition = $factory->create_definition($component, $area);
- if ($definition->has_required_identifiers()) {
- // We will have to purge the stores used by this definition.
- cache_helper::purge_stores_used_by_definition($component, $area);
- } else {
- // Alrighty we can purge just the data belonging to this definition.
- cache_helper::purge_by_definition($component, $area);
- }
-
- $message = get_string('purgexdefinitionsuccess', 'cache', [
- 'name' => $definition->get_name(),
- 'component' => $component,
- 'area' => $area,
- ]);
- $purgeagainlink = html_writer::link(new moodle_url('/cache/admin.php', [
- 'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id]),
- get_string('purgeagain', 'cache'));
- redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
- break;
-
- case 'purgestore':
- case 'purge': // Purge a store cache.
- $store = required_param('store', PARAM_TEXT);
- cache_helper::purge_store($store);
- $message = get_string('purgexstoresuccess', 'cache', ['store' => $store]);
- $purgeagainlink = html_writer::link(new moodle_url('/cache/admin.php', [
- 'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store]),
- get_string('purgeagain', 'cache'));
- redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
- break;
-
- case 'newlockinstance':
- // Adds a new lock instance.
- $lock = required_param('lock', PARAM_ALPHANUMEXT);
- $mform = cache_administration_helper::get_add_lock_form($lock);
- if ($mform->is_cancelled()) {
- redirect($PAGE->url);
- } else if ($data = $mform->get_data()) {
- $factory = cache_factory::instance();
- $config = $factory->create_config_instance(true);
- $name = $data->name;
- $data = cache_administration_helper::get_lock_configuration_from_data($lock, $data);
- $config->add_lock_instance($name, $lock, $data);
- redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
- }
- break;
- case 'deletelock':
- // Deletes a lock instance.
- $lock = required_param('lock', PARAM_ALPHANUMEXT);
- $confirm = optional_param('confirm', false, PARAM_BOOL);
- if (!array_key_exists($lock, $locks)) {
- $notifysuccess = false;
- $notifications[] = array(get_string('invalidlock', 'cache'), false);
- } else if ($locks[$lock]['uses'] > 0) {
- $notifysuccess = false;
- $notifications[] = array(get_string('deletelockhasuses', 'cache'), false);
- }
- if ($notifysuccess) {
- if (!$confirm) {
- $title = get_string('confirmlockdeletion', 'cache');
- $params = array('lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
- $url = new moodle_url($PAGE->url, $params);
- $button = new single_button($url, get_string('deletelock', 'cache'));
-
- $PAGE->set_title($title);
- $PAGE->set_heading($SITE->fullname);
- echo $OUTPUT->header();
- echo $OUTPUT->heading($title);
- $confirmation = get_string('deletelockconfirmation', 'cache', $lock);
- echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
- echo $OUTPUT->footer();
- exit;
- } else {
- $writer = cache_config_writer::instance();
- $writer->delete_lock_instance($lock);
- redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5);
- }
- }
- break;
- }
+ $forminfo = $adminhelper->perform_cache_actions($action, $forminfo);
}
// Add cache store warnings to the list of notifications.
// Obviously as these are warnings they are show as failures.
-foreach (cache_helper::warnings($storeinstancesummaries) as $warning) {
+foreach (cache_helper::warnings(core_cache\administration_helper::get_store_instance_summaries()) as $warning) {
$notifications[] = array($warning, false);
}
+// Decide on display mode based on returned forminfo.
+$mform = array_key_exists('form', $forminfo) ? $forminfo['form'] : null;
+$title = array_key_exists('title', $forminfo) ? $forminfo['title'] : new lang_string('cacheadmin', 'cache');
+
$PAGE->set_title($title);
$PAGE->set_heading($SITE->fullname);
/* @var core_cache_renderer $renderer */
if ($mform instanceof moodleform) {
$mform->display();
} else {
- echo $renderer->store_plugin_summaries($storepluginsummaries);
- echo $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries);
- echo $renderer->definition_summaries($definitionsummaries, $context);
- echo $renderer->lock_summaries($locks);
-
- $applicationstore = join(', ', $defaultmodestores[cache_store::MODE_APPLICATION]);
- $sessionstore = join(', ', $defaultmodestores[cache_store::MODE_SESSION]);
- $requeststore = join(', ', $defaultmodestores[cache_store::MODE_REQUEST]);
- $editurl = new moodle_url('/cache/admin.php', array('action' => 'editmodemappings', 'sesskey' => sesskey()));
- echo $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl);
+ // Handle main page definition in admin helper class.
+ echo $adminhelper->generate_admin_page($renderer);
}
echo $renderer->footer();
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache administration helper.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package core
+ * @category cache
+ * @author Peter Burnett <peterburnett@catalyst-au.net>
+ * @copyright 2020 Catalyst IT
+ * @copyright 2012 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_cache;
+
+defined('MOODLE_INTERNAL') || die();
+use cache_helper, cache_store, cache_config, cache_factory, cache_definition;
+
+/**
+ * Administration helper base class.
+ *
+ * Defines abstract methods for a subclass to define the admin page.
+ *
+ * @package core
+ * @category cache
+ * @author Peter Burnett <peterburnett@catalyst-au.net>
+ * @copyright 2020 Catalyst IT
+ * @copyright 2012 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class administration_helper extends cache_helper {
+
+ /**
+ * Returns an array containing all of the information about stores a renderer needs.
+ * @return array
+ */
+ public static function get_store_instance_summaries(): array {
+ $return = array();
+ $default = array();
+ $instance = \cache_config::instance();
+ $stores = $instance->get_all_stores();
+ $locks = $instance->get_locks();
+ foreach ($stores as $name => $details) {
+ $class = $details['class'];
+ $store = false;
+ if ($class::are_requirements_met()) {
+ $store = new $class($details['name'], $details['configuration']);
+ }
+ $lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock();
+ $record = array(
+ 'name' => $name,
+ 'plugin' => $details['plugin'],
+ 'default' => $details['default'],
+ 'isready' => $store ? $store->is_ready() : false,
+ 'requirementsmet' => $class::are_requirements_met(),
+ 'mappings' => 0,
+ 'lock' => $lock,
+ 'modes' => array(
+ cache_store::MODE_APPLICATION =>
+ ($class::get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
+ cache_store::MODE_SESSION =>
+ ($class::get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
+ cache_store::MODE_REQUEST =>
+ ($class::get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
+ ),
+ 'supports' => array(
+ 'multipleidentifiers' => $store ? $store->supports_multiple_identifiers() : false,
+ 'dataguarantee' => $store ? $store->supports_data_guarantee() : false,
+ 'nativettl' => $store ? $store->supports_native_ttl() : false,
+ 'nativelocking' => ($store instanceof \cache_is_lockable),
+ 'keyawareness' => ($store instanceof \cache_is_key_aware),
+ 'searchable' => ($store instanceof \cache_is_searchable)
+ ),
+ 'warnings' => $store ? $store->get_warnings() : array()
+ );
+ if (empty($details['default'])) {
+ $return[$name] = $record;
+ } else {
+ $default[$name] = $record;
+ }
+ }
+
+ ksort($return);
+ ksort($default);
+ $return = $return + $default;
+
+ foreach ($instance->get_definition_mappings() as $mapping) {
+ if (!array_key_exists($mapping['store'], $return)) {
+ continue;
+ }
+ $return[$mapping['store']]['mappings']++;
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of information about plugins, everything a renderer needs.
+ *
+ * @return array for each store, an array containing various information about each store.
+ * See the code below for details
+ */
+ public static function get_store_plugin_summaries(): array {
+ $return = array();
+ $plugins = \core_component::get_plugin_list_with_file('cachestore', 'lib.php', true);
+ foreach ($plugins as $plugin => $path) {
+ $class = 'cachestore_'.$plugin;
+ $return[$plugin] = array(
+ 'name' => get_string('pluginname', 'cachestore_'.$plugin),
+ 'requirementsmet' => $class::are_requirements_met(),
+ 'instances' => 0,
+ 'modes' => array(
+ cache_store::MODE_APPLICATION => ($class::get_supported_modes() & cache_store::MODE_APPLICATION),
+ cache_store::MODE_SESSION => ($class::get_supported_modes() & cache_store::MODE_SESSION),
+ cache_store::MODE_REQUEST => ($class::get_supported_modes() & cache_store::MODE_REQUEST),
+ ),
+ 'supports' => array(
+ 'multipleidentifiers' => ($class::get_supported_features() & cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS),
+ 'dataguarantee' => ($class::get_supported_features() & cache_store::SUPPORTS_DATA_GUARANTEE),
+ 'nativettl' => ($class::get_supported_features() & cache_store::SUPPORTS_NATIVE_TTL),
+ 'nativelocking' => (in_array('cache_is_lockable', class_implements($class))),
+ 'keyawareness' => (array_key_exists('cache_is_key_aware', class_implements($class))),
+ ),
+ 'canaddinstance' => ($class::can_add_instance() && $class::are_requirements_met())
+ );
+ }
+
+ $instance = cache_config::instance();
+ $stores = $instance->get_all_stores();
+ foreach ($stores as $store) {
+ $plugin = $store['plugin'];
+ if (array_key_exists($plugin, $return)) {
+ $return[$plugin]['instances']++;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array about the definitions. All the information a renderer needs.
+ *
+ * @return array for each store, an array containing various information about each store.
+ * See the code below for details
+ */
+ public static function get_definition_summaries(): array {
+ $factory = cache_factory::instance();
+ $config = $factory->create_config_instance();
+ $storenames = array();
+ foreach ($config->get_all_stores() as $key => $store) {
+ if (!empty($store['default'])) {
+ $storenames[$key] = new \lang_string('store_'.$key, 'cache');
+ } else {
+ $storenames[$store['name']] = $store['name'];
+ }
+ }
+ /* @var cache_definition[] $definitions */
+ $definitions = [];
+ $return = [];
+ foreach ($config->get_definitions() as $key => $definition) {
+ $definitions[$key] = cache_definition::load($definition['component'].'/'.$definition['area'], $definition);
+ }
+ foreach ($definitions as $id => $definition) {
+ $mappings = array();
+ foreach (cache_helper::get_stores_suitable_for_definition($definition) as $store) {
+ $mappings[] = $storenames[$store->my_name()];
+ }
+ $return[$id] = array(
+ 'id' => $id,
+ 'name' => $definition->get_name(),
+ 'mode' => $definition->get_mode(),
+ 'component' => $definition->get_component(),
+ 'area' => $definition->get_area(),
+ 'mappings' => $mappings,
+ 'canuselocalstore' => $definition->can_use_localstore(),
+ 'sharingoptions' => self::get_definition_sharing_options($definition->get_sharing_options(), false),
+ 'selectedsharingoption' => self::get_definition_sharing_options($definition->get_selected_sharing_option(), true),
+ 'userinputsharingkey' => $definition->get_user_input_sharing_key()
+ );
+ }
+ return $return;
+ }
+
+ /**
+ * Get the default stores for all modes.
+ *
+ * @return array An array containing sub-arrays, one for each mode.
+ */
+ public static function get_default_mode_stores(): array {
+ global $OUTPUT;
+ $instance = cache_config::instance();
+ $adequatestores = cache_helper::get_stores_suitable_for_mode_default();
+ $icon = new \pix_icon('i/warning', new \lang_string('inadequatestoreformapping', 'cache'));
+ $storenames = array();
+ foreach ($instance->get_all_stores() as $key => $store) {
+ if (!empty($store['default'])) {
+ $storenames[$key] = new \lang_string('store_'.$key, 'cache');
+ }
+ }
+ $modemappings = array(
+ cache_store::MODE_APPLICATION => array(),
+ cache_store::MODE_SESSION => array(),
+ cache_store::MODE_REQUEST => array(),
+ );
+ foreach ($instance->get_mode_mappings() as $mapping) {
+ $mode = $mapping['mode'];
+ if (!array_key_exists($mode, $modemappings)) {
+ debugging('Unknown mode in cache store mode mappings', DEBUG_DEVELOPER);
+ continue;
+ }
+ if (array_key_exists($mapping['store'], $storenames)) {
+ $modemappings[$mode][$mapping['store']] = $storenames[$mapping['store']];
+ } else {
+ $modemappings[$mode][$mapping['store']] = $mapping['store'];
+ }
+ if (!array_key_exists($mapping['store'], $adequatestores)) {
+ $modemappings[$mode][$mapping['store']] = $modemappings[$mode][$mapping['store']].' '.$OUTPUT->render($icon);
+ }
+ }
+ return $modemappings;
+ }
+
+ /**
+ * Returns an array summarising the locks available in the system.
+ *
+ * @return array array of lock summaries.
+ */
+ public static function get_lock_summaries(): array {
+ $locks = array();
+ $instance = cache_config::instance();
+ $stores = $instance->get_all_stores();
+ foreach ($instance->get_locks() as $lock) {
+ $default = !empty($lock['default']);
+ if ($default) {
+ $name = new \lang_string($lock['name'], 'cache');
+ } else {
+ $name = $lock['name'];
+ }
+ $uses = 0;
+ foreach ($stores as $store) {
+ if (!empty($store['lock']) && $store['lock'] === $lock['name']) {
+ $uses++;
+ }
+ }
+ $lockdata = array(
+ 'name' => $name,
+ 'default' => $default,
+ 'uses' => $uses,
+ 'type' => get_string('pluginname', $lock['type'])
+ );
+ $locks[$lock['name']] = $lockdata;
+ }
+ return $locks;
+ }
+
+ /**
+ * Given a sharing option hash this function returns an array of strings that can be used to describe it.
+ *
+ * @param int $sharingoption The sharing option hash to get strings for.
+ * @param bool $isselectedoptions Set to true if the strings will be used to view the selected options.
+ * @return array An array of lang_string's.
+ */
+ public static function get_definition_sharing_options(int $sharingoption, bool $isselectedoptions = true): array {
+ $options = array();
+ $prefix = ($isselectedoptions) ? 'sharingselected' : 'sharing';
+ if ($sharingoption & cache_definition::SHARING_ALL) {
+ $options[cache_definition::SHARING_ALL] = new \lang_string($prefix.'_all', 'cache');
+ }
+ if ($sharingoption & cache_definition::SHARING_SITEID) {
+ $options[cache_definition::SHARING_SITEID] = new \lang_string($prefix.'_siteid', 'cache');
+ }
+ if ($sharingoption & cache_definition::SHARING_VERSION) {
+ $options[cache_definition::SHARING_VERSION] = new \lang_string($prefix.'_version', 'cache');
+ }
+ if ($sharingoption & cache_definition::SHARING_INPUT) {
+ $options[cache_definition::SHARING_INPUT] = new \lang_string($prefix.'_input', 'cache');
+ }
+ return $options;
+ }
+
+ /**
+ * Get an array of stores that are suitable to be used for a given definition.
+ *
+ * @param string $component
+ * @param string $area
+ * @return array Array containing 3 elements
+ * 1. An array of currently used stores
+ * 2. An array of suitable stores
+ * 3. An array of default stores
+ */
+ public static function get_definition_store_options(string $component, string $area): array {
+ $factory = cache_factory::instance();
+ $definition = $factory->create_definition($component, $area);
+ $config = cache_config::instance();
+ $currentstores = $config->get_stores_for_definition($definition);
+ $possiblestores = $config->get_stores($definition->get_mode(), $definition->get_requirements_bin());
+
+ $defaults = array();
+ foreach ($currentstores as $key => $store) {
+ if (!empty($store['default'])) {
+ $defaults[] = $key;
+ unset($currentstores[$key]);
+ }
+ }
+ foreach ($possiblestores as $key => $store) {
+ if ($store['default']) {
+ unset($possiblestores[$key]);
+ $possiblestores[$key] = $store;
+ }
+ }
+ return array($currentstores, $possiblestores, $defaults);
+ }
+
+ /**
+ * This function must be implemented to display options for store plugins.
+ *
+ * @param string $name the name of the store plugin.
+ * @param array $plugindetails array of store plugin details.
+ * @return array array of actions.
+ */
+ public function get_store_plugin_actions(string $name, array $plugindetails): array {
+ return array();
+ }
+
+ /**
+ * This function must be implemented to display options for store instances.
+ *
+ * @param string $name the store instance name.
+ * @param array $storedetails array of store instance details.
+ * @return array array of actions.
+ */
+ public function get_store_instance_actions(string $name, array $storedetails): array {
+ return array();
+ }
+
+ /**
+ * This function must be implemented to display options for definition mappings.
+ *
+ * @param context $context the context for the definition.
+ * @param array $definitionsummary the definition summary.
+ * @return array array of actions.
+ */
+ public function get_definition_actions(\context $context, array $definitionsummary): array {
+ return array();
+ }
+
+ /**
+ * This function must be implemented to get addable locks.
+ *
+ * @return array array of locks that are addable.
+ */
+ public function get_addable_lock_options(): array {
+ return array();
+ }
+
+ /**
+ * This function must be implemented to perform any page actions by a child class.
+ *
+ * @param string $action the action to perform.
+ * @param array $forminfo empty array to be set by actions.
+ * @return array array of form info.
+ */
+ public abstract function perform_cache_actions(string $action, array $forminfo): array;
+
+ /**
+ * This function must be implemented to display the cache admin page.
+ *
+ * @param core_cache_renderer $renderer the renderer used to generate the page.
+ * @return string the HTML for the page.
+ */
+ public abstract function generate_admin_page(\core_cache_renderer $renderer): string;
+}
protected $state = 0;
/**
- * Returns an instance of the cache_factor method.
+ * The current cache display helper.
+ * @var core_cache\local\administration_display_helper
+ */
+ protected static $displayhelper = null;
+
+ /**
+ * Returns an instance of the cache_factory class.
*
* @param bool $forcereload If set to true a new cache_factory instance will be created and used.
* @return cache_factory
// The cache stores have been disabled.
self::$instance->set_state(self::STATE_STORES_DISABLED);
}
+
+ } else if (!empty($CFG->alternative_cache_factory_class)) {
+ $factoryclass = $CFG->alternative_cache_factory_class;
+ self::$instance = new $factoryclass();
} else {
// We're using the regular factory.
self::$instance = new cache_factory();
$factory->reset_cache_instances();
$factory->set_state(self::STATE_STORES_DISABLED);
}
+
+ /**
+ * Returns an instance of the current display_helper.
+ *
+ * @return core_cache\administration_helper
+ */
+ public static function get_administration_display_helper() : core_cache\administration_helper {
+ if (is_null(self::$displayhelper)) {
+ self::$displayhelper = new \core_cache\local\administration_display_helper();
+ }
+ return self::$displayhelper;
+ }
}
global $CFG;
if ($stores === null) {
require_once($CFG->dirroot.'/cache/locallib.php');
- $stores = cache_administration_helper::get_store_instance_summaries();
+ $stores = core_cache\administration_helper::get_store_instance_summaries();
}
$warnings = array();
foreach ($stores as $store) {
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache display administration helper.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package core
+ * @category cache
+ * @author Peter Burnett <peterburnett@catalyst-au.net>
+ * @copyright 2020 Catalyst IT
+ * @copyright 2012 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_cache\local;
+
+defined('MOODLE_INTERNAL') || die();
+use cache_store, cache_factory, cache_config_writer, cache_helper, core_cache_renderer;
+
+/**
+ * A cache helper for administration tasks
+ *
+ * @package core
+ * @category cache
+ * @copyright 2020 Peter Burnett <peterburnett@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class administration_display_helper extends \core_cache\administration_helper {
+
+ /**
+ * Please do not call constructor directly. Use cache_factory::get_administration_display_helper() instead.
+ */
+ public function __construct() {
+ // Nothing to do here.
+ }
+
+ /**
+ * Returns all of the actions that can be performed on a definition.
+ *
+ * @param context $context the system context.
+ * @param array $definitionsummary information about this cache, from the array returned by
+ * core_cache\administration_helper::get_definition_summaries(). Currently only 'sharingoptions'
+ * element is used.
+ * @return array of actions. Each action is an action_url.
+ */
+ public function get_definition_actions(\context $context, array $definitionsummary): array {
+ global $OUTPUT;
+ if (has_capability('moodle/site:config', $context)) {
+ $actions = array();
+ // Edit mappings.
+ $actions[] = $OUTPUT->action_link(
+ new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping',
+ 'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
+ get_string('editmappings', 'cache')
+ );
+ // Edit sharing.
+ if (count($definitionsummary['sharingoptions']) > 1) {
+ $actions[] = $OUTPUT->action_link(
+ new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionsharing',
+ 'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
+ get_string('editsharing', 'cache')
+ );
+ }
+ // Purge.
+ $actions[] = $OUTPUT->action_link(
+ new \moodle_url('/cache/admin.php', array('action' => 'purgedefinition',
+ 'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
+ get_string('purge', 'cache')
+ );
+ return $actions;
+ }
+ return array();
+ }
+
+ /**
+ * Returns all of the actions that can be performed on a store.
+ *
+ * @param string $name The name of the store
+ * @param array $storedetails information about this store, from the array returned by
+ * core_cache\administration_helper::get_store_instance_summaries().
+ * @return array of actions. Each action is an action_url.
+ */
+ public function get_store_instance_actions(string $name, array $storedetails): array {
+ global $OUTPUT;
+ $actions = array();
+ if (has_capability('moodle/site:config', \context_system::instance())) {
+ $baseurl = new \moodle_url('/cache/admin.php', array('store' => $name, 'sesskey' => sesskey()));
+ if (empty($storedetails['default'])) {
+ $actions[] = $OUTPUT->action_link(
+ new \moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin'])),
+ get_string('editstore', 'cache')
+ );
+
+ $actions[] = $OUTPUT->action_link(
+ new \moodle_url($baseurl, array('action' => 'deletestore')),
+ get_string('deletestore', 'cache')
+ );
+ }
+
+ $actions[] = $OUTPUT->action_link(
+ new \moodle_url($baseurl, array('action' => 'purgestore')),
+ get_string('purge', 'cache')
+ );
+ }
+ return $actions;
+ }
+
+ /**
+ * Returns all of the actions that can be performed on a plugin.
+ *
+ * @param string $name The name of the plugin
+ * @param array $plugindetails information about this store, from the array returned by
+ * core_cache\administration_helper::get_store_plugin_summaries().
+ * @return array of actions. Each action is an action_url.
+ */
+ public function get_store_plugin_actions(string $name, array $plugindetails): array {
+ global $OUTPUT;
+ $actions = array();
+ if (has_capability('moodle/site:config', \context_system::instance())) {
+ if (!empty($plugindetails['canaddinstance'])) {
+ $url = new \moodle_url('/cache/admin.php',
+ array('action' => 'addstore', 'plugin' => $name, 'sesskey' => sesskey()));
+ $actions[] = $OUTPUT->action_link(
+ $url,
+ get_string('addinstance', 'cache')
+ );
+ }
+ }
+ return $actions;
+ }
+
+ /**
+ * Returns a form that can be used to add a store instance.
+ *
+ * @param string $plugin The plugin to add an instance of
+ * @return cachestore_addinstance_form
+ * @throws coding_exception
+ */
+ public function get_add_store_form(string $plugin): \cachestore_addinstance_form {
+ global $CFG; // Needed for includes.
+ $plugins = \core_component::get_plugin_list('cachestore');
+ if (!array_key_exists($plugin, $plugins)) {
+ throw new \coding_exception('Invalid cache plugin used when trying to create an edit form.');
+ }
+ $plugindir = $plugins[$plugin];
+ $class = 'cachestore_addinstance_form';
+ if (file_exists($plugindir.'/addinstanceform.php')) {
+ require_once($plugindir.'/addinstanceform.php');
+ if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
+ $class = 'cachestore_'.$plugin.'_addinstance_form';
+ if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
+ throw new \coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
+ }
+ }
+ }
+
+ $locks = $this->get_possible_locks_for_stores($plugindir, $plugin);
+
+ $url = new \moodle_url('/cache/admin.php', array('action' => 'addstore'));
+ return new $class($url, array('plugin' => $plugin, 'store' => null, 'locks' => $locks));
+ }
+
+ /**
+ * Returns a form that can be used to edit a store instance.
+ *
+ * @param string $plugin
+ * @param string $store
+ * @return cachestore_addinstance_form
+ * @throws coding_exception
+ */
+ public function get_edit_store_form(string $plugin, string $store): \cachestore_addinstance_form {
+ global $CFG; // Needed for includes.
+ $plugins = \core_component::get_plugin_list('cachestore');
+ if (!array_key_exists($plugin, $plugins)) {
+ throw new \coding_exception('Invalid cache plugin used when trying to create an edit form.');
+ }
+ $factory = \cache_factory::instance();
+ $config = $factory->create_config_instance();
+ $stores = $config->get_all_stores();
+ if (!array_key_exists($store, $stores)) {
+ throw new \coding_exception('Invalid store name given when trying to create an edit form.');
+ }
+ $plugindir = $plugins[$plugin];
+ $class = 'cachestore_addinstance_form';
+ if (file_exists($plugindir.'/addinstanceform.php')) {
+ require_once($plugindir.'/addinstanceform.php');
+ if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
+ $class = 'cachestore_'.$plugin.'_addinstance_form';
+ if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
+ throw new \coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
+ }
+ }
+ }
+
+ $locks = $this->get_possible_locks_for_stores($plugindir, $plugin);
+
+ $url = new \moodle_url('/cache/admin.php', array('action' => 'editstore', 'plugin' => $plugin, 'store' => $store));
+ $editform = new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
+ if (isset($stores[$store]['lock'])) {
+ $editform->set_data(array('lock' => $stores[$store]['lock']));
+ }
+ // See if the cachestore is going to want to load data for the form.
+ // If it has a customised add instance form then it is going to want to.
+ $storeclass = 'cachestore_'.$plugin;
+ $storedata = $stores[$store];
+ if (array_key_exists('configuration', $storedata) &&
+ array_key_exists('cache_is_configurable', class_implements($storeclass))) {
+ $storeclass::config_set_edit_form_data($editform, $storedata['configuration']);
+ }
+ return $editform;
+ }
+
+ /**
+ * Returns an array of suitable lock instances for use with this plugin, or false if the plugin handles locking itself.
+ *
+ * @param string $plugindir
+ * @param string $plugin
+ * @return array|false
+ */
+ protected function get_possible_locks_for_stores(string $plugindir, string $plugin) {
+ global $CFG; // Needed for includes.
+ $supportsnativelocking = false;
+ if (file_exists($plugindir.'/lib.php')) {
+ require_once($plugindir.'/lib.php');
+ $pluginclass = 'cachestore_'.$plugin;
+ if (class_exists($pluginclass)) {
+ $supportsnativelocking = array_key_exists('cache_is_lockable', class_implements($pluginclass));
+ }
+ }
+
+ if (!$supportsnativelocking) {
+ $config = \cache_config::instance();
+ $locks = array();
+ foreach ($config->get_locks() as $lock => $conf) {
+ if (!empty($conf['default'])) {
+ $name = get_string($lock, 'cache');
+ } else {
+ $name = $lock;
+ }
+ $locks[$lock] = $name;
+ }
+ } else {
+ $locks = false;
+ }
+
+ return $locks;
+ }
+
+ /**
+ * Processes the results of the add/edit instance form data for a plugin returning an array of config information suitable to
+ * store in configuration.
+ *
+ * @param stdClass $data The mform data.
+ * @return array
+ * @throws coding_exception
+ */
+ public function get_store_configuration_from_data(\stdClass $data): array {
+ global $CFG;
+ $file = $CFG->dirroot.'/cache/stores/'.$data->plugin.'/lib.php';
+ if (!file_exists($file)) {
+ throw new \coding_exception('Invalid cache plugin provided. '.$file);
+ }
+ require_once($file);
+ $class = 'cachestore_'.$data->plugin;
+ if (!class_exists($class)) {
+ throw new \coding_exception('Invalid cache plugin provided.');
+ }
+ if (array_key_exists('cache_is_configurable', class_implements($class))) {
+ return $class::config_get_configuration_array($data);
+ }
+ return array();
+ }
+
+ /**
+ * Returns an array of lock plugins for which we can add an instance.
+ *
+ * Suitable for use within an mform select element.
+ *
+ * @return array
+ */
+ public function get_addable_lock_options(): array {
+ $plugins = \core_component::get_plugin_list_with_class('cachelock', '', 'lib.php');
+ $options = array();
+ $len = strlen('cachelock_');
+ foreach ($plugins as $plugin => $class) {
+ $method = "$class::can_add_instance";
+ if (is_callable($method) && !call_user_func($method)) {
+ // Can't add an instance of this plugin.
+ continue;
+ }
+ $options[substr($plugin, $len)] = get_string('pluginname', $plugin);
+ }
+ return $options;
+ }
+
+ /**
+ * Gets the form to use when adding a lock instance.
+ *
+ * @param string $plugin
+ * @param array $lockplugin
+ * @return cache_lock_form
+ * @throws coding_exception
+ */
+ public function get_add_lock_form(string $plugin, array $lockplugin = null): \cache_lock_form {
+ global $CFG; // Needed for includes.
+ $plugins = \core_component::get_plugin_list('cachelock');
+ if (!array_key_exists($plugin, $plugins)) {
+ throw new \coding_exception('Invalid cache lock plugin requested when trying to create a form.');
+ }
+ $plugindir = $plugins[$plugin];
+ $class = 'cache_lock_form';
+ if (file_exists($plugindir.'/addinstanceform.php') && in_array('cache_is_configurable', class_implements($class))) {
+ require_once($plugindir.'/addinstanceform.php');
+ if (class_exists('cachelock_'.$plugin.'_addinstance_form')) {
+ $class = 'cachelock_'.$plugin.'_addinstance_form';
+ if (!array_key_exists('cache_lock_form', class_parents($class))) {
+ throw new \coding_exception('Cache lock plugin add instance forms must extend cache_lock_form');
+ }
+ }
+ }
+ return new $class(null, array('lock' => $plugin));
+ }
+
+ /**
+ * Gets configuration data from a new lock instance form.
+ *
+ * @param string $plugin
+ * @param stdClass $data
+ * @return array
+ * @throws coding_exception
+ */
+ public function get_lock_configuration_from_data(string $plugin, \stdClass $data): array {
+ global $CFG;
+ $file = $CFG->dirroot.'/cache/locks/'.$plugin.'/lib.php';
+ if (!file_exists($file)) {
+ throw new \coding_exception('Invalid cache plugin provided. '.$file);
+ }
+ require_once($file);
+ $class = 'cachelock_'.$plugin;
+ if (!class_exists($class)) {
+ throw new \coding_exception('Invalid cache plugin provided.');
+ }
+ if (array_key_exists('cache_is_configurable', class_implements($class))) {
+ return $class::config_get_configuration_array($data);
+ }
+ return array();
+ }
+
+ /**
+ * Handles the page actions, based on the parameter.
+ *
+ * @param string $action the action to handle.
+ * @param array $forminfo an empty array to be overridden and set.
+ * @return array the empty or overridden forminfo array.
+ */
+ public function perform_cache_actions(string $action, array $forminfo): array {
+ switch ($action) {
+ case 'rescandefinitions' : // Rescan definitions.
+ $this->action_rescan_definition();
+ break;
+
+ case 'addstore' : // Add the requested store.
+ $forminfo = $this->action_addstore();
+ break;
+
+ case 'editstore' : // Edit the requested store.
+ $forminfo = $this->action_editstore();
+ break;
+
+ case 'deletestore' : // Delete a given store.
+ $this->action_deletestore($action);
+ break;
+
+ case 'editdefinitionmapping' : // Edit definition mappings.
+ $forminfo = $this->action_editdefinitionmapping();
+ break;
+
+ case 'editdefinitionsharing' : // Edit definition sharing.
+ $forminfo = $this->action_editdefinitionsharing();
+ break;
+
+ case 'editmodemappings': // Edit default mode mappings.
+ $forminfo = $this->action_editmodemappings();
+ break;
+
+ case 'purgedefinition': // Purge a specific definition.
+ $this->action_purgedefinition();
+ break;
+
+ case 'purgestore':
+ case 'purge': // Purge a store cache.
+ $this->action_purge();
+ break;
+
+ case 'newlockinstance':
+ $forminfo = $this->action_newlockinstance();
+ break;
+
+ case 'deletelock':
+ // Deletes a lock instance.
+ $this->action_deletelock($action);
+ break;
+ }
+
+ return $forminfo;
+ }
+
+ /**
+ * Performs the rescan definition action.
+ *
+ * @return void
+ */
+ public function action_rescan_definition() {
+ global $PAGE;
+
+ \cache_config_writer::update_definitions();
+ redirect($PAGE->url);
+ }
+
+ /**
+ * Performs the add store action.
+ *
+ * @return array an array of the form to display to the user, and the page title.
+ */
+ public function action_addstore() : array {
+ global $PAGE;
+ $storepluginsummaries = $this->get_store_plugin_summaries();
+
+ $plugin = required_param('plugin', PARAM_PLUGIN);
+ if (!$storepluginsummaries[$plugin]['canaddinstance']) {
+ print_error('ex_unmetstorerequirements', 'cache');
+ }
+ $mform = $this->get_add_store_form($plugin);
+ $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
+ if ($mform->is_cancelled()) {
+ redirect($PAGE->url);
+ } else if ($data = $mform->get_data()) {
+ $config = $this->get_store_configuration_from_data($data);
+ $writer = \cache_config_writer::instance();
+ unset($config['lock']);
+ foreach ($writer->get_locks() as $lock => $lockconfig) {
+ if ($lock == $data->lock) {
+ $config['lock'] = $data->lock;
+ }
+ }
+ $writer->add_store_instance($data->name, $data->plugin, $config);
+ redirect($PAGE->url, get_string('addstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
+ }
+
+ return array('form' => $mform, 'title' => $title);
+ }
+
+ /**
+ * Performs the edit store action.
+ *
+ * @return array an array of the form to display, and the page title.
+ */
+ public function action_editstore(): array {
+ global $PAGE;
+ $storepluginsummaries = $this->get_store_plugin_summaries();
+
+ $plugin = required_param('plugin', PARAM_PLUGIN);
+ $store = required_param('store', PARAM_TEXT);
+ $mform = $this->get_edit_store_form($plugin, $store);
+ $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
+ if ($mform->is_cancelled()) {
+ redirect($PAGE->url);
+ } else if ($data = $mform->get_data()) {
+ $config = $this->get_store_configuration_from_data($data);
+ $writer = \cache_config_writer::instance();
+
+ unset($config['lock']);
+ foreach ($writer->get_locks() as $lock => $lockconfig) {
+ if ($lock == $data->lock) {
+ $config['lock'] = $data->lock;
+ }
+ }
+ $writer->edit_store_instance($data->name, $data->plugin, $config);
+ redirect($PAGE->url, get_string('editstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
+ }
+
+ return array('form' => $mform, 'title' => $title);
+ }
+
+ /**
+ * Performs the deletestore action.
+ *
+ * @param string $action the action calling to this function.
+ * @return void
+ */
+ public function action_deletestore(string $action) {
+ global $OUTPUT, $PAGE, $SITE;
+ $notifysuccess = true;
+ $storeinstancesummaries = $this->get_store_instance_summaries();
+
+ $store = required_param('store', PARAM_TEXT);
+ $confirm = optional_param('confirm', false, PARAM_BOOL);
+
+ if (!array_key_exists($store, $storeinstancesummaries)) {
+ $notifysuccess = false;
+ $notifications[] = array(get_string('invalidstore', 'cache'), false);
+ } else if ($storeinstancesummaries[$store]['mappings'] > 0) {
+ $notifysuccess = false;
+ $notifications[] = array(get_string('deletestorehasmappings', 'cache'), false);
+ }
+
+ if ($notifysuccess) {
+ if (!$confirm) {
+ $title = get_string('confirmstoredeletion', 'cache');
+ $params = array('store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
+ $url = new \moodle_url($PAGE->url, $params);
+ $button = new \single_button($url, get_string('deletestore', 'cache'));
+
+ $PAGE->set_title($title);
+ $PAGE->set_heading($SITE->fullname);
+ echo $OUTPUT->header();
+ echo $OUTPUT->heading($title);
+ $confirmation = get_string('deletestoreconfirmation', 'cache', $storeinstancesummaries[$store]['name']);
+ echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
+ echo $OUTPUT->footer();
+ exit;
+ } else {
+ $writer = \cache_config_writer::instance();
+ $writer->delete_store_instance($store);
+ redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5);
+ }
+ }
+ }
+
+ /**
+ * Performs the edit definition mapping action.
+ *
+ * @return array an array of the form to display, and the page title.
+ * @throws cache_exception
+ */
+ public function action_editdefinitionmapping(): array {
+ global $PAGE;
+ $definitionsummaries = $this->get_definition_summaries();
+
+ $definition = required_param('definition', PARAM_SAFEPATH);
+ if (!array_key_exists($definition, $definitionsummaries)) {
+ throw new \cache_exception('Invalid cache definition requested');
+ }
+ $title = get_string('editdefinitionmappings', 'cache', $definition);
+ $mform = new \cache_definition_mappings_form($PAGE->url, array('definition' => $definition));
+ if ($mform->is_cancelled()) {
+ redirect($PAGE->url);
+ } else if ($data = $mform->get_data()) {
+ $writer = \cache_config_writer::instance();
+ $mappings = array();
+ foreach ($data->mappings as $mapping) {
+ if (!empty($mapping)) {
+ $mappings[] = $mapping;
+ }
+ }
+ $writer->set_definition_mappings($definition, $mappings);
+ redirect($PAGE->url);
+ }
+
+ return array('form' => $mform, 'title' => $title);
+ }
+
+ /**
+ * Performs the edit definition sharing action.
+ *
+ * @return array an array of the edit definition sharing form, and the page title.
+ */
+ public function action_editdefinitionsharing(): array {
+ global $PAGE;
+ $definitionsummaries = $this->get_definition_summaries();
+
+ $definition = required_param('definition', PARAM_SAFEPATH);
+ if (!array_key_exists($definition, $definitionsummaries)) {
+ throw new \cache_exception('Invalid cache definition requested');
+ }
+ $title = get_string('editdefinitionsharing', 'cache', $definition);
+ $sharingoptions = $definitionsummaries[$definition]['sharingoptions'];
+ $customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
+ $mform = new \cache_definition_sharing_form($PAGE->url, $customdata);
+ $mform->set_data(array(
+ 'sharing' => $definitionsummaries[$definition]['selectedsharingoption'],
+ 'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey']
+ ));
+ if ($mform->is_cancelled()) {
+ redirect($PAGE->url);
+ } else if ($data = $mform->get_data()) {
+ $component = $definitionsummaries[$definition]['component'];
+ $area = $definitionsummaries[$definition]['area'];
+ // Purge the stores removing stale data before we alter the sharing option.
+ \cache_helper::purge_stores_used_by_definition($component, $area);
+ $writer = \cache_config_writer::instance();
+ $sharing = array_sum(array_keys($data->sharing));
+ $userinputsharingkey = $data->userinputsharingkey;
+ $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
+ redirect($PAGE->url);
+ }
+
+ return array('form' => $mform, 'title' => $title);
+ }
+
+ /**
+ * Performs the edit mode mappings action.
+ *
+ * @return array an array of the edit mode mappings form.
+ */
+ public function action_editmodemappings(): array {
+ global $PAGE;
+ $storeinstancesummaries = $this->get_store_instance_summaries();
+ $defaultmodestores = $this->get_default_mode_stores();
+
+ $mform = new \cache_mode_mappings_form(null, $storeinstancesummaries);
+ $mform->set_data(array(
+ 'mode_'.cache_store::MODE_APPLICATION => key($defaultmodestores[cache_store::MODE_APPLICATION]),
+ 'mode_'.cache_store::MODE_SESSION => key($defaultmodestores[cache_store::MODE_SESSION]),
+ 'mode_'.cache_store::MODE_REQUEST => key($defaultmodestores[cache_store::MODE_REQUEST]),
+ ));
+ if ($mform->is_cancelled()) {
+ redirect($PAGE->url);
+ } else if ($data = $mform->get_data()) {
+ $mappings = array(
+ cache_store::MODE_APPLICATION => array($data->{'mode_'.cache_store::MODE_APPLICATION}),
+ cache_store::MODE_SESSION => array($data->{'mode_'.cache_store::MODE_SESSION}),
+ cache_store::MODE_REQUEST => array($data->{'mode_'.cache_store::MODE_REQUEST}),
+ );
+ $writer = cache_config_writer::instance();
+ $writer->set_mode_mappings($mappings);
+ redirect($PAGE->url);
+ }
+
+ return array('form' => $mform);
+ }
+
+ /**
+ * Performs the purge definition action.
+ *
+ * @return void
+ */
+ public function action_purgedefinition() {
+ global $PAGE;
+
+ $id = required_param('definition', PARAM_SAFEPATH);
+ list($component, $area) = explode('/', $id, 2);
+ $factory = cache_factory::instance();
+ $definition = $factory->create_definition($component, $area);
+ if ($definition->has_required_identifiers()) {
+ // We will have to purge the stores used by this definition.
+ cache_helper::purge_stores_used_by_definition($component, $area);
+ } else {
+ // Alrighty we can purge just the data belonging to this definition.
+ cache_helper::purge_by_definition($component, $area);
+ }
+
+ $message = get_string('purgexdefinitionsuccess', 'cache', [
+ 'name' => $definition->get_name(),
+ 'component' => $component,
+ 'area' => $area,
+ ]);
+ $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
+ 'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id]),
+ get_string('purgeagain', 'cache'));
+ redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
+ }
+
+ /**
+ * Performs the purge action.
+ *
+ * @return void
+ */
+ public function action_purge() {
+ global $PAGE;
+
+ $store = required_param('store', PARAM_TEXT);
+ cache_helper::purge_store($store);
+ $message = get_string('purgexstoresuccess', 'cache', ['store' => $store]);
+ $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
+ 'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store]),
+ get_string('purgeagain', 'cache'));
+ redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
+ }
+
+ /**
+ * Performs the new lock instance action.
+ *
+ * @return array An array containing the new lock instance form.
+ */
+ public function action_newlockinstance(): array {
+ global $PAGE;
+
+ // Adds a new lock instance.
+ $lock = required_param('lock', PARAM_ALPHANUMEXT);
+ $mform = $this->get_add_lock_form($lock);
+ if ($mform->is_cancelled()) {
+ redirect($PAGE->url);
+ } else if ($data = $mform->get_data()) {
+ $factory = cache_factory::instance();
+ $config = $factory->create_config_instance(true);
+ $name = $data->name;
+ $data = $this->get_lock_configuration_from_data($lock, $data);
+ $config->add_lock_instance($name, $lock, $data);
+ redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
+ }
+
+ return array('form' => $mform);
+ }
+
+ /**
+ * Performs the delete lock action.
+ *
+ * @param string $action the action calling this function.
+ * @return void
+ */
+ public function action_deletelock(string $action) {
+ global $OUTPUT, $PAGE, $SITE;
+ $notifysuccess = true;
+ $locks = $this->get_lock_summaries();
+
+ $lock = required_param('lock', PARAM_ALPHANUMEXT);
+ $confirm = optional_param('confirm', false, PARAM_BOOL);
+ if (!array_key_exists($lock, $locks)) {
+ $notifysuccess = false;
+ $notifications[] = array(get_string('invalidlock', 'cache'), false);
+ } else if ($locks[$lock]['uses'] > 0) {
+ $notifysuccess = false;
+ $notifications[] = array(get_string('deletelockhasuses', 'cache'), false);
+ }
+ if ($notifysuccess) {
+ if (!$confirm) {
+ $title = get_string('confirmlockdeletion', 'cache');
+ $params = array('lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
+ $url = new \moodle_url($PAGE->url, $params);
+ $button = new \single_button($url, get_string('deletelock', 'cache'));
+
+ $PAGE->set_title($title);
+ $PAGE->set_heading($SITE->fullname);
+ echo $OUTPUT->header();
+ echo $OUTPUT->heading($title);
+ $confirmation = get_string('deletelockconfirmation', 'cache', $lock);
+ echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
+ echo $OUTPUT->footer();
+ exit;
+ } else {
+ $writer = cache_config_writer::instance();
+ $writer->delete_lock_instance($lock);
+ redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5);
+ }
+ }
+ }
+
+ /**
+ * Outputs the main admin page by generating it through the renderer.
+ *
+ * @param core_cache_renderer $renderer the renderer to use to generate the page.
+ * @return string the HTML for the admin page.
+ */
+ public function generate_admin_page(core_cache_renderer $renderer): string {
+ $context = \context_system::instance();
+ $html = '';
+
+ $storepluginsummaries = $this->get_store_plugin_summaries();
+ $storeinstancesummaries = $this->get_store_instance_summaries();
+ $definitionsummaries = $this->get_definition_summaries();
+ $defaultmodestores = $this->get_default_mode_stores();
+ $locks = $this->get_lock_summaries();
+
+ $html .= $renderer->store_plugin_summaries($storepluginsummaries);
+ $html .= $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries);
+ $html .= $renderer->definition_summaries($definitionsummaries, $context);
+ $html .= $renderer->lock_summaries($locks);
+ $html .= $renderer->additional_lock_actions();
+
+ $applicationstore = join(', ', $defaultmodestores[cache_store::MODE_APPLICATION]);
+ $sessionstore = join(', ', $defaultmodestores[cache_store::MODE_SESSION]);
+ $requeststore = join(', ', $defaultmodestores[cache_store::MODE_REQUEST]);
+ $editurl = new \moodle_url('/cache/admin.php', array('action' => 'editmodemappings', 'sesskey' => sesskey()));
+ $html .= $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl);
+
+ return $html;
+ }
+}
\ No newline at end of file
if (!preg_match('#^[a-zA-Z0-9\-_ ]+$#', $data['name'])) {
$errors['name'] = get_string('storenameinvalid', 'cache');
} else if (empty($this->_customdata['store'])) {
- $stores = cache_administration_helper::get_store_instance_summaries();
+ $stores = core_cache\administration_helper::get_store_instance_summaries();
if (array_key_exists($data['name'], $stores)) {
$errors['name'] = get_string('storenamealreadyused', 'cache');
}
list($component, $area) = explode('/', $definition, 2);
list($currentstores, $storeoptions, $defaults) =
- cache_administration_helper::get_definition_store_options($component, $area);
+ core_cache\administration_helper::get_definition_store_options($component, $area);
- $storedata = cache_administration_helper::get_definition_summaries();
+ $storedata = core_cache\administration_helper::get_definition_summaries();
if ($storedata[$definition]['mode'] != cache_store::MODE_REQUEST) {
if (isset($storedata[$definition]['canuselocalstore']) && $storedata[$definition]['canuselocalstore']) {
$form->addElement('html', $OUTPUT->notification(get_string('localstorenotification', 'cache'), 'notifymessage'));
public function set_data($data) {
if (!isset($data['sharing'])) {
// Set the default value here. mforms doesn't handle defaults very nicely.
- $data['sharing'] = cache_administration_helper::get_definition_sharing_options(cache_definition::SHARING_DEFAULT);
+ $data['sharing'] = core_cache\administration_helper::get_definition_sharing_options(cache_definition::SHARING_DEFAULT);
}
parent::set_data($data);
}
}
$this->config_save();
}
-
-}
-
-/**
- * A cache helper for administration tasks
- *
- * @package core
- * @category cache
- * @copyright 2012 Sam Hemelryk
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-abstract class cache_administration_helper extends cache_helper {
-
- /**
- * Returns an array containing all of the information about stores a renderer needs.
- * @return array
- */
- public static function get_store_instance_summaries() {
- $return = array();
- $default = array();
- $instance = cache_config::instance();
- $stores = $instance->get_all_stores();
- $locks = $instance->get_locks();
- foreach ($stores as $name => $details) {
- $class = $details['class'];
- $store = false;
- if ($class::are_requirements_met()) {
- $store = new $class($details['name'], $details['configuration']);
- }
- $lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock();
- $record = array(
- 'name' => $name,
- 'plugin' => $details['plugin'],
- 'default' => $details['default'],
- 'isready' => $store ? $store->is_ready() : false,
- 'requirementsmet' => $class::are_requirements_met(),
- 'mappings' => 0,
- 'lock' => $lock,
- 'modes' => array(
- cache_store::MODE_APPLICATION =>
- ($class::get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
- cache_store::MODE_SESSION =>
- ($class::get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
- cache_store::MODE_REQUEST =>
- ($class::get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
- ),
- 'supports' => array(
- 'multipleidentifiers' => $store ? $store->supports_multiple_identifiers() : false,
- 'dataguarantee' => $store ? $store->supports_data_guarantee() : false,
- 'nativettl' => $store ? $store->supports_native_ttl() : false,
- 'nativelocking' => ($store instanceof cache_is_lockable),
- 'keyawareness' => ($store instanceof cache_is_key_aware),
- 'searchable' => ($store instanceof cache_is_searchable)
- ),
- 'warnings' => $store ? $store->get_warnings() : array()
- );
- if (empty($details['default'])) {
- $return[$name] = $record;
- } else {
- $default[$name] = $record;
- }
- }
-
- ksort($return);
- ksort($default);
- $return = $return + $default;
-
- foreach ($instance->get_definition_mappings() as $mapping) {
- if (!array_key_exists($mapping['store'], $return)) {
- continue;
- }
- $return[$mapping['store']]['mappings']++;
- }
-
- return $return;
- }
-
- /**
- * Returns an array of information about plugins, everything a renderer needs.
- *
- * @return array for each store, an array containing various information about each store.
- * See the code below for details
- */
- public static function get_store_plugin_summaries() {
- $return = array();
- $plugins = core_component::get_plugin_list_with_file('cachestore', 'lib.php', true);
- foreach ($plugins as $plugin => $path) {
- $class = 'cachestore_'.$plugin;
- $return[$plugin] = array(
- 'name' => get_string('pluginname', 'cachestore_'.$plugin),
- 'requirementsmet' => $class::are_requirements_met(),
- 'instances' => 0,
- 'modes' => array(
- cache_store::MODE_APPLICATION => ($class::get_supported_modes() & cache_store::MODE_APPLICATION),
- cache_store::MODE_SESSION => ($class::get_supported_modes() & cache_store::MODE_SESSION),
- cache_store::MODE_REQUEST => ($class::get_supported_modes() & cache_store::MODE_REQUEST),
- ),
- 'supports' => array(
- 'multipleidentifiers' => ($class::get_supported_features() & cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS),
- 'dataguarantee' => ($class::get_supported_features() & cache_store::SUPPORTS_DATA_GUARANTEE),
- 'nativettl' => ($class::get_supported_features() & cache_store::SUPPORTS_NATIVE_TTL),
- 'nativelocking' => (in_array('cache_is_lockable', class_implements($class))),
- 'keyawareness' => (array_key_exists('cache_is_key_aware', class_implements($class))),
- ),
- 'canaddinstance' => ($class::can_add_instance() && $class::are_requirements_met())
- );
- }
-
- $instance = cache_config::instance();
- $stores = $instance->get_all_stores();
- foreach ($stores as $store) {
- $plugin = $store['plugin'];
- if (array_key_exists($plugin, $return)) {
- $return[$plugin]['instances']++;
- }
- }
-
- return $return;
- }
-
- /**
- * Returns an array about the definitions. All the information a renderer needs.
- *
- * @return array for each store, an array containing various information about each store.
- * See the code below for details
- */
- public static function get_definition_summaries() {
- $factory = cache_factory::instance();
- $config = $factory->create_config_instance();
- $storenames = array();
- foreach ($config->get_all_stores() as $key => $store) {
- if (!empty($store['default'])) {
- $storenames[$key] = new lang_string('store_'.$key, 'cache');
- } else {
- $storenames[$store['name']] = $store['name'];
- }
- }
- /* @var cache_definition[] $definitions */
- $definitions = array();
- foreach ($config->get_definitions() as $key => $definition) {
- $definitions[$key] = cache_definition::load($definition['component'].'/'.$definition['area'], $definition);
- }
- foreach ($definitions as $id => $definition) {
- $mappings = array();
- foreach (cache_helper::get_stores_suitable_for_definition($definition) as $store) {
- $mappings[] = $storenames[$store->my_name()];
- }
- $return[$id] = array(
- 'id' => $id,
- 'name' => $definition->get_name(),
- 'mode' => $definition->get_mode(),
- 'component' => $definition->get_component(),
- 'area' => $definition->get_area(),
- 'mappings' => $mappings,
- 'canuselocalstore' => $definition->can_use_localstore(),
- 'sharingoptions' => self::get_definition_sharing_options($definition->get_sharing_options(), false),
- 'selectedsharingoption' => self::get_definition_sharing_options($definition->get_selected_sharing_option(), true),
- 'userinputsharingkey' => $definition->get_user_input_sharing_key()
- );
- }
- return $return;
- }
-
- /**
- * Given a sharing option hash this function returns an array of strings that can be used to describe it.
- *
- * @param int $sharingoption The sharing option hash to get strings for.
- * @param bool $isselectedoptions Set to true if the strings will be used to view the selected options.
- * @return array An array of lang_string's.
- */
- public static function get_definition_sharing_options($sharingoption, $isselectedoptions = true) {
- $options = array();
- $prefix = ($isselectedoptions) ? 'sharingselected' : 'sharing';
- if ($sharingoption & cache_definition::SHARING_ALL) {
- $options[cache_definition::SHARING_ALL] = new lang_string($prefix.'_all', 'cache');
- }
- if ($sharingoption & cache_definition::SHARING_SITEID) {
- $options[cache_definition::SHARING_SITEID] = new lang_string($prefix.'_siteid', 'cache');
- }
- if ($sharingoption & cache_definition::SHARING_VERSION) {
- $options[cache_definition::SHARING_VERSION] = new lang_string($prefix.'_version', 'cache');
- }
- if ($sharingoption & cache_definition::SHARING_INPUT) {
- $options[cache_definition::SHARING_INPUT] = new lang_string($prefix.'_input', 'cache');
- }
- return $options;
- }
-
- /**
- * Returns all of the actions that can be performed on a definition.
- *
- * @param context $context the system context.
- * @param array $definitionsummary information about this cache, from the array returned by
- * cache_administration_helper::get_definition_summaries(). Currently only 'sharingoptions'
- * element is used.
- * @return array of actions. Each action is an array with two elements, 'text' and 'url'.
- */
- public static function get_definition_actions(context $context, array $definitionsummary) {
- if (has_capability('moodle/site:config', $context)) {
- $actions = array();
- // Edit mappings.
- $actions[] = array(
- 'text' => get_string('editmappings', 'cache'),
- 'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey()))
- );
- // Edit sharing.
- if (count($definitionsummary['sharingoptions']) > 1) {
- $actions[] = array(
- 'text' => get_string('editsharing', 'cache'),
- 'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionsharing', 'sesskey' => sesskey()))
- );
- }
- // Purge.
- $actions[] = array(
- 'text' => get_string('purge', 'cache'),
- 'url' => new moodle_url('/cache/admin.php', array('action' => 'purgedefinition', 'sesskey' => sesskey()))
- );
- return $actions;
- }
- return array();
- }
-
- /**
- * Returns all of the actions that can be performed on a store.
- *
- * @param string $name The name of the store
- * @param array $storedetails information about this store, from the array returned by
- * cache_administration_helper::get_store_instance_summaries().
- * @return array of actions. Each action is an array with two elements, 'text' and 'url'.
- */
- public static function get_store_instance_actions($name, array $storedetails) {
- $actions = array();
- if (has_capability('moodle/site:config', context_system::instance())) {
- $baseurl = new moodle_url('/cache/admin.php', array('store' => $name, 'sesskey' => sesskey()));
- if (empty($storedetails['default'])) {
- $actions[] = array(
- 'text' => get_string('editstore', 'cache'),
- 'url' => new moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin']))
- );
- $actions[] = array(
- 'text' => get_string('deletestore', 'cache'),
- 'url' => new moodle_url($baseurl, array('action' => 'deletestore'))
- );
- }
- $actions[] = array(
- 'text' => get_string('purge', 'cache'),
- 'url' => new moodle_url($baseurl, array('action' => 'purgestore'))
- );
- }
- return $actions;
- }
-
- /**
- * Returns all of the actions that can be performed on a plugin.
- *
- * @param string $name The name of the plugin
- * @param array $plugindetails information about this store, from the array returned by
- * cache_administration_helper::get_store_plugin_summaries().
- * @param array $plugindetails
- * @return array
- */
- public static function get_store_plugin_actions($name, array $plugindetails) {
- $actions = array();
- if (has_capability('moodle/site:config', context_system::instance())) {
- if (!empty($plugindetails['canaddinstance'])) {
- $url = new moodle_url('/cache/admin.php', array('action' => 'addstore', 'plugin' => $name, 'sesskey' => sesskey()));
- $actions[] = array(
- 'text' => get_string('addinstance', 'cache'),
- 'url' => $url
- );
- }
- }
- return $actions;
- }
-
- /**
- * Returns a form that can be used to add a store instance.
- *
- * @param string $plugin The plugin to add an instance of
- * @return cachestore_addinstance_form
- * @throws coding_exception
- */
- public static function get_add_store_form($plugin) {
- global $CFG; // Needed for includes.
- $plugins = core_component::get_plugin_list('cachestore');
- if (!array_key_exists($plugin, $plugins)) {
- throw new coding_exception('Invalid cache plugin used when trying to create an edit form.');
- }
- $plugindir = $plugins[$plugin];
- $class = 'cachestore_addinstance_form';
- if (file_exists($plugindir.'/addinstanceform.php')) {
- require_once($plugindir.'/addinstanceform.php');
- if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
- $class = 'cachestore_'.$plugin.'_addinstance_form';
- if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
- throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
- }
- }
- }
-
- $locks = self::get_possible_locks_for_stores($plugindir, $plugin);
-
- $url = new moodle_url('/cache/admin.php', array('action' => 'addstore'));
- return new $class($url, array('plugin' => $plugin, 'store' => null, 'locks' => $locks));
- }
-
- /**
- * Returns a form that can be used to edit a store instance.
- *
- * @param string $plugin
- * @param string $store
- * @return cachestore_addinstance_form
- * @throws coding_exception
- */
- public static function get_edit_store_form($plugin, $store) {
- global $CFG; // Needed for includes.
- $plugins = core_component::get_plugin_list('cachestore');
- if (!array_key_exists($plugin, $plugins)) {
- throw new coding_exception('Invalid cache plugin used when trying to create an edit form.');
- }
- $factory = cache_factory::instance();
- $config = $factory->create_config_instance();
- $stores = $config->get_all_stores();
- if (!array_key_exists($store, $stores)) {
- throw new coding_exception('Invalid store name given when trying to create an edit form.');
- }
- $plugindir = $plugins[$plugin];
- $class = 'cachestore_addinstance_form';
- if (file_exists($plugindir.'/addinstanceform.php')) {
- require_once($plugindir.'/addinstanceform.php');
- if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
- $class = 'cachestore_'.$plugin.'_addinstance_form';
- if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
- throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
- }
- }
- }
-
- $locks = self::get_possible_locks_for_stores($plugindir, $plugin);
-
- $url = new moodle_url('/cache/admin.php', array('action' => 'editstore', 'plugin' => $plugin, 'store' => $store));
- $editform = new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
- if (isset($stores[$store]['lock'])) {
- $editform->set_data(array('lock' => $stores[$store]['lock']));
- }
- // See if the cachestore is going to want to load data for the form.
- // If it has a customised add instance form then it is going to want to.
- $storeclass = 'cachestore_'.$plugin;
- $storedata = $stores[$store];
- if (array_key_exists('configuration', $storedata) && array_key_exists('cache_is_configurable', class_implements($storeclass))) {
- $storeclass::config_set_edit_form_data($editform, $storedata['configuration']);
- }
- return $editform;
- }
-
- /**
- * Returns an array of suitable lock instances for use with this plugin, or false if the plugin handles locking itself.
- *
- * @param string $plugindir
- * @param string $plugin
- * @return array|false
- */
- protected static function get_possible_locks_for_stores($plugindir, $plugin) {
- global $CFG; // Needed for includes.
- $supportsnativelocking = false;
- if (file_exists($plugindir.'/lib.php')) {
- require_once($plugindir.'/lib.php');
- $pluginclass = 'cachestore_'.$plugin;
- if (class_exists($pluginclass)) {
- $supportsnativelocking = array_key_exists('cache_is_lockable', class_implements($pluginclass));
- }
- }
-
- if (!$supportsnativelocking) {
- $config = cache_config::instance();
- $locks = array();
- foreach ($config->get_locks() as $lock => $conf) {
- if (!empty($conf['default'])) {
- $name = get_string($lock, 'cache');
- } else {
- $name = $lock;
- }
- $locks[$lock] = $name;
- }
- } else {
- $locks = false;
- }
-
- return $locks;
- }
-
- /**
- * Processes the results of the add/edit instance form data for a plugin returning an array of config information suitable to
- * store in configuration.
- *
- * @param stdClass $data The mform data.
- * @return array
- * @throws coding_exception
- */
- public static function get_store_configuration_from_data(stdClass $data) {
- global $CFG;
- $file = $CFG->dirroot.'/cache/stores/'.$data->plugin.'/lib.php';
- if (!file_exists($file)) {
- throw new coding_exception('Invalid cache plugin provided. '.$file);
- }
- require_once($file);
- $class = 'cachestore_'.$data->plugin;
- if (!class_exists($class)) {
- throw new coding_exception('Invalid cache plugin provided.');
- }
- if (array_key_exists('cache_is_configurable', class_implements($class))) {
- return $class::config_get_configuration_array($data);
- }
- return array();
- }
-
- /**
- * Get an array of stores that are suitable to be used for a given definition.
- *
- * @param string $component
- * @param string $area
- * @return array Array containing 3 elements
- * 1. An array of currently used stores
- * 2. An array of suitable stores
- * 3. An array of default stores
- */
- public static function get_definition_store_options($component, $area) {
- $factory = cache_factory::instance();
- $definition = $factory->create_definition($component, $area);
- $config = cache_config::instance();
- $currentstores = $config->get_stores_for_definition($definition);
- $possiblestores = $config->get_stores($definition->get_mode(), $definition->get_requirements_bin());
-
- $defaults = array();
- foreach ($currentstores as $key => $store) {
- if (!empty($store['default'])) {
- $defaults[] = $key;
- unset($currentstores[$key]);
- }
- }
- foreach ($possiblestores as $key => $store) {
- if ($store['default']) {
- unset($possiblestores[$key]);
- $possiblestores[$key] = $store;
- }
- }
- return array($currentstores, $possiblestores, $defaults);
- }
-
- /**
- * Get the default stores for all modes.
- *
- * @return array An array containing sub-arrays, one for each mode.
- */
- public static function get_default_mode_stores() {
- global $OUTPUT;
- $instance = cache_config::instance();
- $adequatestores = cache_helper::get_stores_suitable_for_mode_default();
- $icon = new pix_icon('i/warning', new lang_string('inadequatestoreformapping', 'cache'));
- $storenames = array();
- foreach ($instance->get_all_stores() as $key => $store) {
- if (!empty($store['default'])) {
- $storenames[$key] = new lang_string('store_'.$key, 'cache');
- }
- }
- $modemappings = array(
- cache_store::MODE_APPLICATION => array(),
- cache_store::MODE_SESSION => array(),
- cache_store::MODE_REQUEST => array(),
- );
- foreach ($instance->get_mode_mappings() as $mapping) {
- $mode = $mapping['mode'];
- if (!array_key_exists($mode, $modemappings)) {
- debugging('Unknown mode in cache store mode mappings', DEBUG_DEVELOPER);
- continue;
- }
- if (array_key_exists($mapping['store'], $storenames)) {
- $modemappings[$mode][$mapping['store']] = $storenames[$mapping['store']];
- } else {
- $modemappings[$mode][$mapping['store']] = $mapping['store'];
- }
- if (!array_key_exists($mapping['store'], $adequatestores)) {
- $modemappings[$mode][$mapping['store']] = $modemappings[$mode][$mapping['store']].' '.$OUTPUT->render($icon);
- }
- }
- return $modemappings;
- }
-
- /**
- * Returns an array summarising the locks available in the system
- */
- public static function get_lock_summaries() {
- $locks = array();
- $instance = cache_config::instance();
- $stores = $instance->get_all_stores();
- foreach ($instance->get_locks() as $lock) {
- $default = !empty($lock['default']);
- if ($default) {
- $name = new lang_string($lock['name'], 'cache');
- } else {
- $name = $lock['name'];
- }
- $uses = 0;
- foreach ($stores as $store) {
- if (!empty($store['lock']) && $store['lock'] === $lock['name']) {
- $uses++;
- }
- }
- $lockdata = array(
- 'name' => $name,
- 'default' => $default,
- 'uses' => $uses,
- 'type' => get_string('pluginname', $lock['type'])
- );
- $locks[$lock['name']] = $lockdata;
- }
- return $locks;
- }
-
- /**
- * Returns an array of lock plugins for which we can add an instance.
- *
- * Suitable for use within an mform select element.
- *
- * @return array
- */
- public static function get_addable_lock_options() {
- $plugins = core_component::get_plugin_list_with_class('cachelock', '', 'lib.php');
- $options = array();
- $len = strlen('cachelock_');
- foreach ($plugins as $plugin => $class) {
- $method = "$class::can_add_instance";
- if (is_callable($method) && !call_user_func($method)) {
- // Can't add an instance of this plugin.
- continue;
- }
- $options[substr($plugin, $len)] = get_string('pluginname', $plugin);
- }
- return $options;
- }
-
- /**
- * Gets the form to use when adding a lock instance.
- *
- * @param string $plugin
- * @param array $lockplugin
- * @return cache_lock_form
- * @throws coding_exception
- */
- public static function get_add_lock_form($plugin, array $lockplugin = null) {
- global $CFG; // Needed for includes.
- $plugins = core_component::get_plugin_list('cachelock');
- if (!array_key_exists($plugin, $plugins)) {
- throw new coding_exception('Invalid cache lock plugin requested when trying to create a form.');
- }
- $plugindir = $plugins[$plugin];
- $class = 'cache_lock_form';
- if (file_exists($plugindir.'/addinstanceform.php') && in_array('cache_is_configurable', class_implements($class))) {
- require_once($plugindir.'/addinstanceform.php');
- if (class_exists('cachelock_'.$plugin.'_addinstance_form')) {
- $class = 'cachelock_'.$plugin.'_addinstance_form';
- if (!array_key_exists('cache_lock_form', class_parents($class))) {
- throw new coding_exception('Cache lock plugin add instance forms must extend cache_lock_form');
- }
- }
- }
- return new $class(null, array('lock' => $plugin));
- }
-
- /**
- * Gets configuration data from a new lock instance form.
- *
- * @param string $plugin
- * @param stdClass $data
- * @return array
- * @throws coding_exception
- */
- public static function get_lock_configuration_from_data($plugin, $data) {
- global $CFG;
- $file = $CFG->dirroot.'/cache/locks/'.$plugin.'/lib.php';
- if (!file_exists($file)) {
- throw new coding_exception('Invalid cache plugin provided. '.$file);
- }
- require_once($file);
- $class = 'cachelock_'.$plugin;
- if (!class_exists($class)) {
- throw new coding_exception('Invalid cache plugin provided.');
- }
- if (array_key_exists('cache_is_configurable', class_implements($class))) {
- return $class::config_get_configuration_array($data);
- }
- return array();
- }
}
* Displays store summaries.
*
* @param array $storeinstancesummaries information about each store instance,
- * as returned by cache_administration_helper::get_store_instance_summaries().
+ * as returned by core_cache\administration_helper::get_store_instance_summaries().
* @param array $storepluginsummaries information about each store plugin as
- * returned by cache_administration_helper::get_store_plugin_summaries().
+ * returned by core_cache\administration_helper::get_store_plugin_summaries().
* @return string HTML
*/
public function store_instance_summariers(array $storeinstancesummaries, array $storepluginsummaries) {
$defaultstoreactions = get_string('defaultstoreactions', 'cache');
foreach ($storeinstancesummaries as $name => $storesummary) {
- $actions = cache_administration_helper::get_store_instance_actions($name, $storesummary);
+ $htmlactions = cache_factory::get_administration_display_helper()->get_store_instance_actions($name, $storesummary);
$modes = array();
foreach ($storesummary['modes'] as $mode => $enabled) {
if ($enabled) {
if (!empty($storesummary['default'])) {
$info = $this->output->pix_icon('i/info', $defaultstoreactions, '', array('class' => 'icon'));
}
- $htmlactions = array();
- foreach ($actions as $action) {
- $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
- }
$isready = $storesummary['isready'] && $storesummary['requirementsmet'];
$readycell = new html_table_cell;
* Displays plugin summaries.
*
* @param array $storepluginsummaries information about each store plugin as
- * returned by cache_administration_helper::get_store_plugin_summaries().
+ * returned by core_cache\administration_helper::get_store_plugin_summaries().
* @return string HTML
*/
public function store_plugin_summaries(array $storepluginsummaries) {
$table->data = array();
foreach ($storepluginsummaries as $name => $plugin) {
- $actions = cache_administration_helper::get_store_plugin_actions($name, $plugin);
+ $htmlactions = cache_factory::get_administration_display_helper()->get_store_plugin_actions($name, $plugin);
$modes = array();
foreach ($plugin['modes'] as $mode => $enabled) {
}
}
- $htmlactions = array();
- foreach ($actions as $action) {
- $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
- }
-
$row = new html_table_row(array(
$plugin['name'],
($plugin['requirementsmet']) ? $this->output->pix_icon('i/valid', '1') : '',
* Displays definition summaries.
*
* @param array $definitionsummaries information about each definition, as returned by
- * cache_administration_helper::get_definition_summaries().
+ * core_cache\administration_helper::get_definition_summaries().
* @param context $context the system context.
*
* @return string HTML.
$none = new lang_string('none', 'cache');
foreach ($definitionsummaries as $id => $definition) {
- $actions = cache_administration_helper::get_definition_actions($context, $definition);
- $htmlactions = array();
- foreach ($actions as $action) {
- $action['url']->param('definition', $id);
- $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
- }
+ $htmlactions = cache_factory::get_administration_display_helper()->get_definition_actions($context, $definition);
if (!empty($definition['mappings'])) {
$mapping = join(', ', $definition['mappings']);
} else {
));
}
- $url = new moodle_url('/cache/admin.php', array('action' => 'newlockinstance', 'sesskey' => sesskey()));
- $select = new single_select($url, 'lock', cache_administration_helper::get_addable_lock_options());
- $select->label = get_string('addnewlockinstance', 'cache');
-
$html = html_writer::start_tag('div', array('id' => 'core-cache-lock-summary'));
$html .= $this->output->heading(get_string('locksummary', 'cache'), 3);
$html .= html_writer::table($table);
+ $html .= html_writer::end_tag('div');
+ return $html;
+ }
+
+ /**
+ * Renders additional actions for locks, such as Add.
+ *
+ * @return string
+ */
+ public function additional_lock_actions() : string {
+ $url = new moodle_url('/cache/admin.php', array('action' => 'newlockinstance', 'sesskey' => sesskey()));
+ $select = new single_select($url, 'lock', cache_factory::get_administration_display_helper()->get_addable_lock_options());
+ $select->label = get_string('addnewlockinstance', 'cache');
+
+ $html = html_writer::start_tag('div', array('id' => 'core-cache-lock-additional-actions'));
$html .= html_writer::tag('div', $this->output->render($select), array('class' => 'new-instance'));
$html .= html_writer::end_tag('div');
return $html;
/**
- * PHPunit tests for the cache API and in particular the cache_administration_helper
+ * PHPunit tests for the cache API and in particular the core_cache\administration_helper
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
cache_store::MODE_REQUEST => array('default_request'),
)));
- $storesummaries = cache_administration_helper::get_store_instance_summaries();
+ $storesummaries = core_cache\administration_helper::get_store_instance_summaries();
$this->assertInternalType('array', $storesummaries);
$this->assertArrayHasKey('summariesstore', $storesummaries);
$summary = $storesummaries['summariesstore'];
$this->assertEquals(1, $summary['requirementsmet']);
$this->assertEquals(1, $summary['mappings']);
- $definitionsummaries = cache_administration_helper::get_definition_summaries();
+ $definitionsummaries = core_cache\administration_helper::get_definition_summaries();
$this->assertInternalType('array', $definitionsummaries);
$this->assertArrayHasKey('core/eventinvalidation', $definitionsummaries);
$summary = $definitionsummaries['core/eventinvalidation'];
$this->assertInternalType('array', $summary['mappings']);
$this->assertContains('summariesstore', $summary['mappings']);
- $pluginsummaries = cache_administration_helper::get_store_plugin_summaries();
+ $pluginsummaries = core_cache\administration_helper::get_store_plugin_summaries();
$this->assertInternalType('array', $pluginsummaries);
$this->assertArrayHasKey('file', $pluginsummaries);
$summary = $pluginsummaries['file'];
$this->assertArrayHasKey('supports', $summary);
$this->assertArrayHasKey('canaddinstance', $summary);
- $locksummaries = cache_administration_helper::get_lock_summaries();
+ $locksummaries = core_cache\administration_helper::get_lock_summaries();
$this->assertInternalType('array', $locksummaries);
$this->assertTrue(count($locksummaries) > 0);
- $mappings = cache_administration_helper::get_default_mode_stores();
+ $mappings = core_cache\administration_helper::get_default_mode_stores();
$this->assertInternalType('array', $mappings);
$this->assertCount(3, $mappings);
$this->assertArrayHasKey(cache_store::MODE_APPLICATION, $mappings);
$this->assertInternalType('array', $mappings[cache_store::MODE_APPLICATION]);
$this->assertContains('summariesstore', $mappings[cache_store::MODE_APPLICATION]);
- $potentials = cache_administration_helper::get_definition_store_options('core', 'eventinvalidation');
+ $potentials = core_cache\administration_helper::get_definition_store_options('core', 'eventinvalidation');
$this->assertInternalType('array', $potentials); // Currently used, suitable, default
$this->assertCount(3, $potentials);
$this->assertArrayHasKey('summariesstore', $potentials[0]);
* Test instantiating an add store form.
*/
public function test_get_add_store_form() {
- $form = cache_administration_helper::get_add_store_form('file');
+ $form = cache_factory::get_administration_display_helper()->get_add_store_form('file');
$this->assertInstanceOf('moodleform', $form);
try {
- $form = cache_administration_helper::get_add_store_form('somethingstupid');
+ $form = cache_factory::get_administration_display_helper()->get_add_store_form('somethingstupid');
$this->fail('You should not be able to create an add form for a store plugin that does not exist.');
} catch (moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e, 'Needs to be: ' .get_class($e)." ::: ".$e->getMessage());
* Test instantiating a form to edit a store instance.
*/
public function test_get_edit_store_form() {
+ // Always instantiate a new core display helper here.
+ $administrationhelper = new core_cache\local\administration_display_helper;
$config = cache_config_writer::instance();
$this->assertTrue($config->add_store_instance('test_get_edit_store_form', 'file'));
- $form = cache_administration_helper::get_edit_store_form('file', 'test_get_edit_store_form');
+ $form = $administrationhelper->get_edit_store_form('file', 'test_get_edit_store_form');
$this->assertInstanceOf('moodleform', $form);
try {
- $form = cache_administration_helper::get_edit_store_form('somethingstupid', 'moron');
+ $form = $administrationhelper->get_edit_store_form('somethingstupid', 'moron');
$this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
} catch (moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
- $form = cache_administration_helper::get_edit_store_form('file', 'blisters');
+ $form = $administrationhelper->get_edit_store_form('file', 'blisters');
$this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
} catch (moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
* The function extend_lock() from the lock_factory interface has been deprecated without replacement including the related
implementations.
* The function extend() from the lock class has been deprecated without replacement.
+* The cache_factory class can now be overridden by an alternative cache config class, which can
+ also now control the frontend display of the cache/admin.php page (see MDL-41492).
=== 3.9 ===
* The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external method for exporting a calendar token.
+ *
+ * @package core_calendar
+ * @since Moodle 3.10
+ * @copyright 2020 Juan Leyva <juan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\external\export;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+use context_system;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+
+/**
+ * This is the external method for exporting a calendar token.
+ *
+ * @copyright 2020 Juan Leyva <juan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class token extends external_api {
+
+ /**
+ * Returns description of method parameters.
+ *
+ * @return external_function_parameters.
+ * @since Moodle 3.10
+ */
+ public static function execute_parameters() {
+ return new external_function_parameters([]);
+ }
+
+ /**
+ * Return the auth token required for exporting a calendar.
+ *
+ * @return array The access information
+ * @throws moodle_exception
+ * @since Moodle 3.10
+ */
+ public static function execute() {
+ global $CFG, $USER;
+
+ $context = context_system::instance();
+ self::validate_context($context);
+
+ if (empty($CFG->enablecalendarexport)) {
+ throw new moodle_exception('Calendar export is disabled in this site.');
+ }
+
+ return [
+ 'token' => calendar_get_export_token($USER),
+ 'warnings' => [],
+ ];
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return external_description.
+ * @since Moodle 3.10
+ */
+ public static function execute_returns() {
+
+ return new external_single_structure(
+ [
+ 'token' => new external_value(PARAM_RAW, 'The calendar permanent access token for calendar export.'),
+ 'warnings' => new external_warnings(),
+ ]
+ );
+ }
+}
$exportform = new core_calendar_export_form(null, $formdata);
$calendarurl = '';
if ($data = $exportform->get_data()) {
- $password = $DB->get_record('user', array('id' => $USER->id), 'password');
$params = array();
$params['userid'] = $USER->id;
- $params['authtoken'] = sha1($USER->id . (isset($password->password) ? $password->password : '') . $CFG->calendar_exportsalt);
+ $params['authtoken'] = calendar_get_export_token($USER);
$params['preset_what'] = $data->events['exportevents'];
$params['preset_time'] = $data->period['timeperiod'];
}
//Check authentication token
-$authuserid = !empty($userid) && $authtoken == sha1($userid . $user->password . $CFG->calendar_exportsalt);
+$authuserid = !empty($userid) && $authtoken == calendar_get_export_token($user);
//allowing for fallback check of old url - MDL-27542
$authusername = !empty($username) && $authtoken == sha1($username . $user->password . $CFG->calendar_exportsalt);
if (!$authuserid && !$authusername) {
$allowedtime = ['weeknow', 'weeknext', 'monthnow', 'monthnext', 'recentupcoming', 'custom'];
if (!empty($generateurl)) {
- $authtoken = sha1($user->id . $user->password . $CFG->calendar_exportsalt);
+ $authtoken = calendar_get_export_token($user);
$params = array();
$params['preset_what'] = $what;
$params['preset_time'] = $time;
$event = (array) $eventobj;
// Description formatting.
$calendareventobj = new calendar_event($event);
+ $event['name'] = $calendareventobj->format_external_name();
list($event['description'], $event['format']) = $calendareventobj->format_external_text();
if ($hassystemcap) {
'events' => new external_multiple_structure( new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'event id'),
- 'name' => new external_value(PARAM_TEXT, 'event name'),
+ 'name' => new external_value(PARAM_RAW, 'event name'),
'description' => new external_value(PARAM_RAW, 'Description', VALUE_OPTIONAL, null, NULL_ALLOWED),
'format' => new external_format_value('description'),
'courseid' => new external_value(PARAM_INT, 'course id'),
'events' => new external_multiple_structure( new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'event id'),
- 'name' => new external_value(PARAM_TEXT, 'event name'),
+ 'name' => new external_value(PARAM_RAW, 'event name'),
'description' => new external_value(PARAM_RAW, 'Description', VALUE_OPTIONAL),
'format' => new external_format_value('description'),
'courseid' => new external_value(PARAM_INT, 'course id'),
}
}
+ /**
+ * Format the event name using the external API.
+ *
+ * This function should we used when text formatting is required in external functions.
+ *
+ * @return string Formatted name.
+ */
+ public function format_external_name() {
+ if ($this->editorcontext === null) {
+ // Switch on the event type to decide upon the appropriate context to use for this event.
+ $this->editorcontext = $this->get_context();
+ }
+
+ return external_format_string($this->properties->name, $this->editorcontext->id);
+ }
+
/**
* Format the text using the external API.
*
}
/**
- * Implements callback user_preferences, whitelists preferences that users are allowed to update directly
+ * Implements callback user_preferences, lists preferences that users are allowed to update directly
*
* Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()}
*
* @return array The data for template and template name.
*/
function calendar_get_footer_options($calendar) {
- global $CFG, $USER, $DB, $PAGE;
+ global $CFG, $USER, $PAGE;
// Generate hash for iCal link.
- $rawhash = $USER->id . $DB->get_field('user', 'password', ['id' => $USER->id]) . $CFG->calendar_exportsalt;
- $authtoken = sha1($rawhash);
+ $authtoken = calendar_get_export_token($USER);
$renderer = $PAGE->get_renderer('core_calendar');
$footer = new \core_calendar\external\footer_options_exporter($calendar, $USER->id, $authtoken);
}
}
}
+
+/**
+ * Get the auth token for exporting the given user calendar.
+ * @param stdClass $user The user to export the calendar for
+ *
+ * @return string The export token.
+ */
+function calendar_get_export_token(stdClass $user): string {
+ global $CFG, $DB;
+
+ return sha1($user->id . $DB->get_field('user', 'password', ['id' => $user->id]) . $CFG->calendar_exportsalt);
+}
$this->assertEquals($category2->id, $events['events'][1]['categoryid']);
}
+ /**
+ * Test get_calendar_events with mathjax in the name.
+ */
+ public function test_get_calendar_events_with_mathjax() {
+ global $USER;
+
+ $this->resetAfterTest(true);
+ set_config('calendar_adminseesall', 1);
+ $this->setAdminUser();
+
+ // Enable MathJax filter in content and headings.
+ $this->configure_filters([
+ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+ ]);
+
+ // Create a site event with mathjax in the name and description.
+ $siteevent = $this->create_calendar_event('Site Event $$(a+b)=2$$', $USER->id, 'site', 0, time(),
+ ['description' => 'Site Event Description $$(a+b)=2$$']);
+
+ // Now call the WebService.
+ $events = core_calendar_external::get_calendar_events();
+ $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+
+ // Format the original data.
+ $sitecontext = context_system::instance();
+ $siteevent->name = $siteevent->format_external_name();
+ list($siteevent->description, $siteevent->descriptionformat) = $siteevent->format_external_text();
+
+ // Check that the event data is formatted.
+ $this->assertCount(1, $events['events']);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $events['events'][0]['name']);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $events['events'][0]['description']);
+ $this->assertEquals($siteevent->name, $events['events'][0]['name']);
+ $this->assertEquals($siteevent->description, $events['events'][0]['description']);
+ }
+
/**
* Test core_calendar_external::create_calendar_events
*/
// Viewing as someone not enrolled in a course with guest access on.
$this->assertTrue(calendar_view_event_allowed($caleventguest));
}
+
+ /**
+ * Test for calendar_get_export_token for current user.
+ */
+ public function test_calendar_get_export_token_for_current_user() {
+ global $USER, $DB, $CFG;
+
+ $this->setAdminUser();
+
+ // Get my token.
+ $authtoken = calendar_get_export_token($USER);
+ $expected = sha1($USER->id . $DB->get_field('user', 'password', ['id' => $USER->id]) . $CFG->calendar_exportsalt);
+
+ $this->assertEquals($expected, $authtoken);
+ }
+
+ /**
+ * Test for calendar_get_export_token for another user.
+ */
+ public function test_calendar_get_export_token_for_another_user() {
+ global $CFG;
+
+ // Get any user token.
+ $generator = $this->getDataGenerator();
+ $user = $generator->create_user();
+
+ // Get other user token.
+ $authtoken = calendar_get_export_token($user);
+ $expected = sha1($user->id . $user->password . $CFG->calendar_exportsalt);
+
+ $this->assertEquals($expected, $authtoken);
+ }
}
\core\event\course_completed::create_from_completion($data)->trigger();
}
+ // Notify user.
+ $course = get_course($data->course);
+ $messagesubject = get_string('coursecompleted', 'completion');
+ $a = [
+ 'coursename' => get_course_display_name_for_list($course),
+ 'courselink' => (string) new moodle_url('/course/view.php', array('id' => $course->id)),
+ ];
+ $messagebody = get_string('coursecompletedmessage', 'completion', $a);
+ $messageplaintext = html_to_text($messagebody);
+
+ $eventdata = new \core\message\message();
+ $eventdata->courseid = $course->id;
+ $eventdata->component = 'moodle';
+ $eventdata->name = 'coursecompleted';
+ $eventdata->userfrom = core_user::get_noreply_user();
+ $eventdata->userto = $data->userid;
+ $eventdata->notification = 1;
+ $eventdata->subject = $messagesubject;
+ $eventdata->fullmessage = $messageplaintext;
+ $eventdata->fullmessageformat = FORMAT_HTML;
+ $eventdata->fullmessagehtml = $messagebody;
+ $eventdata->smallmessage = $messageplaintext;
+
+ if ($courseimage = \core_course\external\course_summary_exporter::get_course_image($course)) {
+ $eventdata->customdata = [
+ 'notificationpictureurl' => $courseimage,
+ ];
+ }
+ message_send($eventdata);
+
return $result;
}
//
// $CFG->forumpostcountchunksize = 5000;
//
+// Course and category sorting
+//
+// If the number of courses in a category exceeds $CFG->maxcoursesincategory (10000 by default), it may lead to duplicate
+// sort orders of courses in separated categories. For example:
+// - Category A has the sort order of 10000, and has 10000 courses. The last course will have the sort order of 20000.
+// - Category B has the sort order of 20000, and has a course with the sort order of 20001.
+// - If we add another course in category A, it will have a sort order of 20001,
+// which is the same as the course in category B
+// The duplicate will cause sorting issue and hence we need to increase $CFG->maxcoursesincategory
+// to fix the duplicate sort order
+// Please also make sure $CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer.
+//
+// $CFG->maxcoursesincategory = 10000;
+//
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================
// $CFG->showcampaigncontent = true;
//
//=========================================================================
+// 16. ALTERNATIVE CACHE CONFIG SETTINGS
+//=========================================================================
+//
+// Alternative cache config.
+// Since 3.10 it is possible to override the cache_factory class with an alternative caching factory.
+// This overridden factory can provide alternative classes for caching such as cache_config,
+// cache_config_writer and core_cache\local\administration_display_helper.
+// The autoloaded factory class name can be specified to use.
+//
+// $CFG->alternative_cache_factory_class = 'tool_alternativecache_cache_factory';
+//
+//=========================================================================
// ALL DONE! To continue installation, visit your main page with a browser
//=========================================================================
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class contentbank {
+
+ /** @var array All the context levels allowed in the content bank */
+ private const ALLOWED_CONTEXT_LEVELS = [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE];
+
/** @var array Enabled content types. */
private $enabledcontenttypes = null;
$contentclass = "\\$record->contenttype\\content";
return new $contentclass($record);
}
+
+ /**
+ * Whether the context is allowed.
+ *
+ * @param context $context Context to check.
+ * @return bool
+ */
+ public function is_context_allowed(context $context): bool {
+ return in_array($context->contextlevel, self::ALLOWED_CONTEXT_LEVELS);
+ }
}
$pluginname = required_param('plugin', PARAM_PLUGIN);
$id = optional_param('id', null, PARAM_INT);
$context = context::instance_by_id($contextid, MUST_EXIST);
+
+$cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+ print_error('contextnotallowed', 'core_contentbank');
+}
+
require_capability('moodle/contentbank:access', $context);
$returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]);
$search = optional_param('search', '', PARAM_CLEAN);
$context = context::instance_by_id($contextid, MUST_EXIST);
+$cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+ print_error('contextnotallowed', 'core_contentbank');
+}
+
require_capability('moodle/contentbank:access', $context);
$statusmsg = optional_param('statusmsg', '', PARAM_ALPHANUMEXT);
$PAGE->set_pagetype('contentbank');
// Get all contents managed by active plugins where the user has permission to render them.
-$cb = new \core_contentbank\contentbank();
$contenttypes = [];
$enabledcontenttypes = $cb->get_enabled_content_types();
foreach ($enabledcontenttypes as $contenttypename) {
$this->expectException(Exception::class);
$cb->get_content_from_id(0);
}
+
+ /**
+ * Test the behaviour of is_context_allowed().
+ *
+ * @dataProvider context_provider
+ * @param \Closure $getcontext Get the context to check.
+ * @param bool $expectedresult Expected result.
+ *
+ * @covers ::is_context_allowed
+ */
+ public function test_is_context_allowed(\Closure $getcontext, bool $expectedresult): void {
+ $this->resetAfterTest();
+
+ $cb = new contentbank();
+ $context = $getcontext();
+ $this->assertEquals($expectedresult, $cb->is_context_allowed($context));
+ }
+
+ /**
+ * Data provider for test_is_context_allowed().
+ *
+ * @return array
+ */
+ public function context_provider(): array {
+
+ return [
+ 'System context' => [
+ function (): \context {
+ return \context_system::instance();
+ },
+ true,
+ ],
+ 'User context' => [
+ function (): \context {
+ $user = $this->getDataGenerator()->create_user();
+ return \context_user::instance($user->id);
+ },
+ false,
+ ],
+ 'Course category context' => [
+ function (): \context {
+ $coursecat = $this->getDataGenerator()->create_category();
+ return \context_coursecat::instance($coursecat->id);
+ },
+ true,
+ ],
+ 'Course context' => [
+ function (): \context {
+ $course = $this->getDataGenerator()->create_course();
+ return \context_course::instance($course->id);
+ },
+ true,
+ ],
+ 'Module context' => [
+ function (): \context {
+ $course = $this->getDataGenerator()->create_course();
+ $module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
+ return \context_module::instance($module->cmid);
+ },
+ false,
+ ],
+ 'Block context' => [
+ function (): \context {
+ $course = $this->getDataGenerator()->create_course();
+ $coursecontext = context_course::instance($course->id);
+ $block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
+ return \context_block::instance($block->id);
+ },
+ false,
+ ],
+ ];
+ }
}
$contextid = optional_param('contextid', \context_system::instance()->id, PARAM_INT);
$context = context::instance_by_id($contextid, MUST_EXIST);
-require_capability('moodle/contentbank:upload', $context);
-
$cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+ print_error('contextnotallowed', 'core_contentbank');
+}
+
+require_capability('moodle/contentbank:upload', $context);
$id = optional_param('id', null, PARAM_INT);
if ($id) {
$context->update_moved($newparent);
// Now make it last in new category.
- $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES, ['id' => $this->id]);
+ $DB->set_field('course_categories', 'sortorder',
+ get_max_courses_in_category() * MAX_COURSE_CATEGORIES, ['id' => $this->id]);
if ($hidecat) {
fix_course_sortorder();
// Add any subplugins to the list of item types.
$subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
foreach ($subplugins as $subpluginname => $subplugininfo) {
- if (component_callback_exists($subpluginname, 'get_course_content_items')) {
- $itemtypes[] = $prefix . $subpluginname;
+ try {
+ if (component_callback_exists($subpluginname, 'get_course_content_items')) {
+ $itemtypes[] = $prefix . $subpluginname;
+ }
+ } catch (\moodle_exception $e) {
+ debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
}
}
$module['id'] = $cm->id;
$module['name'] = external_format_string($cm->name, $modcontext->id);
$module['instance'] = $cm->instance;
+ $module['contextid'] = $modcontext->id;
$module['modname'] = (string) $cm->modname;
$module['modplural'] = (string) $cm->modplural;
$module['modicon'] = $cm->get_icon_url()->out(false);
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'Section ID'),
- 'name' => new external_value(PARAM_TEXT, 'Section name'),
+ 'name' => new external_value(PARAM_RAW, 'Section name'),
'visible' => new external_value(PARAM_INT, 'is the section visible', VALUE_OPTIONAL),
'summary' => new external_value(PARAM_RAW, 'Section description'),
'summaryformat' => new external_format_value('summary'),
'url' => new external_value(PARAM_URL, 'activity url', VALUE_OPTIONAL),
'name' => new external_value(PARAM_RAW, 'activity module name'),
'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL),
+ 'contextid' => new external_value(PARAM_INT, 'Activity context id.', VALUE_OPTIONAL),
'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL),
'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL),
'uservisible' => new external_value(PARAM_BOOL, 'Is the module visible for the user?',
$courseinfo['customfields'][] = [
'type' => $data->get_type(),
'value' => $data->get_value(),
+ 'valueraw' => $data->get_data_controller()->get_value(),
'name' => $data->get_name(),
'shortname' => $data->get_shortname()
];
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'course id'),
- 'shortname' => new external_value(PARAM_TEXT, 'course short name'),
+ 'shortname' => new external_value(PARAM_RAW, 'course short name'),
'categoryid' => new external_value(PARAM_INT, 'category id'),
'categorysortorder' => new external_value(PARAM_INT,
'sort order into the category', VALUE_OPTIONAL),
- 'fullname' => new external_value(PARAM_TEXT, 'full name'),
- 'displayname' => new external_value(PARAM_TEXT, 'course display name'),
+ 'fullname' => new external_value(PARAM_RAW, 'full name'),
+ 'displayname' => new external_value(PARAM_RAW, 'course display name'),
'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL),
'summary' => new external_value(PARAM_RAW, 'summary'),
'summaryformat' => new external_format_value('summary'),
),
'customfields' => new external_multiple_structure(
new external_single_structure(
- ['name' => new external_value(PARAM_TEXT, 'The name of the custom field'),
+ ['name' => new external_value(PARAM_RAW, 'The name of the custom field'),
'shortname' => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'),
'type' => new external_value(PARAM_COMPONENT,
'The type of the custom field - text, checkbox...'),
+ 'valueraw' => new external_value(PARAM_RAW, 'The raw value of the custom field'),
'value' => new external_value(PARAM_RAW, 'The value of the custom field')]
), 'Custom fields and associated values', VALUE_OPTIONAL),
), 'course'
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'course id'),
- 'shortname' => new external_value(PARAM_TEXT, 'short name'),
+ 'shortname' => new external_value(PARAM_RAW, 'short name'),
)
)
);
return new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'course id'),
- 'shortname' => new external_value(PARAM_TEXT, 'short name'),
+ 'shortname' => new external_value(PARAM_RAW, 'short name'),
)
);
}
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'category id'),
- 'name' => new external_value(PARAM_TEXT, 'category name'),
+ 'name' => new external_value(PARAM_RAW, 'category name'),
'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL),
'description' => new external_value(PARAM_RAW, 'category description'),
'descriptionformat' => new external_format_value('description'),
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'new category id'),
- 'name' => new external_value(PARAM_TEXT, 'new category name'),
+ 'name' => new external_value(PARAM_RAW, 'new category name'),
)
)
);
$coursereturns['customfields'][] = [
'type' => $data->get_type(),
'value' => $data->get_value(),
+ 'valueraw' => $data->get_data_controller()->get_value(),
'name' => $data->get_name(),
'shortname' => $data->get_shortname()
];
protected static function get_course_structure($onlypublicdata = true) {
$coursestructure = array(
'id' => new external_value(PARAM_INT, 'course id'),
- 'fullname' => new external_value(PARAM_TEXT, 'course full name'),
- 'displayname' => new external_value(PARAM_TEXT, 'course display name'),
- 'shortname' => new external_value(PARAM_TEXT, 'course short name'),
+ 'fullname' => new external_value(PARAM_RAW, 'course full name'),
+ 'displayname' => new external_value(PARAM_RAW, 'course display name'),
+ 'shortname' => new external_value(PARAM_RAW, 'course short name'),
'categoryid' => new external_value(PARAM_INT, 'category id'),
- 'categoryname' => new external_value(PARAM_TEXT, 'category name'),
+ 'categoryname' => new external_value(PARAM_RAW, 'category name'),
'sortorder' => new external_value(PARAM_INT, 'Sort order in the category', VALUE_OPTIONAL),
'summary' => new external_value(PARAM_RAW, 'summary'),
'summaryformat' => new external_format_value('summary'),
'The shortname of the custom field - to be able to build the field class in the code'),
'type' => new external_value(PARAM_ALPHANUMEXT,
'The type of the custom field - text field, checkbox...'),
+ 'valueraw' => new external_value(PARAM_RAW, 'The raw value of the custom field'),
'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
)
), 'Custom fields', VALUE_OPTIONAL),
new external_single_structure(
array(
'id' => new external_value(PARAM_ALPHANUMEXT, 'Outcome id'),
- 'name' => new external_value(PARAM_TEXT, 'Outcome full name'),
+ 'name' => new external_value(PARAM_RAW, 'Outcome full name'),
'scale' => new external_value(PARAM_TEXT, 'Scale items')
)
),
$course->id = $dbcourse->id;
$course->timemodified = time();
$course->category = $category->id;
- $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++;
+ $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++;
if ($category->visible == 0) {
// Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
// to previous state if somebody unhides the category.
array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
));
}
- if ($dbcourse->id == 4) {
- $this->assertEquals($course['customfields'], [array_merge($customfield, $customfieldvalue)]);
+
+ // Assert custom field that we previously added to test course 4.
+ if ($dbcourse->id == $course4->id) {
+ $this->assertEquals([
+ 'shortname' => $customfield['shortname'],
+ 'name' => $customfield['name'],
+ 'type' => $customfield['type'],
+ 'value' => $customfieldvalue['value'],
+ 'valueraw' => $customfieldvalue['value'],
+ ], $course['customfields'][0]);
}
}
$this->assertEquals($DB->count_records('course'), count($courses));
}
+ /**
+ * Test retrieving courses returns custom field data
+ */
+ public function test_get_courses_customfields(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
+ $datefield = $this->getDataGenerator()->create_custom_field([
+ 'categoryid' => $fieldcategory->get('id'),
+ 'shortname' => 'mydate',
+ 'name' => 'My date',
+ 'type' => 'date',
+ ]);
+
+ $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
+ [
+ 'shortname' => $datefield->get('shortname'),
+ 'value' => 1580389200, // 30/01/2020 13:00 GMT.
+ ],
+ ]]);
+
+ $courses = external_api::clean_returnvalue(
+ core_course_external::get_courses_returns(),
+ core_course_external::get_courses(['ids' => [$newcourse->id]])
+ );
+
+ $this->assertCount(1, $courses);
+ $course = reset($courses);
+
+ $this->assertArrayHasKey('customfields', $course);
+ $this->assertCount(1, $course['customfields']);
+
+ // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
+ $this->assertEquals([
+ 'name' => $datefield->get('name'),
+ 'shortname' => $datefield->get('shortname'),
+ 'type' => $datefield->get('type'),
+ 'value' => userdate(1580389200),
+ 'valueraw' => 1580389200,
+ ], reset($course['customfields']));
+ }
+
/**
* Test get_courses without capability
*/
$results = core_course_external::search_courses('blocklist', $blockid);
}
+ /**
+ * Test searching for courses returns custom field data
+ */
+ public function test_search_courses_customfields(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
+ $datefield = $this->getDataGenerator()->create_custom_field([
+ 'categoryid' => $fieldcategory->get('id'),
+ 'shortname' => 'mydate',
+ 'name' => 'My date',
+ 'type' => 'date',
+ ]);
+
+ $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
+ [
+ 'shortname' => $datefield->get('shortname'),
+ 'value' => 1580389200, // 30/01/2020 13:00 GMT.
+ ],
+ ]]);
+
+ $result = external_api::clean_returnvalue(
+ core_course_external::search_courses_returns(),
+ core_course_external::search_courses('search', $newcourse->shortname)
+ );
+
+ $this->assertCount(1, $result['courses']);
+ $course = reset($result['courses']);
+
+ $this->assertArrayHasKey('customfields', $course);
+ $this->assertCount(1, $course['customfields']);
+
+ // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
+ $this->assertEquals([
+ 'name' => $datefield->get('name'),
+ 'shortname' => $datefield->get('shortname'),
+ 'type' => $datefield->get('type'),
+ 'value' => userdate(1580389200),
+ 'valueraw' => 1580389200,
+ ], reset($course['customfields']));
+ }
+
/**
* Create a course with contents
* @return array A list with the course object and course modules objects
array('noclean' => true, 'para' => false, 'filter' => false));
$this->assertEquals($formattedtext, $module['description']);
$this->assertEquals($forumcm->instance, $module['instance']);
+ $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
$this->assertContains('1 unread post', $module['afterlink']);
$this->assertFalse($module['noviewlink']);
$this->assertNotEmpty($module['description']); // Module showdescription is on.
array('noclean' => true, 'para' => false, 'filter' => false));
$this->assertEquals($formattedtext, $module['description']);
$this->assertEquals($labelcm->instance, $module['instance']);
+ $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
$this->assertTrue($module['noviewlink']);
$this->assertNotEmpty($module['description']); // Label always prints the description.
$testexecuted = $testexecuted + 1;
$this->assertCount(1, $result['courses']);
$this->assertEquals($course2->id, $result['courses'][0]['id']);
// Check custom fields properly returned.
- unset($customfield['categoryid']);
- $this->assertEquals([array_merge($customfield, $customfieldvalue)], $result['courses'][0]['customfields']);
+ $this->assertEquals([
+ 'shortname' => $customfield['shortname'],
+ 'name' => $customfield['name'],
+ 'type' => $customfield['type'],
+ 'value' => $customfieldvalue['value'],
+ 'valueraw' => $customfieldvalue['value'],
+ ], $result['courses'][0]['customfields'][0]);
$result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(0, $result['courses']);
}
+ /**
+ * Test retrieving courses by field returns custom field data
+ */
+ public function test_get_courses_by_field_customfields(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
+ $datefield = $this->getDataGenerator()->create_custom_field([
+ 'categoryid' => $fieldcategory->get('id'),
+ 'shortname' => 'mydate',
+ 'name' => 'My date',
+ 'type' => 'date',
+ ]);
+
+ $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
+ [
+ 'shortname' => $datefield->get('shortname'),
+ 'value' => 1580389200, // 30/01/2020 13:00 GMT.
+ ],
+ ]]);
+
+ $result = external_api::clean_returnvalue(
+ core_course_external::get_courses_by_field_returns(),
+ core_course_external::get_courses_by_field('id', $newcourse->id)
+ );
+
+ $this->assertCount(1, $result['courses']);
+ $course = reset($result['courses']);
+
+ $this->assertArrayHasKey('customfields', $course);
+ $this->assertCount(1, $course['customfields']);
+
+ // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
+ $this->assertEquals([
+ 'name' => $datefield->get('name'),
+ 'shortname' => $datefield->get('shortname'),
+ 'type' => $datefield->get('type'),
+ 'value' => userdate(1580389200),
+ 'valueraw' => 1580389200,
+ ], reset($course['customfields']));
+ }
+
public function test_get_courses_by_field_invalid_field() {
$this->expectException('invalid_parameter_exception');
$result = core_course_external::get_courses_by_field('zyx', 'x');
=== 3.10 ===
* The function make_categories_options() has now been deprecated. Please use \core_course_category::make_categories_list() instead.
+* External function core_course_external::get_course_contents now returns a new field contextid with the module context id.
+* The core_course_external class methods get_courses(), get_courses_by_field() and search_courses() now return a "valueraw" property
+ for each custom course field, which contains the original/unformatted version of the custom field value.
=== 3.9 ===
'id' => new external_value(PARAM_INT, 'id of course'),
'shortname' => new external_value(PARAM_RAW, 'short name of course'),
'fullname' => new external_value(PARAM_RAW, 'long name of course'),
- 'displayname' => new external_value(PARAM_TEXT, 'course display name for lists.', VALUE_OPTIONAL),
+ 'displayname' => new external_value(PARAM_RAW, 'course display name for lists.', VALUE_OPTIONAL),
'enrolledusercount' => new external_value(PARAM_INT, 'Number of enrolled users in this course',
VALUE_OPTIONAL),
'idnumber' => new external_value(PARAM_RAW, 'id number of course'),
$this->assertEquals(0, $enrolledincourses[0]['lastaccess']); // I can't see this, hidden by global setting.
}
+ /**
+ * Test get_users_courses with mathjax in the name.
+ */
+ public function test_get_users_courses_with_mathjax() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ // Enable MathJax filter in content and headings.
+ $this->configure_filters([
+ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+ ]);
+
+ // Create a course with MathJax in the name and summary.
+ $coursedata = [
+ 'fullname' => 'Course 1 $$(a+b)=2$$',
+ 'shortname' => 'Course 1 $$(a+b)=2$$',
+ 'summary' => 'Lightwork Course 1 description $$(a+b)=2$$',
+ 'summaryformat' => FORMAT_HTML,
+ ];
+
+ $course = self::getDataGenerator()->create_course($coursedata);
+ $context = context_course::instance($course->id);
+
+ // Enrol a student in the course.
+ $student = $this->getDataGenerator()->create_user();
+ $studentroleid = $DB->get_field('role', 'id', ['shortname' => 'student']);
+ $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentroleid);
+
+ $this->setUser($student);
+
+ // Call the external function.
+ $enrolledincourses = core_enrol_external::get_users_courses($student->id, true);
+
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $enrolledincourses = external_api::clean_returnvalue(core_enrol_external::get_users_courses_returns(), $enrolledincourses);
+
+ // Check that the amount of courses is the right one.
+ $this->assertCount(1, $enrolledincourses);
+
+ // Filter the values to compare them with the returned ones.
+ $course->fullname = external_format_string($course->fullname, $context->id);
+ $course->shortname = external_format_string($course->shortname, $context->id);
+ list($course->summary, $course->summaryformat) =
+ external_format_text($course->summary, $course->summaryformat, $context->id, 'course', 'summary', 0);
+
+ // Compare the values.
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $enrolledincourses[0]['fullname']);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $enrolledincourses[0]['shortname']);
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $enrolledincourses[0]['summary']);
+ $this->assertEquals($course->fullname, $enrolledincourses[0]['fullname']);
+ $this->assertEquals($course->shortname, $enrolledincourses[0]['shortname']);
+ $this->assertEquals($course->summary, $enrolledincourses[0]['summary']);
+ }
+
/**
* Test get_course_enrolment_methods
*/
$texexp = preg_replace('/\\\int\\\left\((.+?),(.+?),(.+?)\\\right\)/s','\int_'. "{\$2}^{\$3}\$1 ",$texexp);
$texexp = preg_replace('/\\\int\\\left\((.+?d[a-z])\\\right\)/s','\int '. "\$1 ",$texexp);
$texexp = preg_replace('/\\\lim\\\left\((.+?),(.+?),(.+?)\\\right\)/s','\lim_'. "{\$2\\to \$3}\$1 ",$texexp);
- $texexp = str_replace('\mbox', '', $texexp); // now blacklisted in tex, sorry
+ // Remove a forbidden keyword.
+ $texexp = str_replace('\mbox', '', $texexp);
$texcache = new stdClass();
$texcache->filter = 'algebra';
$texcache->version = 1;
print_error('mimetexisnotexist', 'error');
}
-function filter_tex_sanitize_formula($texexp) {
- /// Check $texexp against blacklist (whitelisting could be more complete but also harder to maintain)
- $tex_blacklist = array(
- 'include','command','loop','repeat','open','toks','output',
- 'input','catcode','name','^^',
- '\def','\edef','\gdef','\xdef',
- '\every','\errhelp','\errorstopmode','\scrollmode','\nonstopmode',
- '\batchmode','\read','\write','csname','\newhelp','\uppercase',
- '\lowercase','\relax','\aftergroup',
- '\afterassignment','\expandafter','\noexpand','\special',
- '\let', '\futurelet','\else','\fi','\chardef','\makeatletter','\afterground',
- '\noexpand','\line','\mathcode','\item','\section','\mbox','\declarerobustcommand'
- );
-
- return str_ireplace($tex_blacklist, 'forbiddenkeyword', $texexp);
+/**
+ * Check the formula expression against the list of denied keywords.
+ *
+ * List of allowed could be more complete but also harder to maintain.
+ *
+ * @param string $texexp Formula expression to check.
+ * @return string Formula expression with denied keywords replaced with 'forbiddenkeyword'.
+ */
+function filter_tex_sanitize_formula(string $texexp): string {
+
+ $denylist = [
+ 'include', 'command', 'loop', 'repeat', 'open', 'toks', 'output',
+ 'input', 'catcode', 'name', '^^',
+ '\def', '\edef', '\gdef', '\xdef',
+ '\every', '\errhelp', '\errorstopmode', '\scrollmode', '\nonstopmode',
+ '\batchmode', '\read', '\write', 'csname', '\newhelp', '\uppercase',
+ '\lowercase', '\relax', '\aftergroup',
+ '\afterassignment', '\expandafter', '\noexpand', '\special',
+ '\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
+ '\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
+ ];
+
+ return str_ireplace($denylist, 'forbiddenkeyword', $texexp);
}
function filter_tex_get_cmd($pathname, $texexp) {
'requires' => array('base', 'dom', 'event', 'event-simulate', 'io-base')
);
+ $PAGE->requires->string_for_js('overridenoneconfirm', 'gradereport_singleview');
$PAGE->requires->js_init_call('M.gradereport_singleview.init', array(), false, $module);
}
M.gradereport_singleview = {};
M.gradereport_singleview.init = function(Y) {
+ if (this.initialised) {
+ return;
+ }
+ this.initialised = true;
+
var getColumnIndex = function(cell) {
var rowNode = cell.ancestor('tr');
if (!rowNode || !cell) {
link.on('click', function(e) {
e.preventDefault();
- Y.all('input[name^=' + type + ']').each(toggle(link.hasClass('all')));
+ var selectall = link.hasClass('all');
+ var self = this;
+ if ((type === 'override') && !selectall) {
+ Y.use('moodle-core-notification-confirm', function() {
+ var confirm = new M.core.confirm({
+ title: M.util.get_string('confirm', 'moodle'),
+ question: M.util.get_string('overridenoneconfirm', 'gradereport_singleview'),
+ });
+ confirm.on('complete-yes', function() {
+ confirm.hide();
+ confirm.destroy();
+ Y.all('input[name^=' + type + ']').each(toggle(link.hasClass('all')));
+ }, self);
+ confirm.show();
+ });
+ } else {
+ Y.all('input[name^=' + type + ']').each(toggle(link.hasClass('all')));
+ }
});
});
$string['overrideall'] = 'Override all grades';
$string['overridefor'] = 'Override for {$a}';
$string['overridenone'] = 'Do not override any grades';
+$string['overridenoneconfirm'] = 'You are trying to disable all grade overrides. After saving, all the previously overridden grades will be lost. Do you want to continue?';
$string['pluginname'] = 'Single view';
$string['privacy:metadata'] = 'The Grade single view report only shows data stored in other locations.';
$string['savegrades'] = 'Saving grades';
$factory = new factory();
$editor = $factory->get_editor();
+// Set context to default system context.
+$PAGE->set_context(null);
+
switch ($action) {
// Load list of libraries or details for library.
case 'libraries':
* minorVersion as properties.
*/
public function alterLibraryFiles(&$files, $libraries): void {
- // This is to be implemented when the renderer is used.
+ global $PAGE;
+
+ // Refactor dependency list.
+ $librarylist = [];
+ foreach ($libraries as $dependency) {
+ $librarylist[$dependency['machineName']] = [
+ 'majorVersion' => $dependency['majorVersion'],
+ 'minorVersion' => $dependency['minorVersion']
+ ];
+ }
+
+ $renderer = $PAGE->get_renderer('core_h5p');
+
+ $embedtype = 'editor';
+ $renderer->h5p_alter_scripts($files['scripts'], $librarylist, $embedtype);
+ $renderer->h5p_alter_styles($files['styles'], $librarylist, $embedtype);
}
/**
* @param int $minorversion The library's minor version
*/
public function alterLibrarySemantics(&$semantics, $name, $majorversion, $minorversion) {
- global $DB;
+ global $PAGE;
- $library = $DB->get_record('h5p_libraries',
- array(
- 'machinename' => $name,
- 'majorversion' => $majorversion,
- 'minorversion' => $minorversion,
- )
- );
-
- if ($library) {
- $library->semantics = json_encode($semantics);
- $DB->update_record('h5p_libraries', $library);
- }
+ $renderer = $PAGE->get_renderer('core_h5p');
+ $renderer->h5p_alter_semantics($semantics, $name, $majorversion, $minorversion);
}
/**
*/
class renderer extends plugin_renderer_base {
-}
\ No newline at end of file
+ /**
+ * Alter which stylesheets are loaded for H5P.
+ * This is useful for adding custom styles or replacing existing ones.
+ *
+ * @param array|object $scripts List of stylesheets that will be loaded
+ * @param array $libraries Array of libraries indexed by the library's machineName
+ * @param string $embedtype Possible values: div, iframe, external, editor
+ */
+ public function h5p_alter_styles(&$scripts, array $libraries, string $embedtype) {
+ }
+
+ /**
+ * Alter which scripts are loaded for H5P.
+ * This is useful for adding custom scripts or replacing existing ones.
+ *
+ * @param array|object $scripts List of JavaScripts that will be loaded
+ * @param array $libraries Array of libraries indexed by the library's machineName
+ * @param string $embedtype Possible values: div, iframe, external, editor
+ */
+ public function h5p_alter_scripts(&$scripts, array $libraries, string $embedtype) {
+ }
+
+ /**
+ * Alter semantics before they are processed. This is useful for changing
+ * how the editor looks and how content parameters are filtered.
+ *
+ * @param object|object $semantics Semantics as object
+ * @param string $name Machine name of library
+ * @param int $majorversion Major version of library
+ * @param int $minorversion Minor version of library
+ */
+ public function h5p_alter_semantics(&$semantics, $name, $majorversion, $minorversion) {
+ }
+
+ /**
+ * Alter parameters of H5P content after it has been filtered through semantics.
+ * This is useful for adapting the content to the current context.
+ *
+ * @param array|object $parameters The content parameters for the library
+ * @param string $name The machine readable name of the library
+ * @param int $majorversion Major version of the library
+ * @param int $minorversion Minor version of the library
+ */
+ public function h5p_alter_filtered_parameters(&$parameters, string $name, int $majorversion, int $minorversion) {
+ }
+}
$cid = $this->get_cid();
// The filterParameters function should be called before getting the dependencyfiles because it rebuild content
// dependency cache and export file.
- $settings['contents'][$cid]['jsonContent'] = $this->core->filterParameters($this->content);
+ $settings['contents'][$cid]['jsonContent'] = $this->get_filtered_parameters();
$files = $this->get_dependency_files();
if ($this->embedtype === 'div') {
return $settings;
}
+ /**
+ * Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function.
+ *
+ * @return string Filtered parameters.
+ */
+ private function get_filtered_parameters(): string {
+ global $PAGE;
+
+ $safeparams = $this->core->filterParameters($this->content);
+ $decodedparams = json_decode($safeparams);
+ $h5poutput = $PAGE->get_renderer('core_h5p');
+ $h5poutput->h5p_alter_filtered_parameters(
+ $decodedparams,
+ $this->content['library']['name'],
+ $this->content['library']['majorVersion'],
+ $this->content['library']['minorVersion']
+ );
+ $safeparams = json_encode($decodedparams);
+
+ return $safeparams;
+ }
+
/**
* Finds library dependencies of view
*
* @return array Files that the view has dependencies to
*/
private function get_dependency_files(): array {
+ global $PAGE;
+
$preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded');
$files = $this->core->getDependenciesFiles($preloadeddeps);
+ // Add additional asset files if required.
+ $h5poutput = $PAGE->get_renderer('core_h5p');
+ $h5poutput->h5p_alter_scripts($files['scripts'], $preloadeddeps, $this->embedtype);
+ $h5poutput->h5p_alter_styles($files['styles'], $preloadeddeps, $this->embedtype);
+
return $files;
}
// Get the semantics of 'Library1' from the DB.
$currentsemantics = $DB->get_field('h5p_libraries', 'semantics', array('id' => $library1->id));
- // The semantics for Library1 should be successfully updated.
- $this->assertEquals(json_encode($updatedsemantics), $currentsemantics);
+ // The semantics for Library1 shouldn't be updated.
+ $this->assertEquals($semantics, $currentsemantics);
}
/**
<p> المسؤول عن الموقع يجب أن يتحقق من إعدادات قاعدة بيانات. </p>';
$string['cannotcreatelangdir'] = 'لا يمكن إنشاء مجلد اللغة';
$string['cannotcreatetempdir'] = 'لا يمكن إنشاء المجلد المؤقت';
-$string['cannotdownloadcomponents'] = 'لم يتم تحميل العناصر';
+$string['cannotdownloadcomponents'] = 'تعذر تنزيل المُكونات';
$string['cannotdownloadzipfile'] = 'لم يتم تحميل الملف المضغوط';
$string['cannotfindcomponent'] = 'لم يتم العثور على المكون';
$string['cannotsavemd5file'] = 'لم يتم حفظ ملف md5';
$string['moodlelogo'] = 'شعار مودل';
$string['next'] = 'التالي';
$string['previous'] = 'السابق';
-$string['reload'] = 'إعادة تحميل';
+$string['reload'] = 'إعادة اÙ\84تØÙ\85Ù\8aÙ\84';
$string['disableuserimages'] = 'Disable user profile images';
$string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
$string['displayloginfailures'] = 'Display login failures';
+$string['divertallemails'] = 'Email diverting';
+$string['divertallemailsdetail'] = 'Used as a safeguard in development environments when testing emails and should not be used in production.';
+$string['divertallemailsexcept'] = 'Email diversion exceptions';
+$string['divertallemailsexcept_desc'] = 'A list of email exception rules separated by either commas or new lines. Each rule is interpreted as a regular expression, eg<pre>simone@acme.com
+.*@acme.com
+fred(\\+.*)?@acme.com
+</pre>';
+$string['divertallemailsto'] = 'Divert all emails';
+$string['divertallemailsto_desc'] = 'If set then all emails will be diverted to this single email address instead.';
$string['dndallowtextandlinks'] = 'Drag and drop upload of text/links';
$string['doclang'] = 'Language for docs';
$string['docroot'] = 'Moodle Docs document root';
$string['coursealreadycompleted'] = 'You have already completed this course';
$string['coursecomplete'] = 'Course complete';
$string['coursecompleted'] = 'Course completed';
+$string['coursecompletedmessage'] = '<p>Congratulations!</p><p>You just completed the following course: <a href="{$a->courselink}">{$a->coursename}</a>.</p>';
$string['coursecompletion'] = 'Course completion';
$string['coursecompletioncondition'] = 'Condition: {$a}';
$string['coursegrade'] = 'Course grade';
$string['contentsmoved'] = 'Content bank contents moved to {$a}.';
$string['contenttypenoaccess'] = 'You cannot view this {$a} instance.';
$string['contenttypenoedit'] = 'You can not edit this content';
+$string['contextnotallowed'] = 'Context is not allowed';
$string['emptynamenotallowed'] = 'Empty name is not allowed';
$string['eventcontentcreated'] = 'Content created';
$string['eventcontentdeleted'] = 'Content deleted';
managelicenses,core_admin
userfilterplaceholder,core
sitebackpackverify,core_badges
+filetypesnotwhitelisted,core_form
$string['err_rangelength'] = 'You must enter between {$a->format[0]} and {$a->format[1]} characters here.';
$string['err_required'] = 'You must supply a value here.';
$string['err_wrappingwhitespace'] = 'The value must not start or end with whitespace.';
-$string['err_wrongfileextension'] = 'Some files ({$a->wrongfiles}) cannot be uploaded. Only file types {$a->whitelist} are allowed.';
+$string['err_wrongfileextension'] = 'Some files ({$a->wrongfiles}) cannot be uploaded. Only file types {$a->allowlist} are allowed.';
$string['filesofthesetypes'] = 'Accepted file types:';
$string['filetypesany'] = 'All file types';
$string['filetypesnotall'] = 'It is not allowed to select \'All file types\' here';
-$string['filetypesnotwhitelisted'] = 'These file types are not allowed here: {$a}';
+$string['filetypesnotallowed'] = 'These file types are not allowed here: {$a}';
$string['filetypesothers'] = 'Other files';
$string['filetypesunknown'] = 'Unknown file types: {$a}';
$string['general'] = 'General';
$string['timing'] = 'Timing';
$string['unmaskpassword'] = 'Unmask';
$string['year'] = 'Year';
+
+// Deprecated since Moodle 3.10.
+$string['filetypesnotwhitelisted'] = 'These file types are not allowed here: {$a}';
$string['messageprovider:badgerecipientnotice'] = 'Badge recipient notifications';
$string['messageprovider:competencyplancomment'] = 'Comment posted on a learning plan';
$string['messageprovider:competencyusercompcomment'] = 'Comment posted on a competency';
+$string['messageprovider:coursecompleted'] = 'Course completed';
$string['messageprovider:courserequestapproved'] = 'Course creation request approval notification';
$string['messageprovider:courserequested'] = 'Course creation request notification';
$string['messageprovider:courserequestrejected'] = 'Course creation request rejection notification';
$string['cannotdownloaddir'] = 'Cannot download this folder';
$string['cannotinitplugin'] = 'Call plugin_init failed';
$string['cannotunzipcontentunreadable'] = 'Cannot unzip this file because the contents of the file cannot be read.';
+$string['cannotunzipextractfileerror'] = 'Cannot unzip this file because one or more of it\'s files cannot be read.';
$string['cannotunzipquotaexceeded'] = 'Cannot unzip this file because the maximum size allowed in this draft area will be exceeded.';
$string['cleancache'] = 'Clean my cache files';
$string['close'] = 'Close';
*/
function db_should_replace($table, $column = ''): bool {
- // TODO: this is horrible hack, we should do whitelisting and each plugin should be responsible for proper replacing...
+ // TODO: this is horrible hack, we should have a hook and each plugin should be responsible for proper replacing...
$skiptables = ['config', 'config_plugins', 'filter_config', 'sessions',
'events_queue', 'repository_instance_config', 'block_instances', 'files'];
// No need to call parent's validation here as we are PARAM_RAW.
- if ($this->util->is_whitelisted($data, $this->onlytypes)) {
+ if ($this->util->is_listed($data, $this->onlytypes)) {
return true;
} else {
- $troublemakers = $this->util->get_not_whitelisted($data, $this->onlytypes);
- return get_string('filetypesnotwhitelisted', 'core_form', implode(' ', $troublemakers));
+ $troublemakers = $this->util->get_not_listed($data, $this->onlytypes);
+ return get_string('filetypesnotallowed', 'core_form', implode(' ', $troublemakers));
}
}
/** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */
var isLoadingTemplates = false;
- /** @var {Array} blacklistedNestedHelpers - List of helpers that can't be called within other helpers */
- var blacklistedNestedHelpers = ['js'];
+ /** @var {Array} disallowedNestedHelpers - List of helpers that can't be called within other helpers */
+ var disallowedNestedHelpers = ['js'];
/**
* Search the various caches for a template promise for the given search key.
* template.
*
* This will parse the provided text before giving it to the helper function
- * in order to remove any blacklisted nested helpers to prevent one helper
+ * in order to remove any disallowed nested helpers to prevent one helper
* from calling another.
*
* In particular to prevent the JS helper from being called from within another
Renderer.prototype.addHelperFunction = function(helperFunction, context) {
return function() {
return function(sectionText, helper) {
- // Override the blacklisted helpers in the template context with
+ // Override the disallowed helpers in the template context with
// a function that returns an empty string for use when executing
// other helpers. This is to prevent these helpers from being
// executed as part of the rendering of another helper in order to
// prevent any potential security issues.
- var originalHelpers = blacklistedNestedHelpers.reduce(function(carry, name) {
+ var originalHelpers = disallowedNestedHelpers.reduce(function(carry, name) {
if (context.hasOwnProperty(name)) {
carry[name] = context[name];
}
return carry;
}, {});
- blacklistedNestedHelpers.forEach(function(helperName) {
+ disallowedNestedHelpers.forEach(function(helperName) {
context[helperName] = function() {
return '';
};
});
// Execute the helper with the modified context that doesn't include
- // the blacklisted nested helpers. This prevents the blacklisted
+ // the disallowed nested helpers. This prevents the disallowed
// helpers from being called from within other helpers.
var result = helperFunction.apply(this, [context, sectionText, helper]);
/**
* List all core subsystems and their location
*
- * This is a whitelist of components that are part of the core and their
+ * This is a list of components that are part of the core and their
* language strings are defined in /lang/en/<<subsystem>>.php. If a given
* plugin is not listed here and it does not have proper plugintype prefix,
* then it is considered as course activity module.
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Contains a class providing functions used to check the host/port black/whitelists for curl.
+ * Contains a class providing functions used to check the allowed/blocked host/ports for curl.
*
* @package core
* @copyright 2016 Jake Dallimore
* Host and port checking for curl.
*
* This class provides a means to check URL/host/port against the system-level cURL security entries.
- * It does not provide a means to add URLs, hosts or ports to the black/white lists; this is configured manually
+ * It does not provide a means to add URLs, hosts or ports to the allowed/blocked lists; this is configured manually
* via the site admin section of Moodle (See: 'Site admin' > 'Security' > 'HTTP Security').
*
* This class is currently used by the 'curl' wrapper class in lib/filelib.php.
];
/**
- * Checks whether the given URL is blacklisted by checking its address and port number against the black/white lists.
+ * Checks whether the given URL is blocked by checking its address and port number against the allow/block lists.
* The behaviour of this function can be classified as strict, as it returns true for URLs which are invalid or
- * could not be parsed, as well as those valid URLs which were found in the blacklist.
+ * could not be parsed, as well as those valid URLs which were found in the blocklist.
*
* @param string $urlstring the URL to check.
- * @return bool true if the URL is blacklisted or invalid and false if the URL is not blacklisted.
+ * @return bool true if the URL is blocked or invalid and false if the URL is not blocked.
*/
public function url_is_blocked($urlstring) {
// If no config data is present, then all hosts/ports are allowed.
}
if ($parsed['port'] && $parsed['host']) {
- // Check the host and port against the blacklist/whitelist entries.
+ // Check the host and port against the allow/block entries.
return $this->host_is_blocked($parsed['host']) || $this->port_is_blocked($parsed['port']);
}
return true;
* - This will perform a DNS reverse lookup if required.
*
* The behaviour of this function can be classified as strict, as it returns true for hosts which are invalid or
- * could not be parsed, as well as those valid URLs which were found in the blacklist.
+ * could not be parsed, as well as those valid URLs which were found in the blocklist.
*
- * @param string $host the host component of the URL to check against the blacklist.
+ * @param string $host the host component of the URL to check against the blocklist.
* @return bool true if the host is both valid and blocked, false otherwise.
*/
protected function host_is_blocked($host) {
// Fix for square brackets in the 'host' portion of the URL (only occurs if an IPv6 address is specified).
$host = str_replace(array('[', ']'), '', $host); // RFC3986, section 3.2.2.
- $blacklistedhosts = $this->get_blacklisted_hosts_by_category();
+ $blockedhosts = $this->get_blocked_hosts_by_category();
if (ip_utils::is_ip_address($host)) {
if ($this->address_explicitly_blocked($host)) {
}
// Only perform a reverse lookup if there is a point to it (i.e. we have rules to check against).
- if ($blacklistedhosts['domain'] || $blacklistedhosts['domainwildcard']) {
+ if ($blockedhosts['domain'] || $blockedhosts['domainwildcard']) {
// DNS reverse lookup - supports both IPv4 and IPv6 address formats.
$hostname = gethostbyaddr($host);
if ($hostname !== $host && $this->host_explicitly_blocked($hostname)) {
}
// Only perform a forward lookup if there are IP rules to check against.
- if ($blacklistedhosts['ipv4'] || $blacklistedhosts['ipv6']) {
+ if ($blockedhosts['ipv4'] || $blockedhosts['ipv6']) {
// DNS forward lookup - returns a list of only IPv4 addresses!
$hostips = $this->get_host_list_by_name($host);
return true;
}
- // If any of the returned IPs are in the blacklist, block the request.
+ // If any of the returned IPs are in the blocklist, block the request.
foreach ($hostips as $hostip) {
if ($this->address_explicitly_blocked($hostip)) {
return true;
}
/**
- * Checks whether the given port is blocked, as determined by its absence on the ports whitelist.
- * Ports are assumed to be blocked unless found in the whitelist.
+ * Checks whether the given port is blocked, as determined by its absence on the ports allowlist.
+ * Ports are assumed to be blocked unless found in the allowlist.
*
- * @param integer|string $port the port to check against the ports whitelist.
+ * @param integer|string $port the port to check against the ports allowlist.
* @return bool true if the port is blocked, false otherwise.
*/
protected function port_is_blocked($port) {
if (empty($port) || (string)$portnum !== (string)$port || $port < 0) {
return true;
}
- $allowedports = $this->get_whitelisted_ports();
+ $allowedports = $this->get_allowed_ports();
return !empty($allowedports) && !in_array($portnum, $allowedports);
}
/**
- * Convenience method to check whether we have any entries in the host blacklist or ports whitelist admin settings.
- * If no entries are found at all, the assumption is that the blacklist is disabled entirely.
+ * Convenience method to check whether we have any entries in the host blocklist or ports allowlist admin settings.
+ * If no entries are found at all, the assumption is that the blocklist is disabled entirely.
*
* @return bool true if one or more entries exist, false otherwise.
*/
public function is_enabled() {
- return (!empty($this->get_whitelisted_ports()) || !empty($this->get_blacklisted_hosts()));
+ return (!empty($this->get_allowed_ports()) || !empty($this->get_blocked_hosts()));
}
/**
* Checks whether the input address is blocked by at any of the IPv4 or IPv6 address rules.
*
* @param string $addr the ip address to check.
- * @return bool true if the address is covered by an entry in the blacklist, false otherwise.
+ * @return bool true if the address is covered by an entry in the blocklist, false otherwise.
*/
protected function address_explicitly_blocked($addr) {
- $blockedhosts = $this->get_blacklisted_hosts_by_category();
+ $blockedhosts = $this->get_blocked_hosts_by_category();
$iphostsblocked = array_merge($blockedhosts['ipv4'], $blockedhosts['ipv6']);
return address_in_subnet($addr, implode(',', $iphostsblocked));
}
* Checks whether the input hostname is blocked by any of the domain/wildcard rules.
*
* @param string $host the hostname to check
- * @return bool true if the host is covered by an entry in the blacklist, false otherwise.
+ * @return bool true if the host is covered by an entry in the blocklist, false otherwise.
*/
protected function host_explicitly_blocked($host) {
- $blockedhosts = $this->get_blacklisted_hosts_by_category();
+ $blockedhosts = $this->get_blocked_hosts_by_category();
$domainhostsblocked = array_merge($blockedhosts['domain'], $blockedhosts['domainwildcard']);
return ip_utils::is_domain_in_allowed_list($host, $domainhostsblocked);
}
*
* @return array of host/domain/ip entries from the 'curlsecurityblockedhosts' config.
*/
- protected function get_blacklisted_hosts_by_category() {
+ protected function get_blocked_hosts_by_category() {
// For each of the admin setting entries, check and place in the correct section of the config array.
$config = ['ipv6' => [], 'ipv4' => [], 'domain' => [], 'domainwildcard' => []];
- $entries = $this->get_blacklisted_hosts();
+ $entries = $this->get_blocked_hosts();
foreach ($entries as $entry) {
if (ip_utils::is_ipv6_address($entry) || ip_utils::is_ipv6_range($entry)) {
$config['ipv6'][] = $entry;
}
/**
- * Helper that returns the whitelisted ports, as defined in the 'curlsecurityallowedport' setting.
+ * Helper that returns the allowed ports, as defined in the 'curlsecurityallowedport' setting.
*
- * @return array the array of whitelisted ports.
+ * @return array the array of allowed ports.
*/
- protected function get_whitelisted_ports() {
+ protected function get_allowed_ports() {
global $CFG;
if (!isset($CFG->curlsecurityallowedport)) {
return [];
}
/**
- * Helper that returns the blacklisted hosts, as defined in the 'curlsecurityblockedhosts' setting.
+ * Helper that returns the blocked hosts, as defined in the 'curlsecurityblockedhosts' setting.
*
- * @return array the array of blacklisted host entries.
+ * @return array the array of blocked host entries.
*/
- protected function get_blacklisted_hosts() {
+ protected function get_blocked_hosts() {
global $CFG;
if (!isset($CFG->curlsecurityblockedhosts)) {
return [];
/**
* @var string[] Names of helpers that aren't allowed to be called within other helpers.
*/
- private $blacklistednestedhelpers = [];
+ private $disallowednestedhelpers = [];
/**
* Mustache engine constructor.
* $options = [
* // A list of helpers (by name) to prevent from executing within the rendering
* // of other helpers.
- * 'blacklistednestedhelpers' => ['js']
+ * 'disallowednestedhelpers' => ['js']
* ];
* @param array $options [description]
*/
public function __construct(array $options = []) {
+
if (isset($options['blacklistednestedhelpers'])) {
- $this->blacklistednestedhelpers = $options['blacklistednestedhelpers'];
+ debugging('blacklistednestedhelpers option is deprecated. Use disallowednestedhelpers instead.', DEBUG_DEVELOPER);
+ $this->disallowednestedhelpers = $options['blacklistednestedhelpers'];
+ }
+
+ if (isset($options['disallowednestedhelpers'])) {
+ $this->disallowednestedhelpers = $options['disallowednestedhelpers'];
}
parent::__construct($options);
public function getHelpers()
{
if (!isset($this->helpers)) {
- $this->helpers = new mustache_helper_collection(null, $this->blacklistednestedhelpers);
+ $this->helpers = new mustache_helper_collection(null, $this->disallowednestedhelpers);
}
return $this->helpers;
/**
* @var string[] Names of helpers that aren't allowed to be called within other helpers.
*/
- private $blacklistednestedhelpers = [];
+ private $disallowednestedhelpers = [];
/**
* Helper Collection constructor.
* @throws \Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
*
* @param array|\Traversable $helpers (default: null)
- * @param string[] $blacklistednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
+ * @param string[] $disallowednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
*/
- public function __construct($helpers = null, array $blacklistednestedhelpers = []) {
- $this->blacklistednestedhelpers = $blacklistednestedhelpers;
+ public function __construct($helpers = null, array $disallowednestedhelpers = []) {
+ $this->disallowednestedhelpers = $disallowednestedhelpers;
parent::__construct($helpers);
}
/**
* Add a helper to this collection.
*
- * This function has overridden the parent implementation to provide blacklist
+ * This function has overridden the parent implementation to provide disallowing
* functionality for certain helpers to prevent them being called from within
* other helpers. This is because the JavaScript helper can be used in a
* security exploit if it can be nested.
*
* The function will wrap callable helpers in an anonymous function that strips
- * out the blacklisted helpers from the source string before giving it to the
- * helper function. This prevents the blacklisted helper functions from being
+ * out the disallowed helpers from the source string before giving it to the
+ * helper function. This prevents the disallowed helper functions from being
* called by nested render functions from within other helpers.
*
* @see \Mustache_HelperCollection::add()
* @param string $name
* @param mixed $helper
*/
- public function add($name, $helper)
- {
- $blacklist = $this->blacklistednestedhelpers;
+ public function add($name, $helper) {
- if (is_callable($helper) && !empty($blacklist)) {
- $helper = function($source, \Mustache_LambdaHelper $lambdahelper) use ($helper, $blacklist) {
+ $disallowedlist = $this->disallowednestedhelpers;
- // Temporarily override the blacklisted helpers to return nothing
+ if (is_callable($helper) && !empty($disallowedlist)) {
+ $helper = function($source, \Mustache_LambdaHelper $lambdahelper) use ($helper, $disallowedlist) {
+
+ // Temporarily override the disallowed helpers to return nothing
// so that they can't be executed from within other helpers.
- $disabledhelpers = $this->disable_helpers($blacklist);
+ $disabledhelpers = $this->disable_helpers($disallowedlist);
// Call the original function with the modified sources.
$result = call_user_func($helper, $source, $lambdahelper);
- // Restore the original blacklisted helper implementations now
+ // Restore the original disallowed helper implementations now
// that this helper has finished executing so that the rest of
// the rendering process continues to work correctly.
$this->restore_helpers($disabledhelpers);
// This is done because a secondary render is called on the result
// of a helper function if it still includes mustache tags. See
// the section function of Mustache_Compiler for details.
- return $this->strip_blacklisted_helpers($blacklist, $result);
+ return $this->strip_disallowed_helpers($disallowedlist, $result);
};
}
}
/**
- * Parse the given string and remove any reference to blacklisted helpers.
+ * Parse the given string and remove any reference to disallowed helpers.
*
* E.g.
- * $blacklist = ['js'];
+ * $disallowedlist = ['js'];
* $string = "core, move, {{#js}} some nasty JS hack {{/js}}"
* result: "core, move, {{}}"
*
- * @param string[] $blacklist List of helper names to strip
+ * @param string[] $disallowedlist List of helper names to strip
* @param string $string String to parse
* @return string Parsed string
*/
- public function strip_blacklisted_helpers($blacklist, $string) {
+ public function strip_disallowed_helpers($disallowedlist, $string) {
$starttoken = \Mustache_Tokenizer::T_SECTION;
$endtoken = \Mustache_Tokenizer::T_END_SECTION;
if ($endtoken == '/') {
// the user is able to change the delimeters on a per template
// basis so they may not be curly braces.
return '/\s*' . $starttoken . '\s*'. $name . '\W+.*' . $endtoken . '\s*' . $name . '\s*/';
- }, $blacklist);
+ }, $disallowedlist);
// This will strip out unwanted helpers from the $source string
// before providing it to the original helper function.
// Before:
// "core, move, {{#js}} some nasty JS hack {{/js}}"
// After:
- // "core, move, {{}}"
+ // "core, move, {{}}".
return preg_replace_callback($regexes, function() {
return '';
}, $string);
}
+
+ /**
+ * Parse the given string and remove any reference to disallowed helpers.
+ *
+ * @deprecated Deprecated since Moodle 3.10 (MDL-69050) - use {@see self::strip_disallowed_helpers()}
+ * @param string[] $disallowedlist List of helper names to strip
+ * @param string $string String to parse
+ * @return string Parsed string
+ */
+ public function strip_blacklisted_helpers($disallowedlist, $string) {
+
+ debugging('mustache_helper_collection::strip_blacklisted_helpers() is deprecated. ' .
+ 'Please use mustache_helper_collection::strip_disallowed_helpers() instead.', DEBUG_DEVELOPER);
+
+ return $this->strip_disallowed_helpers($disallowedlist, $string);
+ }
}
// The user has chosen to delete the selected users picture.
$fs->delete_area_files($context->id, 'user', 'icon'); // Drop all images in area.
$newpicture = 0;
+ }
- } else {
- // Save newly uploaded file, this will avoid context mismatch for newly created users.
- file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
- if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
- // Get file which was uploaded in draft area.
- foreach ($iconfiles as $file) {
- if (!$file->is_directory()) {
- break;
- }
- }
- // Copy file to temporary location and the send it for processing icon.
- if ($iconfile = $file->copy_content_to_temp()) {
- // There is a new image that has been uploaded.
- // Process the new image and set the user to make use of it.
- // NOTE: Uploaded images always take over Gravatar.
- $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
- // Delete temporary file.
- @unlink($iconfile);
- // Remove uploaded file.
- $fs->delete_area_files($context->id, 'user', 'newicon');
- } else {
- // Something went wrong while creating temp file.
- // Remove uploaded file.
- $fs->delete_area_files($context->id, 'user', 'newicon');
- return false;
+ // Save newly uploaded file, this will avoid context mismatch for newly created users.
+ if (!isset($usernew->imagefile)) {
+ $usernew->imagefile = 0;
+ }
+ file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
+ if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
+ // Get file which was uploaded in draft area.
+ foreach ($iconfiles as $file) {
+ if (!$file->is_directory()) {
+ break;
}
}
+ // Copy file to temporary location and the send it for processing icon.
+ if ($iconfile = $file->copy_content_to_temp()) {
+ // There is a new image that has been uploaded.
+ // Process the new image and set the user to make use of it.
+ // NOTE: Uploaded images always take over Gravatar.
+ $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
+ // Delete temporary file.
+ @unlink($iconfile);
+ // Remove uploaded file.
+ $fs->delete_area_files($context->id, 'user', 'newicon');
+ } else {
+ // Something went wrong while creating temp file.
+ // Remove uploaded file.
+ $fs->delete_area_files($context->id, 'user', 'newicon');
+ return false;
+ }
}
if ($newpicture != $user->picture) {
*
* @global object
* @global object
- * @uses MAX_COURSES_IN_CATEGORY
* @uses MAX_COURSE_CATEGORIES
* @uses SITEID
* @uses CONTEXT_COURSE
if ($unsorted = $DB->get_records('course_categories', array('sortorder'=>0))) {
//move all categories that are not sorted yet to the end
- $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('sortorder'=>0));
+ $DB->set_field('course_categories', 'sortorder',
+ get_max_courses_in_category() * MAX_COURSE_CATEGORIES, array('sortorder' => 0));
$cacheevents['changesincoursecat'] = true;
}
$categories = array();
foreach ($updatecounts as $cat) {
$cat->coursecount = $cat->newcount;
- if ($cat->coursecount >= MAX_COURSES_IN_CATEGORY) {
+ if ($cat->coursecount >= get_max_courses_in_category()) {
$categories[] = $cat->id;
}
unset($cat->newcount);
}
if (!empty($categories)) {
$str = implode(', ', $categories);
- debugging("The number of courses (category id: $str) has reached MAX_COURSES_IN_CATEGORY (" . MAX_COURSES_IN_CATEGORY . "), it will cause a sorting performance issue, please increase the value of MAX_COURSES_IN_CATEGORY in lib/datalib.php file. See tracker issue: MDL-25669", DEBUG_DEVELOPER);
+ debugging("The number of courses (category id: $str) has reached max number of courses " .
+ "in a category (" . get_max_courses_in_category() . "). It will cause a sorting performance issue. " .
+ "Please set higher value for \$CFG->maxcoursesincategory in config.php. " .
+ "Please also make sure \$CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer. " .
+ "See tracker issues: MDL-25669 and MDL-69573", DEBUG_DEVELOPER);
}
$cacheevents['changesincoursecat'] = true;
}
$sql = "SELECT DISTINCT cc.id, cc.sortorder
FROM {course_categories} cc
JOIN {course} c ON c.category = cc.id
- WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + ".MAX_COURSES_IN_CATEGORY;
+ WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + " . get_max_courses_in_category();
if ($fixcategories = $DB->get_records_sql($sql)) {
//fix the course sortorder ranges
foreach ($fixcategories as $cat) {
$sql = "UPDATE {course}
- SET sortorder = ".$DB->sql_modulo('sortorder', MAX_COURSES_IN_CATEGORY)." + ?
+ SET sortorder = ".$DB->sql_modulo('sortorder', get_max_courses_in_category())." + ?
WHERE category = ?";
$DB->execute($sql, array($cat->sortorder, $cat->id));
}
* @todo Document the arguments of this function better
*
* @global object
- * @uses MAX_COURSES_IN_CATEGORY
* @uses CONTEXT_COURSECAT
* @param array $children
* @param int $sortorder
$changesmade = false;
foreach ($children as $cat) {
- $sortorder = $sortorder + MAX_COURSES_IN_CATEGORY;
+ $sortorder = $sortorder + get_max_courses_in_category();
$update = false;
if ($parent != $cat->parent or $depth != $cat->depth or $path.'/'.$cat->id != $cat->path) {
$cat->parent = $parent;
return $safechanges;
}
+
+/**
+ * Return maximum number of courses in a category
+ *
+ * @uses MAX_COURSES_IN_CATEGORY
+ * @return int number of courses
+ */
+function get_max_courses_in_category() {
+ global $CFG;
+ // Use default MAX_COURSES_IN_CATEGORY if $CFG->maxcoursesincategory is not set or invalid.
+ if (!isset($CFG->maxcoursesincategory) || clean_param($CFG->maxcoursesincategory, PARAM_INT) == 0) {
+ return MAX_COURSES_IN_CATEGORY;
+ } else {
+ return $CFG->maxcoursesincategory;
+ }
+}
$cat = new stdClass();
$cat->name = get_string('miscellaneous');
$cat->depth = 1;
- $cat->sortorder = MAX_COURSES_IN_CATEGORY;
+ $cat->sortorder = get_max_courses_in_category();
$cat->timemodified = time();
$catid = $DB->insert_record('course_categories', $cat);
$DB->set_field('course_categories', 'path', '/'.$catid, array('id'=>$catid));
),
),
+ // Course completed. Requires course completion configured at course level. It does not work with just activity progress.
+ 'coursecompleted' => [],
+
// Badge award notification to a badge recipient.
'badgerecipientnotice' => array (
'defaults' => array(
'type' => 'read',
'ajax' => true,
],
+ 'core_calendar_get_calendar_export_token' => [
+ 'classname' => 'core_calendar\external\export\token',
+ 'methodname' => 'execute',
+ 'description' => 'Return the auth token required for exporting a calendar.',
+ 'type' => 'read',
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+ ],
'core_cohort_add_cohort_members' => array(
'classname' => 'core_cohort_external',
'methodname' => 'add_cohort_members',
/**
* List all core subsystems and their location
*
- * This is a whitelist of components that are part of the core and their
+ * This is a list of components that are part of the core and their
* language strings are defined in /lang/en/<<subsystem>>.php. If a given
* plugin is not listed here and it does not have proper plugintype prefix,
* then it is considered as course activity module.
continue;
}
if ($instance->enrol === 'guest') {
- // blacklist known temporary guest plugins
continue;
}
if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
'value' => new external_value(PARAM_RAW, 'value of the item as it is stored', VALUE_OPTIONAL),
'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_OPTIONAL),
'edithint' => new external_value(PARAM_NOTAGS, 'hint for editing element', VALUE_OPTIONAL),
- 'editlabel' => new external_value(PARAM_NOTAGS, 'label for editing element', VALUE_OPTIONAL),
+ 'editlabel' => new external_value(PARAM_RAW, 'label for editing element', VALUE_OPTIONAL),
'type' => new external_value(PARAM_ALPHA, 'type of the element (text, toggle, select)', VALUE_OPTIONAL),
'options' => new external_value(PARAM_RAW, 'options of the element, format depends on type', VALUE_OPTIONAL),
'linkeverything' => new external_value(PARAM_INT, 'Should everything be wrapped in the edit link or link displayed separately', VALUE_OPTIONAL),
$tag = $this->getDataGenerator()->create_tag();
$res = core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name');
$res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
+
$this->assertEquals('new tag name', $res['value']);
}
+ /**
+ * Test update_inplace_editable with mathjax.
+ */
+ public function test_update_inplace_editable_with_mathjax() {
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+
+ // Enable MathJax filter in content and headings.
+ $this->configure_filters([
+ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+ ]);
+
+ // Create a forum.
+ $course = $this->getDataGenerator()->create_course();
+ $forum = self::getDataGenerator()->create_module('forum', array('course' => $course->id, 'name' => 'forum name'));
+
+ // Change the forum name.
+ $newname = 'New forum name $$(a+b)=2$$';
+ $res = core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, $newname);
+ $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
+
+ // Format original data.
+ $context = context_module::instance($forum->cmid);
+ $newname = external_format_string($newname, $context->id);
+ $editlabel = get_string('newactivityname', '', $newname);
+
+ // Check editlabel is the same and has mathjax.
+ $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $res['editlabel']);
+ $this->assertEquals($editlabel, $res['editlabel']);
+ }
+
public function test_get_user_dates() {
$this->resetAfterTest();
$unsettoken = true;
}
- // Remove token if its ip not in whitelist.
+ // Remove token if its IP is restricted.
if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
$unsettoken = true;
}
/** @var string The session lang */
private $lang = '';
+ /** @var string The timezone to use during this WS request */
+ private $timezone = '';
+
/**
* Constructor - protected - can not be instanciated
*/
public function get_lang() {
return $this->lang;
}
+
+ /**
+ * Set timezone
+ *
+ * @param string $timezone
+ */
+ public function set_timezone($timezone) {
+ $this->timezone = $timezone;
+ }
+
+ /**
+ * Get timezone
+ *
+ * @return string
+ */
+ public function get_timezone() {
+ return $this->timezone;
+ }
}
/**
}
}
- if ($filesize > 10000000) {
- // for large files try to flush and close all buffers to conserve memory
- while(@ob_get_level()) {
- if (!@ob_end_flush()) {
- break;
- }
- }
- }
-
- // Send this header after we have flushed the buffers so that if we fail
- // later can remove this because it wasn't sent.
header('Content-Length: ' . $filesize);
if (!empty($_SERVER['REQUEST_METHOD']) and $_SERVER['REQUEST_METHOD'] === 'HEAD') {
exit;
}
+ while (ob_get_level()) {
+ $handlerstack = ob_list_handlers();
+ $activehandler = array_pop($handlerstack);
+ if ($activehandler === 'default output handler') {
+ // We do not expect any content in the buffer when we are serving files.
+ $buffercontents = ob_get_clean();
+ if ($buffercontents !== '') {
+ error_log('Non-empty default output handler buffer detected while serving the file ' . $file);
+ }
+ } else {
+ // Some handlers such as zlib output compression may have file signature buffered - flush it.
+ ob_end_flush();
+ }
+ }
+
// send the whole file content
if (is_object($file)) {
$file->readfile();
private $cookie = false;
/** @var bool tracks multiple headers in response - redirect detection */
private $responsefinished = false;
- /** @var security helper class, responsible for checking host/ports against blacklist/whitelist entries.*/
+ /** @var security helper class, responsible for checking host/ports against allowed/blocked entries.*/
private $securityhelper;
/** @var bool ignoresecurity a flag which can be supplied to the constructor, allowing security to be bypassed. */
private $ignoresecurity;
* @return bool
*/
protected function request($url, $options = array()) {
- // Reset here so that the data is valid when result returned from cache, or if we return due to a blacklist hit.
+ // Reset here so that the data is valid when result returned from cache, or if we return due to a blocked URL hit.
$this->reset_request_state_vars();
if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
}
}
- // If curl security is enabled, check the URL against the blacklist before calling curl_exec.
- // Note: This will only check the base url. In the case of redirects, the blacklist is also after the curl_exec.
+ // If curl security is enabled, check the URL against the list of blocked URLs before calling curl_exec.
+ // Note: This will only check the base url. In the case of redirects, the blocking check is also after the curl_exec.
if (!$this->ignoresecurity && $this->securityhelper->url_is_blocked($url)) {
$this->error = $this->securityhelper->get_blocked_url_string();
return $this->error;
$this->errno = curl_errno($curl);
// Note: $this->response and $this->rawresponse are filled by $hits->formatHeader callback.
- // In the case of redirects (which curl blindly follows), check the post-redirect URL against the blacklist entries too.
+ // In the case of redirects (which curl blindly follows), check the post-redirect URL against the list of blocked list too.
if (intval($this->info['redirect_count']) > 0 && !$this->ignoresecurity
&& $this->securityhelper->url_is_blocked($this->info['url'])) {
$this->reset_request_state_vars();
unlink($archive);
}
+ /**
+ * Test opening an encrypted archive
+ */
+ public function test_open_encrypted_archive() {
+ $this->resetAfterTest();
+
+ // The archive contains a single encrypted "hello.txt" file.
+ $archive = __DIR__ . '/fixtures/passwordis1.zip';
+
+ /** @var zip_packer $packer */
+ $packer = get_file_packer('application/zip');
+ $result = $packer->extract_to_pathname($archive, make_temp_directory('zip'));
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('hello.txt', $result);
+ $this->assertEquals('Can not read file from zip archive', $result['hello.txt']);
+ }
+
/**
* Tests the progress reporting.
*/
$types = [];
foreach ($groupinfo->extensions as $extension) {
- if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
+ if ($onlytypes && !$this->is_listed($extension, $onlytypes)) {
$group->selectable = false;
$group->expanded = true;
$group->ext = '';
continue;
}
$extension = '.'.$extension;
- if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
+ if ($onlytypes && !$this->is_listed($extension, $onlytypes)) {
continue;
}
if (!isset($info['groups']) || empty($info['groups'])) {
}