diff --git a/admin/filters.php b/admin/filters.php index 411751f..acb21c7 100644 --- a/admin/filters.php +++ b/admin/filters.php @@ -1,12 +1,44 @@ -wwwroot/$CFG->admin/settings.php?section=managefilters"; @@ -14,89 +46,98 @@ redirect($returnurl); } - // get a list of installed filters - $installedfilters = array(); - $filterlocations = array('mod','filter'); - foreach ($filterlocations as $filterlocation) { - $plugins = get_list_of_plugins($filterlocation); - foreach ($plugins as $plugin) { - $pluginpath = "$CFG->dirroot/$filterlocation/$plugin/filter.php"; - if (is_readable($pluginpath)) { - $installedfilters["$filterlocation/$plugin"] = "$filterlocation/$plugin"; - } - } - } + $filters = filter_get_global_states(); - // get all the currently selected filters - if (!empty($CFG->textfilters)) { - $activefilters = explode(',', $CFG->textfilters); - } else { - $activefilters = array(); + // In case any new filters have been installed, but not put in the table yet. + $fitlernames = filter_get_all_installed(); + $newfilters = $fitlernames; + foreach ($filters as $filter => $notused) { + unset($newfilters[$filter]); } - //====================== - // Process Actions - //====================== + if (!isset($filters[$filterpath]) && !isset($newfilters[$filterpath])) { + throw new moodle_exception('filternotinstalled', 'error', $returnurl, $filterpath); + } switch ($action) { - case 'hide': - $key=array_search($filterpath, $activefilters); - // check filterpath is valid - if ($key===false) { - break; + case 'setstate': + if ($newstate = optional_param('newstate', '', PARAM_INTEGER)) { + filter_set_global_state($filterpath, $newstate); + if ($newstate == TEXTFILTER_DISABLED) { + filter_set_applies_to_strings($filterpath, false); + } + unset($newfilters[$filterpath]); } - // just delete it - unset($activefilters[$key]); break; - case 'show': - // check filterpath is valid - if (!array_key_exists($filterpath, $installedfilters)) { - print_error('filternotinstalled', 'error', $url, $filterpath); - } elseif (array_search($filterpath,$activefilters)) { - // filterpath is already active - doubleclick?? - } else { - // add it to installed filters - $activefilters[] = $filterpath; - $activefilters = array_unique($activefilters); - } + case 'setapplyto': + $applytostrings = optional_param('stringstoo', false, PARAM_BOOL); + filter_set_applies_to_strings($filterpath, $applytostrings); break; case 'down': - $key=array_search($filterpath, $activefilters); - // check filterpath is valid - if ($key===false) { - print_error("filternotactive", 'error', $url, $filterpath ); - } elseif ($key>=(count($activefilters)-1)) { - // cannot be moved any further down - doubleclick?? - } else { - // swap with $key+1 - $fsave = $activefilters[$key]; - $activefilters[$key] = $activefilters[$key+1]; - $activefilters[$key+1] = $fsave; + if (isset($filters[$filterpath])) { + $oldpos = $filters[$filterpath]->sortorder; + if ($oldpos <= count($filters)) { + filter_set_global_state($filterpath, $filters[$filterpath]->active, $oldpos + 1); + } } break; case 'up': - $key=array_search($filterpath, $activefilters); - // check filterpath is valid - if ($key===false) { - print_error("filternotactive", 'error', $url, $filterpath ); - } elseif ($key<1) { - //cannot be moved any further up - doubleclick?? - } else { - // swap with $key-1 - $fsave = $activefilters[$key]; - $activefilters[$key] = $activefilters[$key-1]; - $activefilters[$key-1] = $fsave; + if (isset($filters[$filterpath])) { + $oldpos = $filters[$filterpath]->sortorder; + if ($oldpos >= 1) { + filter_set_global_state($filterpath, $filters[$filterpath]->active, $oldpos - 1); + } } break; + + case 'delete': + $filtername = $fitlernames[$filterpath]; + if (substr($filterpath, 0, 4) == 'mod/') { + $mod = basename($filterpath); + $a = new stdClass; + $a->filter = $filtername; + $a->module = get_string('modulename', $mod); + print_error('cannotdeletemodfilter', 'admin', admin_url('qtypes.php'), $a); + } + + // If not yet confirmed, display a confirmation message. + if (!optional_param('confirm', '', PARAM_BOOL)) { + $title = get_string('deletefilterareyousure', 'admin', $filtername); + print_header($title, $title); + print_heading($title); + notice_yesno(get_string('deletefilterareyousuremessage', 'admin', $filtername), + admin_url('filters.php?action=delete&filterpath=' . $delete . '&confirm=1&sesskey=' . sesskey()), + $returnurl, NULL, NULL, 'post', 'get'); + print_footer('empty'); + exit; + } + + // Do the deletion. + $title = get_string('deletingfilter', 'admin', $filtername); + print_header($title, $title); + print_heading($title); + + // Delete all data for this plugin. + filter_delete_all_data($filterpath); + + $a = new stdClass; + $a->fitler = $filtername; + $a->directory = $filterparth; + print_box(get_string('deletefilterfiles', 'admin', $a), 'generalbox', 'notice'); + print_continue($returnurl); + admin_externalpage_print_footer(); + exit; } - // save, reset cache and return - set_config('textfilters', implode(',', $activefilters)); + // Add any missing filters to the DB table. + foreach ($newfilters as $filter => $notused) { + filter_set_global_state($filter, TEXTFILTER_DISABLED); + } + + // Reset caches and return reset_text_filters_cache(); redirect($returnurl); - -?> diff --git a/admin/report/security/lib.php b/admin/report/security/lib.php index 2daf7dc..4e26705 100644 --- a/admin/report/security/lib.php +++ b/admin/report/security/lib.php @@ -226,11 +226,7 @@ function report_security_check_mediafilterswf($detailed=false) { $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=filtersettingfiltermediaplugin\">".get_string('filtersettings', 'admin').''; - if (!empty($CFG->textfilters)) { - $activefilters = explode(',', $CFG->textfilters); - } else { - $activefilters = array(); - } + $activefilters = filters_get_globally_enabled(); if (array_search('filter/mediaplugin', $activefilters) !== false and !empty($CFG->filter_mediaplugin_enable_swf)) { $result->status = REPORT_SECURITY_CRITICAL; diff --git a/admin/roles/tabs.php b/admin/roles/tabs.php index c86fe4e..86194b8 100755 --- a/admin/roles/tabs.php +++ b/admin/roles/tabs.php @@ -35,6 +35,15 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page } +if (!isset($availablefilters)) { + $availablefilters = array(); + if (in_array($context->contextlevel, array(CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE)) && + !($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) && + has_capability('moodle/filter:manage', $context)) { + $availablefilters = filter_get_available_in_context($context); + } +} + $navlinks = array(); if ($currenttab != 'update') { switch ($context->contextlevel) { @@ -202,6 +211,11 @@ if ($context->contextlevel != CONTEXT_SYSTEM) { // Print tabs for anything ex get_string('checkpermissions', 'role')); } + if (!empty($availablefilters)) { + $toprow[] = new tabobject('filters', + $CFG->wwwroot.'/filter/manage.php?contextid=' . $context->id, + get_string('filters', 'admin')); + } } /// Here other core tabs should go (always calling tabs.php files) @@ -211,7 +225,7 @@ if ($context->contextlevel != CONTEXT_SYSTEM) { // Print tabs for anything ex /// Finally, we support adding some 'on-the-fly' tabs here /// All the logic to decide what to show must be self-cointained in the tabs file - if (isset($CFG->extratabs) && !empty($CFG->extratabs)) { + if (!empty($CFG->extratabs)) { if ($extratabs = explode(',', $CFG->extratabs)) { asort($extratabs); foreach($extratabs as $extratab) { diff --git a/admin/settings/frontpage.php b/admin/settings/frontpage.php index 7376ce1..9306939 100644 --- a/admin/settings/frontpage.php +++ b/admin/settings/frontpage.php @@ -63,6 +63,8 @@ if (get_site()) { //do not use during installation $ADMIN->add('frontpage', new admin_externalpage('frontpageroles', get_string('frontpageroles', 'admin'), "$CFG->wwwroot/$CFG->admin/roles/assign.php?contextid=" . $frontpagecontext->id, 'moodle/role:assign', false, $frontpagecontext)); + $ADMIN->add('frontpage', new admin_externalpage('frontpagefilters', get_string('frontpagefilters', 'admin'), "$CFG->wwwroot/filter/manage.php?contextid=" . $frontpagecontext->id, 'moodle/filter:manage', false, $frontpagecontext)); + $ADMIN->add('frontpage', new admin_externalpage('frontpagebackup', get_string('frontpagebackup', 'admin'), $CFG->wwwroot.'/backup/backup.php?id='.SITEID, 'moodle/site:backup', false, $frontpagecontext)); $ADMIN->add('frontpage', new admin_externalpage('frontpagerestore', get_string('frontpagerestore', 'admin'), $CFG->wwwroot.'/files/index.php?id='.SITEID.'&wdir=/backupdata', 'moodle/site:restore', false, $frontpagecontext)); diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index ed32575..9a82a27 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -77,36 +77,37 @@ if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) // "filtersettings" settingpage $temp = new admin_settingpage('managefilters', get_string('filtersettings', 'admin')); if ($ADMIN->fulltree) { + $cachetimes = array( + 604800 => get_string('numdays','',7), + 86400 => get_string('numdays','',1), + 43200 => get_string('numhours','',12), + 10800 => get_string('numhours','',3), + 7200 => get_string('numhours','',2), + 3600 => get_string('numhours','',1), + 2700 => get_string('numminutes','',45), + 1800 => get_string('numminutes','',30), + 900 => get_string('numminutes','',15), + 600 => get_string('numminutes','',10), + 540 => get_string('numminutes','',9), + 480 => get_string('numminutes','',8), + 420 => get_string('numminutes','',7), + 360 => get_string('numminutes','',6), + 300 => get_string('numminutes','',5), + 240 => get_string('numminutes','',4), + 180 => get_string('numminutes','',3), + 120 => get_string('numminutes','',2), + 60 => get_string('numminutes','',1), + 30 => get_string('numseconds','',30), + 0 => get_string('no') + ); $items = array(); $items[] = new admin_setting_managefilters(); $items[] = new admin_setting_heading('managefilterscommonheading', get_string('commonsettings', 'admin'), ''); - $items[] = new admin_setting_configselect('cachetext', get_string('cachetext', 'admin'), get_string('configcachetext', 'admin'), 60, array(604800 => get_string('numdays','',7), - 86400 => get_string('numdays','',1), - 43200 => get_string('numhours','',12), - 10800 => get_string('numhours','',3), - 7200 => get_string('numhours','',2), - 3600 => get_string('numhours','',1), - 2700 => get_string('numminutes','',45), - 1800 => get_string('numminutes','',30), - 900 => get_string('numminutes','',15), - 600 => get_string('numminutes','',10), - 540 => get_string('numminutes','',9), - 480 => get_string('numminutes','',8), - 420 => get_string('numminutes','',7), - 360 => get_string('numminutes','',6), - 300 => get_string('numminutes','',5), - 240 => get_string('numminutes','',4), - 180 => get_string('numminutes','',3), - 120 => get_string('numminutes','',2), - 60 => get_string('numminutes','',1), - 30 => get_string('numseconds','',30), - 0 => get_string('no'))); - $items[] = new admin_setting_configselect('filteruploadedfiles', get_string('filteruploadedfiles', 'admin'), get_string('configfilteruploadedfiles', 'admin'), 0, array('0' => get_string('none'), - '1' => get_string('allfiles'), - '2' => get_string('htmlfilesonly'))); + $items[] = new admin_setting_configselect('cachetext', get_string('cachetext', 'admin'), get_string('configcachetext', 'admin'), 60, $cachetimes); + $items[] = new admin_setting_configselect('filteruploadedfiles', get_string('filteruploadedfiles', 'admin'), get_string('configfilteruploadedfiles', 'admin'), 0, + array('0' => get_string('none'), '1' => get_string('allfiles'), '2' => get_string('htmlfilesonly'))); $items[] = new admin_setting_configcheckbox('filtermatchoneperpage', get_string('filtermatchoneperpage', 'admin'), get_string('configfiltermatchoneperpage', 'admin'), 0); $items[] = new admin_setting_configcheckbox('filtermatchonepertext', get_string('filtermatchonepertext', 'admin'), get_string('configfiltermatchonepertext', 'admin'), 0); - $items[] = new admin_setting_configcheckbox('filterall', get_string('filterall', 'admin'), get_string('configfilterall', 'admin'), 0); foreach ($items as $item) { $item->set_updatedcallback('reset_text_filters_cache'); $temp->add($item); @@ -114,16 +115,12 @@ if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) } $ADMIN->add('filtersettings', $temp); - if (empty($CFG->textfilters)) { - $activefilters = array(); - } else { - $activefilters = explode(',', $CFG->textfilters); - } + $activefilters = filter_get_globally_enabled(); $filternames = filter_get_all_installed(); foreach ($filternames as $filterpath => $strfiltername) { if (file_exists("$CFG->dirroot/$filterpath/filtersettings.php")) { $settings = new admin_settingpage('filtersetting'.str_replace('/', '', $filterpath), - $strfiltername, 'moodle/site:config', !in_array($filterpath, $activefilters)); + $strfiltername, 'moodle/site:config', !isset($activefilters[$filterpath])); if ($ADMIN->fulltree) { include("$CFG->dirroot/$filterpath/filtersettings.php"); } diff --git a/backup/backuplib.php b/backup/backuplib.php index 129a27f..2150076 100644 --- a/backup/backuplib.php +++ b/backup/backuplib.php @@ -715,12 +715,11 @@ fwrite ($bf,full_tag("ENROLENDDATE",3,false,$course->enrolenddate)); fwrite ($bf,full_tag("ENROLPERIOD",3,false,$course->enrolperiod)); fwrite ($bf,full_tag("ENABLECOMPLETION",3,false,$course->enablecompletion)); - - /// write local course overrides here? - write_role_overrides_xml($bf, $context, 3); - /// write role_assign code here - write_role_assignments_xml($bf, $preferences, $context, 3); - //Print header end + + // Write role assigns, overrides, etc. + write_per_context_data($bf, $preferences, $context, 3); + + // Print header end fwrite ($bf,end_tag("HEADER",2,true)); } else { $status = false; @@ -1073,10 +1072,11 @@ $status = $blockobj->instance_backup($bf, $preferences); fwrite ($bf,end_tag('INSTANCEDATA',4,true)); } + + // Write role assigns, overrides, etc. $context = get_context_instance(CONTEXT_BLOCK, $instance->id); - write_role_overrides_xml($bf, $context, 4); - /// write role_assign code here - write_role_assignments_xml($bf, $preferences, $context, 4); + write_per_context_data($bf, $preferences, $context, 4); + //End Block fwrite ($bf,end_tag('BLOCK',3,true)); } @@ -1233,10 +1233,9 @@ fwrite ($bf,full_tag("AVAILABLEUNTIL",6,false,$course_module->availableuntil)); fwrite ($bf,full_tag("SHOWAVAILABILITY",6,false,$course_module->showavailability)); - // get all the role_capabilities overrides in this mod - write_role_overrides_xml($bf, $context, 6); - /// write role_assign code here - write_role_assignments_xml($bf, $preferences, $context, 6); + // Write role assigns, overrides, etc. + write_per_context_data($bf, $preferences, $context, 6); + // write completion data if enabled and user data enabled require_once($CFG->libdir.'/completionlib.php'); $completion=new completion_info($course); @@ -1460,11 +1459,10 @@ fwrite ($bf,end_tag("USER_PREFERENCES",4,true)); } + // Write role assigns, overrides, etc. $context = get_context_instance(CONTEXT_USER, $user->old_id); + write_per_context_data($bf, $preferences, $context, 4); - write_role_overrides_xml($bf, $context, 4); - /// write role_assign code here - write_role_assignments_xml($bf, $preferences, $context, 4); //End User tag fwrite ($bf,end_tag("USER",3,true)); //Do some output @@ -2818,6 +2816,15 @@ } /** + * Writes out all of the data that should appear for every context. + */ + function write_per_context_data($bf, $preferences, $context, $startlevel) { + write_role_overrides_xml($bf, $context, $startlevel); + write_role_assignments_xml($bf, $preferences, $context, $startlevel); + write_local_filter_settings($bf, $preferences, $context, $startlevel); + } + + /** * function to print xml for overrides */ function write_role_overrides_xml($bf, $context, $startlevel) { @@ -2894,6 +2901,36 @@ fwrite ($bf, end_tag("ROLES_ASSIGNMENTS", $startlevel, true)); } + /** + * Write any local filter settings for this context to the backup file. + * They comprise On/off filter_active.active overrides, and any filter_config + * records for this contextid. + */ + function write_local_filter_settings($bf, $preferences, $context, $startlevel) { + if (!filter_context_may_have_filter_settings($context)) { + return; + } + list($actives, $configs) = filter_get_all_local_settings($context->id); + + fwrite($bf, start_tag("FILTERACTIVES", $startlevel, true)); + foreach ($actives as $active) { + fwrite($bf, start_tag("FILTERACTIVE", $startlevel + 1, true)); + fwrite($bf, full_tag("FILTER", $startlevel + 2, false, $active->filter)); + fwrite($bf, full_tag("ACTIVE", $startlevel + 2, false, $active->active)); + fwrite($bf, end_tag("FILTERACTIVE", $startlevel + 1, true)); + } + fwrite($bf, end_tag("FILTERACTIVES", $startlevel, true)); + + fwrite($bf, start_tag("FILTERCONFIGS", $startlevel, true)); + foreach ($configs as $config) { + fwrite($bf, start_tag("FILTERCONFIG", $startlevel + 1, true)); + fwrite($bf, full_tag("FILTER", $startlevel + 2, false, $config->filter)); + fwrite($bf, full_tag("NAME", $startlevel + 2, false, $config->name)); + fwrite($bf, full_tag("VALUE", $startlevel + 2, false, $config->value)); + fwrite($bf, end_tag("FILTERCONFIG", $startlevel + 1, true)); + } + fwrite($bf, end_tag("FILTERCONFIGS", $startlevel, true)); + } function backup_execute(&$preferences, &$errorstr) { global $CFG, $DB; diff --git a/backup/restorelib.php b/backup/restorelib.php index b1899cf..7ab4c01 100644 --- a/backup/restorelib.php +++ b/backup/restorelib.php @@ -5678,6 +5678,39 @@ define('RESTORE_GROUPS_GROUPINGS', 3); } } } /// ends role_overrides + + if ($this->tree[4] == "FILTERACTIVES") { + if ($this->level == 6) { + switch ($tagName) { + case "FILTER": + $this->info->tempfilter = $this->getContents(); + break; + case "ACTIVE": + $this->info->filteractives[$this->info->tempfilter] = $this->getContents(); + break; + } + } + } /// ends FILTERACTIVES + + if ($this->tree[4] == "FILTERCONFIGS") { + if ($this->level == 6) { + switch ($tagName) { + case "FILTER": + $this->info->tempfilter = $this->getContents(); + break; + case "NAME": + $this->info->tempname = $this->getContents(); + break; + case "VALUE": + $fc = new stdClass; + $fc->filter = $this->info->tempfilter; + $fc->name = $this->info->tempfilter; + $fc->value = $this->getContents(); + $this->info->filterconfigs[] = $fc; + break; + } + } + } /// ends FILTERCONFIGS } //Stop parsing if todo = COURSE_HEADER and tagName = HEADER (en of the tag, of course) @@ -6123,6 +6156,40 @@ define('RESTORE_GROUPS_GROUPINGS', 3); } } /// ends role_overrides + + if (isset($this->tree[7]) && $this->tree[7] == "FILTERACTIVES") { + if ($this->level == 9) { + switch ($tagName) { + case "FILTER": + $this->info->tempfilter = $this->getContents(); + break; + case "ACTIVE": + $this->info->filteractives[$this->info->tempfilter] = $this->getContents(); + break; + } + } + } /// ends FILTERACTIVES + + if (isset($this->tree[7]) && $this->tree[7] == "FILTERCONFIGS") { + if ($this->level == 9) { + switch ($tagName) { + case "FILTER": + $this->info->tempfilter = $this->getContents(); + break; + case "NAME": + $this->info->tempname = $this->getContents(); + break; + case "VALUE": + $fc = new stdClass; + $fc->filter = $this->info->tempfilter; + $fc->name = $this->info->tempfilter; + $fc->value = $this->getContents(); + $this->info->filteractives[]->filter = $fc; + break; + } + } + } /// ends FILTERCONFIGS + if (isset($this->tree[7]) && $this->tree[7] == "COMPLETIONDATA") { if($this->level == 8) { switch($tagName) { @@ -8682,7 +8749,7 @@ define('RESTORE_GROUPS_GROUPINGS', 3); // try to restore roles even when restore is going to fail - teachers might have // at least some role assigned - this is not correct though $status = restore_create_roles($restore, $xml_file) && $status; - $status = restore_roles_settings($restore, $xml_file) && $status; + $status = restore_roles_and_filter_settings($restore, $xml_file) && $status; //Now if all is OK, update: // - course modinfo field @@ -8696,7 +8763,7 @@ define('RESTORE_GROUPS_GROUPINGS', 3); $course = $DB->get_record("course", array("id"=>$restore->course_id)); fix_course_sortorder(); // Check if the user has course update capability in the newly restored course - // there is no need to load his capabilities again, because restore_roles_settings + // there is no need to load his capabilities again, because restore_roles_and_filter_settings // would have loaded it anyway, if there is any assignments. // fix for MDL-6831 $newcontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); @@ -8992,7 +9059,7 @@ WHERE * in course/user/block/mod level, it passed through * the xml file again */ - function restore_roles_settings($restore, $xmlfile) { + function restore_roles_and_filter_settings($restore, $xmlfile) { // data pulls from course, mod, user, and blocks /******************************************************* @@ -9009,11 +9076,12 @@ WHERE $isimport = false; // course restore with role assignments } + $newcoursecontext = restore_get_new_context($restore, 'course', CONTEXT_COURSE, $course->course_id); if (!empty($course->roleassignments) && !$isimport) { $courseassignments = $course->roleassignments; foreach ($courseassignments as $oldroleid => $courseassignment) { - restore_write_roleassignments($restore, $courseassignment->assignments, "course", CONTEXT_COURSE, $course->course_id, $oldroleid); + restore_write_roleassignments($restore, $courseassignment->assignments, $newcoursecontext, $oldroleid); } } /***************************************************** @@ -9026,11 +9094,14 @@ WHERE // if not importing into exiting course, or creating new role, we are ok // local course overrides to be respected (i.e. restored course overrides ignored) if ($restore->restoreto != 1 || empty($restore->rolesmapping[$oldroleid])) { - restore_write_roleoverrides($restore, $courseoverride->overrides, "course", CONTEXT_COURSE, $course->course_id, $oldroleid); + restore_write_roleoverrides($restore, $courseoverride->overrides, $newcoursecontext, $oldroleid); } } } + // Per-context filter settings. + restore_write_local_filter_settings($restore, $course, $newcoursecontext); + /******************************************************* * Restoring role assignments/overrdies * * from module level assignments * @@ -9045,17 +9116,20 @@ WHERE foreach ($secs as $section) { if (isset($section->mods)) { foreach ($section->mods as $modid=>$mod) { + $newmodcontext = restore_get_new_context($restore, 'course_modules', CONTEXT_MODULE, $modid); if (isset($mod->roleassignments) && !$isimport) { foreach ($mod->roleassignments as $oldroleid=>$modassignment) { - restore_write_roleassignments($restore, $modassignment->assignments, "course_modules", CONTEXT_MODULE, $modid, $oldroleid); + restore_write_roleassignments($restore, $modassignment->assignments, $newmodcontext, $oldroleid); } } // role overrides always applies, in import or backup/restore if (isset($mod->roleoverrides)) { foreach ($mod->roleoverrides as $oldroleid=>$modoverride) { - restore_write_roleoverrides($restore, $modoverride->overrides, "course_modules", CONTEXT_MODULE, $modid, $oldroleid); + restore_write_roleoverrides($restore, $modoverride->overrides, $newmodcontext, $oldroleid); } } + // Per-context filter settings. + restore_write_local_filter_settings($restore, $mod, $newmodcontext); } } } @@ -9072,16 +9146,17 @@ WHERE $blocks = restore_read_xml_blocks($restore, $xmlfile); if (isset($blocks->instances)) { foreach ($blocks->instances as $instance) { + $newblockcontext = restore_get_new_context($restore, 'block_instance', CONTEXT_BLOCK, $instance->id); if (isset($instance->roleassignments) && !$isimport) { foreach ($instance->roleassignments as $oldroleid=>$blockassignment) { - restore_write_roleassignments($restore, $blockassignment->assignments, "block_instance", CONTEXT_BLOCK, $instance->id, $oldroleid); + restore_write_roleassignments($restore, $blockassignment->assignments, $newblockcontext, $oldroleid); } } // likewise block overrides should always be restored like mods if (isset($instance->roleoverrides)) { foreach ($instance->roleoverrides as $oldroleid=>$blockoverride) { - restore_write_roleoverrides($restore, $blockoverride->overrides, "block_instance", CONTEXT_BLOCK, $instance->id, $oldroleid); + restore_write_roleoverrides($restore, $blockoverride->overrides, $newblockcontext, $oldroleid); } } } @@ -9099,14 +9174,15 @@ WHERE //For each user, take its info from backup_ids foreach ($info->users as $userid) { $rec = backup_getid($restore->backup_unique_code,"user",$userid); + $newusercontext = restore_get_new_context($restore, 'user', CONTEXT_USER, $userid); if (isset($rec->info->roleassignments)) { foreach ($rec->info->roleassignments as $oldroleid=>$userassignment) { - restore_write_roleassignments($restore, $userassignment->assignments, "user", CONTEXT_USER, $userid, $oldroleid); + restore_write_roleassignments($restore, $userassignment->assignments, $newusercontext, $oldroleid); } } if (isset($rec->info->roleoverrides)) { foreach ($rec->info->roleoverrides as $oldroleid=>$useroverride) { - restore_write_roleoverrides($restore, $useroverride->overrides, "user", CONTEXT_USER, $userid, $oldroleid); + restore_write_roleoverrides($restore, $useroverride->overrides, $newusercontext, $oldroleid); } } } @@ -9115,8 +9191,30 @@ WHERE return true; } + /** + * Get the context object from the site we are restoring to that corresponds + * to a particular context on the site we backed up from. + * @return object the restored context. + */ + function restore_get_new_context($restore, $table, $contextlevel, $oldid) { + // hack to make the correct contextid for course level imports + if ($contextlevel == CONTEXT_COURSE) { + $oldinstance->new_id = $restore->course_id; + } else { + $oldinstance = backup_getid($restore->backup_unique_code,$table,$oldid); + } + + // new instance id not found (not restored module/block/user)... skip any assignment + if (!$oldinstance || empty($oldinstance->new_id)) { + continue; + } + + $newcontext = get_context_instance($contextlevel, $oldinstance->new_id); + return $newcontext; + } + // auxillary function to write role assignments read from xml to db - function restore_write_roleassignments($restore, $assignments, $table, $contextlevel, $oldid, $oldroleid) { + function restore_write_roleassignments($restore, $assignments, $newcontext, $oldroleid) { $role = backup_getid($restore->backup_unique_code, "role", $oldroleid); @@ -9132,19 +9230,6 @@ WHERE $assignment->modifierid = !empty($oldmodifier->new_id) ? $oldmodifier->new_id : 0; // new modifier id here $assignment->roleid = $role->new_id; // restored new role id - // hack to make the correct contextid for course level imports - if ($contextlevel == CONTEXT_COURSE) { - $oldinstance->new_id = $restore->course_id; - } else { - $oldinstance = backup_getid($restore->backup_unique_code,$table,$oldid); - } - - // new instance id not found (not restored module/block/user)... skip any assignment - if (!$oldinstance || empty($oldinstance->new_id)) { - continue; - } - - $newcontext = get_context_instance($contextlevel, $oldinstance->new_id); $assignment->contextid = $newcontext->id; // new context id // might already have same assignment role_assign($assignment->roleid, $assignment->userid, 0, $assignment->contextid, $assignment->timestart, $assignment->timeend, $assignment->hidden, $assignment->enrol, $assignment->timemodified); @@ -9153,7 +9238,7 @@ WHERE } // auxillary function to write role assignments read from xml to db - function restore_write_roleoverrides($restore, $overrides, $table, $contextlevel, $oldid, $oldroleid) { + function restore_write_roleoverrides($restore, $overrides, $newcontext, $oldroleid) { // it is possible to have an override not relevant to this course context. // should be ignored(?) @@ -9167,24 +9252,44 @@ WHERE $override->modifierid = !empty($oldmodifier->new_id)?$oldmodifier->new_id:0; // new modifier id here $override->roleid = $role->new_id; // restored new role id - // hack to make the correct contextid for course level imports - if ($contextlevel == CONTEXT_COURSE) { - $oldinstance->new_id = $restore->course_id; - } else { - $oldinstance = backup_getid($restore->backup_unique_code,$table,$oldid); - } - - // new instance id not found (not restored module/block/user)... skip any override - if (!$oldinstance || empty($oldinstance->new_id)) { - continue; - } - - $newcontext = get_context_instance($contextlevel, $oldinstance->new_id); $override->contextid = $newcontext->id; // new context id // use assign capability instead so we can add context to context_rel assign_capability($override->capability, $override->permission, $override->roleid, $override->contextid); } } + + /** + * Write any per-context filter settings from the backup XML to the DB. + * @param object $restore the restore we are part of. + * @param object $data sata loaded from the XML. + * @param object $newmodcontext the restored context object. + */ + function restore_write_local_filter_settings($restore, $data, $newcontext) { + if (filter_context_may_have_filter_settings($newcontext)) { + return; + } + + $installedfilters = filter_get_all_installed(); + + if (!isset($data->filteractives)) { + $data->filteractives = array(); + } + foreach ($data->filteractives as $filter => $state) { + if (isset($installedfilters[$filter])) { + filter_set_local_state($filter, $newcontext->id, $state); + } + } + + if (!isset($data->filterconfigs)) { + $data->filterconfigs = array(); + } + foreach ($data->filterconfigs as $fc) { + if (isset($installedfilters[$fc->filter])) { + filter_set_local_config($fc->filter, $newcontext->id, $fc->name, $fc->value); + } + } + } + //write activity date changes to the html log file, and update date values in the the xml array function restore_log_date_changes($recordtype, &$restore, &$xml, $TAGS, $NAMETAG='NAME') { diff --git a/blocks/moodleblock.class.php b/blocks/moodleblock.class.php index e6f51ad..c6bb802 100644 --- a/blocks/moodleblock.class.php +++ b/blocks/moodleblock.class.php @@ -380,7 +380,7 @@ class block_base { } //Accesssibility: added H2 (was in, weblib.php: print_side_block) - $title .= '

'.filter_text($this->title).'

'; + $title .= '

'.format_string($this->title).'

'; if ($this->edit_controls !== NULL) { $title .= $this->edit_controls; diff --git a/course/info.php b/course/info.php index 47eebd1..7ced517 100644 --- a/course/info.php +++ b/course/info.php @@ -53,8 +53,7 @@ print_box_start('generalbox info'); - echo filter_text(text_to_html($course->summary),$course->id); - + echo format_text($course->summary, FORMAT_MOODLE, NULL, $course->id); if ($managerroles = get_config('', 'coursemanager')) { $coursemanagerroles = split(',', $managerroles); diff --git a/filter/activitynames/filter.php b/filter/activitynames/filter.php index 89347b8..67923a2 100644 --- a/filter/activitynames/filter.php +++ b/filter/activitynames/filter.php @@ -3,15 +3,11 @@ //activities when its name (title) is found inside every Moodle text //It's based in the glosssary filter by Williams Castillo //Modifications by stronk7. -class activitynames_filter extends filter_base { +class activitynames_filter extends moodle_text_filter { // Trivial-cache - keyed on $cachedcourseid static $activitylist = null; static $cachedcourseid; - function __construct($courseid, $format, $options){ - parent::__construct($courseid, $format, $options); - } - function filter($text) { global $CFG, $COURSE, $DB; diff --git a/filter/algebra/algebradebug.php b/filter/algebra/algebradebug.php index f864454..d6953e2 100644 --- a/filter/algebra/algebradebug.php +++ b/filter/algebra/algebradebug.php @@ -7,13 +7,8 @@ require_once("../../config.php"); - if (empty($CFG->textfilters)) { + if (!filter_is_enabled('filter/algebra')) { error ('Filter not enabled!'); - } else { - $filters = explode(',', $CFG->textfilters); - if (array_search('filter/algebra', $filters) === FALSE) { - error ('Filter not enabled!'); - } } require_once($CFG->libdir.'/filelib.php'); diff --git a/filter/algebra/filter.php b/filter/algebra/filter.php index 1ec66b1..555e8bf 100644 --- a/filter/algebra/filter.php +++ b/filter/algebra/filter.php @@ -84,10 +84,7 @@ function string_file_picture_algebra($imagefile, $tex= "", $height="", $width="" return $output; } -class algebra_filter extends filter_base { - function __construct($courseid, $format, $options){ - parent::__construct($courseid, $format, $options); - } +class algebra_filter extends moodle_text_filter { function filter($text){ global $CFG, $DB; diff --git a/filter/algebra/pix.php b/filter/algebra/pix.php index b572a4c..eeb326d 100644 --- a/filter/algebra/pix.php +++ b/filter/algebra/pix.php @@ -7,13 +7,8 @@ require_once('../../config.php'); - if (empty($CFG->textfilters)) { + if (!filter_is_enabled('filter/algebra')) { error ('Filter not enabled!'); - } else { - $filters = explode(',', $CFG->textfilters); - if (array_search('filter/algebra', $filters) === FALSE) { - error ('Filter not enabled!'); - } } // disable moodle specific debug messages diff --git a/filter/censor/filter.php b/filter/censor/filter.php index db06e15..28c7c37 100644 --- a/filter/censor/filter.php +++ b/filter/censor/filter.php @@ -11,10 +11,7 @@ /// This is the filtering class. It accepts the courseid and /// options to be filtered (In HTML form). -class censor_filter extends filter_base { - function __construct($courseid, $format, $options) { - parent::__construct($courseid, $format, $options); - } +class censor_filter extends moodle_text_filter { private function _canseecensor() { $cansee = false; $context = get_context_instance(CONTEXT_SYSTEM, SITEID); diff --git a/filter/emailprotect/filter.php b/filter/emailprotect/filter.php index 02b1f4e..9427b5e 100644 --- a/filter/emailprotect/filter.php +++ b/filter/emailprotect/filter.php @@ -3,15 +3,8 @@ // hides them using the Moodle obfuscate_text function. // Original code by Mike Churchward -class emailprotect_filter extends filter_base { - function __construct($courseid, $format, $options) { - parent::__construct($courseid, $format, $options); - } +class emailprotect_filter extends moodle_text_filter { function filter($text) { - if (!empty($CFG->formatstring)) { - return $text; - } - /// Do a quick check using stripos to avoid unnecessary work if (strpos($text, '@') === false) { return $text; diff --git a/filter/local_settings_form.php b/filter/local_settings_form.php new file mode 100644 index 0000000..08a199b --- /dev/null +++ b/filter/local_settings_form.php @@ -0,0 +1,91 @@ +libdir . '/formslib.php'); + +abstract class filter_local_settings_form extends moodleform { + protected $filter; + protected $context; + + public function __construct($submiturl, $filter, $context) { + $this->filter = $filter; + $this->context = $context; + parent::moodleform($submiturl); + } + + /** + * Build the form definition. Rather than overriding this method, you + * should probably override definition_inner instead. + * + * This method adds the necessary hidden fields and submit buttons, + * and calls definition_inner to insert the custom controls in the appropriate place. + */ + public function definition() { + $mform =& $this->_form; + + $this->definition_inner($mform); + + $mform->addElement('hidden', 'contextid'); + $mform->setType('contextid', PARAM_INT); + $mform->setDefault('contextid', $this->context->id); + + $mform->addElement('hidden', 'filter'); + $mform->setType('filter', PARAM_ALPHAEXT); + $mform->setDefault('filter', $this->filter); + + $this->add_action_buttons(); + } + + /** + * Override this method to add your form controls. + * @param $mform the form we are building. $this->_form, but passed in for convinience. + */ + abstract protected function definition_inner($mform); + + /** + * Override this method to save the settings to the database. The default + * implementation will probalby be sufficient for most simple cases. + * @param object $data the form data that was submitted. + */ + public function save_changes($data) { + $data = (array) $data; + unset($data['filter']); + unset($data['contextid']); + foreach ($data as $name => $value) { + if ($value !== '') { + filter_set_local_config($this->filter, $this->context->id, $name, $value); + } else { + filter_unset_local_config($this->filter, $this->context->id, $name); + } + } + } +} +?> \ No newline at end of file diff --git a/filter/manage.php b/filter/manage.php new file mode 100755 index 0000000..6febdc5 --- /dev/null +++ b/filter/manage.php @@ -0,0 +1,213 @@ +libdir . '/adminlib.php'); + +$contextid = required_param('contextid',PARAM_INT); +$forfilter = optional_param('filter', '', PARAM_SAFEPATH); + +if (!$context = get_context_instance_by_id($contextid)) { + print_error('wrongcontextid', 'error'); +} + +// This is a policy decision, rather than something that would be impossible to implement. +if (!in_array($context->contextlevel, array(CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE))) { + print_error('cannotcustomisefiltersblockuser', 'error'); +} + +$isfrontpage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID; +$contextname = print_context_name($context); +$baseurl = $CFG->wwwroot . '/filter/manage.php?contextid=' . $context->id; + +if ($context->contextlevel == CONTEXT_COURSECAT) { + $course = clone($SITE); +} else if ($context->contextlevel == CONTEXT_COURSE) { + $course = $DB->get_record('course', array('id' => $context->instanceid)); +} else { + // Must be module context. + $course = $DB->get_record_sql('SELECT c.* FROM {course} c JOIN {context} ctx ON c.id = ctx.instanceid WHERE ctx.id = ?', + array(get_parent_contextid($context))); +} +if (!$course) { + print_error('invalidcourse', 'error'); +} + +/// Check login and permissions. +require_login($course); +require_capability('moodle/filter:manage', $context); + +/// Get the list of available filters. +$availablefilters = filter_get_available_in_context($context); +if (!$isfrontpage && empty($availablefilters)) { + print_error('nofiltersenabled', 'error'); +} + +// If we are handling local settings for a particular filter, start processing. +if ($forfilter) { + if (!filter_has_local_settings($forfilter)) { + print_error('filterdoesnothavelocalconfig', 'error', $forfilter); + } + require_once($CFG->dirroot . '/filter/local_settings_form.php'); + require_once($CFG->dirroot . '/' . $forfilter . '/filterlocalsettings.php'); + $formname = basename($forfilter) . '_filter_local_settings_form'; + $settingsform = new $formname($CFG->wwwroot . '/filter/manage.php', $forfilter, $context); + if ($settingsform->is_cancelled()) { + redirect($baseurl); + } else if ($data = $settingsform->get_data()) { + $settingsform->save_changes($data); + redirect($baseurl); + } +} + +/// Process any form submission. +if ($forfilter == '' && optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) { + foreach ($availablefilters as $filter => $filterinfo) { + $newstate = optional_param(str_replace('/', '_', $filter), false, PARAM_INT); + if ($newstate !== false && $newstate != $filterinfo->localstate) { + filter_set_local_state($filter, $context->id, $newstate); + } + } + redirect($CFG->wwwroot . '/filter/manage.php?contextid=' . $context->id, get_string('changessaved'), 1); +} + +/// These are needed early because of tabs.php +$assignableroles = get_assignable_roles($context, ROLENAME_BOTH); +$overridableroles = get_overridable_roles($context, ROLENAME_BOTH); + +/// Work out an appropriate page title. +if ($forfilter) { + $a = new stdClass; + $a->filter = filter_get_name($forfilter); + $a->context = $contextname; + $title = get_string('filtersettingsforin', 'filters', $a); +} else { + $title = get_string('filtersettingsin', 'filters', $contextname); +} +$straction = get_string('filters', 'admin'); // Used by tabs.php + +/// Print the header and tabs +if ($context->contextlevel == CONTEXT_COURSE and $context->instanceid == SITEID) { + admin_externalpage_setup('frontpagefilters'); + admin_externalpage_print_header(); +} else { + $currenttab = 'filters'; + include_once($CFG->dirroot . '/' . $CFG->admin . '/roles/tabs.php'); +} + +/// Print heading. +print_heading_with_help($title, 'localfiltersettings'); + +if (empty($availablefilters)) { + echo '

' . get_string('nofiltersenabled', 'filters') . "

\n"; +} else if ($forfilter) { + $current = filter_get_local_config($forfilter, $contextid); + $settingsform->set_data((object) $current); + $settingsform->display(); +} else { + $settingscol = false; + foreach ($availablefilters as $filter => $notused) { + $hassettings = filter_has_local_settings($filter); + $availablefilters[$filter]->hassettings = $hassettings; + $settingscol = $settingscol || $hassettings; + } + + $strsettings = get_string('settings'); + $stroff = get_string('off', 'filters'); + $stron = get_string('on', 'filters'); + $strdefaultoff = get_string('defaultx', 'filters', $stroff); + $strdefaulton = get_string('defaultx', 'filters', $stron); + $activechoices = array( + TEXTFILTER_INHERIT => '', + TEXTFILTER_OFF => $stroff, + TEXTFILTER_ON => $stron, + ); + + echo '
'; + echo "\n
\n"; + echo ''; + + $table = new stdClass; + $table->head = array(get_string('filter'), get_string('isactive', 'filters')); + $table->align = array('left', 'left'); + if ($settingscol) { + $table->head[] = $strsettings; + $table->align[] = 'left'; + } + $table->width = ' '; + $table->data = array(); + + // iterate through filters adding to display table + foreach ($availablefilters as $filter => $filterinfo) { + $row = array(); + + // Filter name. + $row[] = filter_get_name($filter); + + // Default/on/off choice. + if ($filterinfo->inheritedstate == TEXTFILTER_ON) { + $activechoices[TEXTFILTER_INHERIT] = $strdefaulton; + } else { + $activechoices[TEXTFILTER_INHERIT] = $strdefaultoff; + } + $row[] = choose_from_menu($activechoices, str_replace('/', '_', $filter), + $filterinfo->localstate, '', '', '', true); + + // Settings link, if required + if ($settingscol) { + $settings = ''; + if ($filterinfo->hassettings) { + $settings = '' . $strsettings . ''; + } + $row[] = $settings; + } + + $table->data[] = $row; + } + + print_table($table); + echo '
' . "\n"; + echo ''; + echo "\n
\n"; + echo "
\n"; + echo "
\n"; + +} + +/// Appropriate back link. +if (!$isfrontpage && ($url = get_context_url($context))) { + echo ''; +} + +print_footer($course); +?> diff --git a/filter/mediaplugin/filter.php b/filter/mediaplugin/filter.php index 5086fd6..f42f8f7 100644 --- a/filter/mediaplugin/filter.php +++ b/filter/mediaplugin/filter.php @@ -17,12 +17,8 @@ require_once($CFG->libdir.'/filelib.php'); -class mediaplugin_filter extends filter_base { - private $eolas_fix_applied; - function __construct($courseid, $format, $options) { - parent::__construct($courseid, $format, $options); - $this->eolas_fix_applied = false; - } +class mediaplugin_filter extends moodle_text_filter { + private $eolas_fix_applied = false; function filter($text) { global $CFG; // You should never modify parameters passed to a method or function, it's BAD practice. Create a copy instead. diff --git a/filter/multilang/filter.php b/filter/multilang/filter.php index 2f5eb0c..3aa23c1 100644 --- a/filter/multilang/filter.php +++ b/filter/multilang/filter.php @@ -36,11 +36,7 @@ // Following new syntax is not compatible with old one: // one langanother language -class multilang_filter extends filter_base { - function __construct($courseid, $format, $options) { - parent::__construct($courseid, $format, $options); - } - +class multilang_filter extends moodle_text_filter { function filter($text) { global $CFG; diff --git a/filter/tex/filter.php b/filter/tex/filter.php index 01ee633..c113ec6 100644 --- a/filter/tex/filter.php +++ b/filter/tex/filter.php @@ -101,10 +101,7 @@ function string_file_picture_tex($imagefile, $tex= "", $height="", $width="", $a return $output; } -class tex_filter extends filter_base { - function __construct($courseid, $format, $options) { - parent::__construct($courseid, $format, $options); - } +class tex_filter extends moodle_text_filter { function filter ($text) { global $CFG, $DB; diff --git a/filter/tex/pix.php b/filter/tex/pix.php index 6c48542..e7240b5 100644 --- a/filter/tex/pix.php +++ b/filter/tex/pix.php @@ -7,13 +7,8 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching require_once('../../config.php'); - if (empty($CFG->textfilters)) { + if (!filter_is_enabled('filter/tex')) { error ('Filter not enabled!'); - } else { - $filters = explode(',', $CFG->textfilters); - if (array_search('filter/tex', $filters) === FALSE) { - error ('Filter not enabled!'); - } } // disable moodle specific debug messages diff --git a/filter/tex/texdebug.php b/filter/tex/texdebug.php index 917239c..0940b41 100644 --- a/filter/tex/texdebug.php +++ b/filter/tex/texdebug.php @@ -5,13 +5,8 @@ require_once("../../config.php"); - if (empty($CFG->textfilters)) { + if (!filter_is_enabled('filter/tex')) { error ('Filter not enabled!'); - } else { - $filters = explode(',', $CFG->textfilters); - if (array_search('filter/tex', $filters) === FALSE) { - error ('Filter not enabled!'); - } } require_once($CFG->libdir.'/filelib.php'); diff --git a/filter/tex/texed.php b/filter/tex/texed.php index 3ac929a..d246192 100644 --- a/filter/tex/texed.php +++ b/filter/tex/texed.php @@ -8,13 +8,8 @@ require_once("../../config.php"); require_once($CFG->dirroot.'/filter/tex/lib.php'); - if (empty($CFG->textfilters)) { + if (!filter_is_enabled('filter/tex')) { error ('Filter not enabled!'); - } else { - $filters = explode(',', $CFG->textfilters); - if (array_search('filter/tex', $filters) === FALSE) { - error ('Filter not enabled!'); - } } error_reporting(E_ALL); diff --git a/filter/tidy/filter.php b/filter/tidy/filter.php index bb6893d..53d1f2d 100644 --- a/filter/tidy/filter.php +++ b/filter/tidy/filter.php @@ -11,11 +11,7 @@ // If you want to know what you can set in $tidyoptions and what their default // values are, see http://php.net/manual/en/function.tidy-get-config.php. -class tidy_filter extends filter_base { - function __construct($courseid, $format, $options) { - parent::__construct($courseid, $format, $options); - } - +class tidy_filter extends moodle_text_filter { /** * @author Hannes Gassert * @param string text to be filtered diff --git a/lang/en_utf8/admin.php b/lang/en_utf8/admin.php index da31d7e..86c18d2 100644 --- a/lang/en_utf8/admin.php +++ b/lang/en_utf8/admin.php @@ -46,6 +46,7 @@ $string['calendar_weekend'] = 'Weekend Days'; $string['calendarexportsalt'] = 'Calendar export salt'; $string['calendarsettings'] = 'Calendar'; $string['cannotdeletemissingqtype'] = 'You cannot delete the missing question type. It is needed by the system.'; +$string['cannotdeletemodfilter'] = 'You cannot uninstall the \'$a->filter\' because it is part of the \'$a->module\' module.'; $string['cannotdeleteqtypeinuse'] = 'You cannot delete the question type \'$a\'. There are questions of this type in the question bank.'; $string['cannotdeleteqtypeneeded'] = 'You cannot delete the question type \'$a\'. There are other question types installed that rely on it.'; $string['cfgwwwrootwarning'] = 'You have defined $CFG->wwwroot incorrectly in your config.php file. It does not match the URL you are using to access this page. Please correct it, or you will experience strange bugs like MDL-11061.'; @@ -334,11 +335,15 @@ $string['defaultsettinginfo'] = 'Default: $a'; $string['defaultuserroleid'] = 'Default role for all users'; $string['defaultvalues'] = 'Default values'; $string['deleteerrors'] = 'Delete errors'; +$string['deletefilterareyousure'] = 'Are you sure you want to delete the filter \'$a\''; +$string['deletefilterareyousuremessage'] = 'You are about to completely delete the filter \'$a\'. Are you sure you want to uninstall it?'; +$string['deletefilterfiles'] = 'All data associated with the filter \'$a->filter\' has been deleted from the database. To complete the deletion (and to prevent the filter from re-installing itself), you should now delete this directory from your server: $a->directory'; $string['deleteincompleteusers'] = 'Delete incomplete users after'; $string['deleteqtypeareyousure'] = 'Are you sure you want to delete the question type \'$a\''; $string['deleteqtypeareyousuremessage'] = 'You are about to completely delete the question type \'$a\'. Are you sure you want to uninstall it?'; $string['deleteunconfirmed'] = 'Delete not fully setup users after'; $string['deleteuser'] = 'Delete user'; +$string['deletingfilter'] = 'Deleting filter \'$a\''; $string['deletingqtype'] = 'Deleting question type \'$a\''; $string['density'] = 'Density'; $string['denyemailaddresses'] = 'Denied email domains'; @@ -417,6 +422,7 @@ $string['filestoredinhelp'] = 'Where the file will be stored'; $string['filterall'] = 'Filter all strings'; $string['filtermatchoneperpage'] = 'Filter match once per page'; $string['filtermatchonepertext'] = 'Filter match once per text'; +$string['filters'] = 'Filters'; $string['filtersettings'] = 'Manage filters'; $string['filtersettingsgeneral'] = 'General filter settings'; $string['filteruploadedfiles'] = 'Filter uploaded files'; @@ -427,6 +433,7 @@ $string['framename'] = 'Frame name'; $string['frontpage'] = 'Front Page'; $string['frontpagebackup'] = 'Front Page backup'; $string['frontpagedefaultrole'] = 'Default frontpage role'; +$string['frontpagefilters'] = 'Front page filters'; $string['frontpageloggedin'] = 'Front page items when logged in'; $string['frontpagequestions'] = 'Front Page questions'; $string['frontpageoverrides'] = 'Front Page permission overrides'; diff --git a/lang/en_utf8/error.php b/lang/en_utf8/error.php index 023f984..52f7d89 100644 --- a/lang/en_utf8/error.php +++ b/lang/en_utf8/error.php @@ -40,6 +40,7 @@ $string['cannotcreateuploaddir'] = 'Cannot create upload folder. The site admini $string['cannotcreateuser'] = 'Error creating user record'; $string['cannotcreateorfindstructs'] = 'Error finding or creating section structures for this course'; $string['cannotcreatepopupwin'] = 'Undefined element - cannot create pop-up window'; +$string['cannotcustomisefiltersblockuser'] = 'You cannot customise filters settings in user or block contexts.'; $string['cannotcustomizelocallang'] = 'You do not have permission to customize the strings translation. This permission is controlled by the capability \"moodle/site:langeditlocal\". Set this capability to allow you to edit local language packages in case you want to modify translations for your site.'; $string['cannotdeletelangcache'] = 'Language cache cannot be deleted, please fix permissions in dataroot/cache/languages!'; $string['cannotdeletebackupids'] = 'Couldn\'t delete previous backup ids'; @@ -221,6 +222,7 @@ $string['fieldrequired'] = '\"$a\" is a required field'; $string['filenotfound'] = 'Sorry, the requested file could not be found'; $string['filenotreadable'] = 'File is not readable'; $string['filemismatch'] = 'Non-core file name mismatch. The file \"$a->current\" should be $a->file'; +$string['filterdoesnothavelocalconfig'] = 'The filter $a does not allow local configuration.'; $string['filternotinstalled'] = 'Filter $a is not currently installed'; $string['filternotactive'] = 'Filter $a is not currently active'; $string['forumblockingtoomanyposts'] = 'You have exceeded the posting threshold set for this forum'; @@ -347,6 +349,7 @@ $string['noguest'] = 'No guests here!'; $string['nologinas'] = 'You are not allowed to login as that user'; $string['noadmins'] = 'No administrators!'; $string['noexistingcategory'] = 'No existing category'; +$string['nofiltersenabled'] = 'No filters are enabled.'; $string['notlocalisederrormessage'] = '$a'; $string['nousers'] = 'No such user!'; $string['nonmeaningfulcontent'] = 'Non meaningful content'; diff --git a/lang/en_utf8/filters.php b/lang/en_utf8/filters.php index 3608dd6..e7c91d9 100644 --- a/lang/en_utf8/filters.php +++ b/lang/en_utf8/filters.php @@ -6,8 +6,11 @@ $string['anycourse'] = 'any course'; $string['anyfield'] = 'any field'; $string['anyrole'] = 'any role'; $string['anyvalue'] = 'any value'; +$string['applyto'] = 'Apply to'; $string['categoryrole'] = 'Category role'; $string['tablenosave'] = 'Changes in table above are saved automatically.'; +$string['content'] = 'Content'; +$string['contentandheadings'] = 'Content and headings'; $string['contains'] = 'contains'; $string['courserole'] = 'Course role'; $string['courserolelabel'] = '$a->label is $a->rolename in $a->coursename from $a->categoryname'; @@ -15,10 +18,16 @@ $string['courserolelabelerror'] = '$a->label error: course $a->coursename does n $string['datelabelisafter'] = '$a->label is after $a->after'; $string['datelabelisbefore'] = '$a->label is before $a->before'; $string['datelabelisbetween'] = '$a->label is between $a->after and $a->before'; +$string['defaultx'] = 'Default ($a)'; +$string['disabled'] = 'Disabled'; $string['doesnotcontain'] = 'doesn\'t contain'; $string['endswith'] = 'ends with'; +$string['filterallwarning'] = 'Applying filters to headings as well as content can greatly increase the load on your server. Please use that \'Apply to\' settings sparingly. The main use is with the multilang filter.'; +$string['filtersettingsforin'] = 'Filter settings for $a->filter in $a->context'; +$string['filtersettingsin'] = 'Filter settings in $a'; $string['firstaccess'] = 'First access'; $string['globalrolelabel'] = '$a->label is $a->value'; +$string['isactive'] = 'Active?'; $string['isanyvalue'] = 'is any value'; $string['isafter'] = 'is after'; $string['isbefore'] = 'is before'; @@ -28,6 +37,10 @@ $string['isequalto'] = 'is equal to'; $string['isnotequalto'] = 'isn\'t equal to'; $string['isnotdefined'] = 'isn\'t defined'; $string['newfilter'] = 'New filter'; +$string['nofiltersenabled'] = 'No filter plugins have been enabled on this site.'; +$string['off'] = 'Off'; +$string['offbutavailable'] = 'Off, but available'; +$string['on'] = 'On'; $string['profilelabel'] = '$a->label: $a->profile $a->operator $a->value'; $string['profilelabelnovalue'] = '$a->label: $a->profile $a->operator'; $string['removeall'] = 'Remove all filters'; diff --git a/lang/en_utf8/help/localfiltersettings.html b/lang/en_utf8/help/localfiltersettings.html new file mode 100644 index 0000000..175a7f2 --- /dev/null +++ b/lang/en_utf8/help/localfiltersettings.html @@ -0,0 +1,8 @@ +

Filter settings

+ +

This page lets you turn filters on or off in a particular part of the site. +This page only gives you access to those filters that the Administrator has +enabled.

+ +

Some filters may also let you set local settings, in which case there will be +a 'Settings' link next to their name.

\ No newline at end of file diff --git a/lang/en_utf8/role.php b/lang/en_utf8/role.php index 71e8e6c..83ec50a 100644 --- a/lang/en_utf8/role.php +++ b/lang/en_utf8/role.php @@ -103,6 +103,7 @@ $string['explainpermissionsinfo'] = '

To use this table:

  1. First look $string['explainpermissionsdoanything'] = 'Note that this user has the moodle/site:doanything capability, so even though the table above shows that has_capability will return false, this user will actually be deemed to have the capability $a in most circumstances.'; $string['extusers'] = 'Existing users'; $string['extusersmatching'] = 'Existing users matching \'$a\''; +$string['filter:manage'] = 'Manage local filter settings'; $string['globalrole'] = 'System role'; $string['globalroleswarning'] = 'WARNING! Any roles you assign from this page will apply to the assigned users throughout the entire system, including the front page and all the courses.'; $string['gotoassignroles'] = 'Go to Assign roles for this $a->contextlevel'; diff --git a/lib/adminlib.php b/lib/adminlib.php index f771db5..d3d90e8 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -3711,7 +3711,8 @@ class admin_setting_managefilters extends admin_setting { } public function write_setting($data) { - // do not write any setting + // do not write any settings. Instead all our UI submits to admin/filters.php + // which makes and changes, then redirects back. return ''; } @@ -3734,121 +3735,131 @@ class admin_setting_managefilters extends admin_setting { return false; } - public function output_html($data, $query='') { + protected function action_url($filterpath, $action) { global $CFG; + return $CFG->wwwroot . '/' . $CFG->admin . '/filters.php?sesskey=' . sesskey() . + '&filterpath=' . urlencode($filterpath) . '&action=' . $action; + } - $strname = get_string('name'); - $strhide = get_string('disable'); - $strshow = get_string('enable'); - $strhideshow = "$strhide/$strshow"; - $strsettings = get_string('settings'); - $strup = get_string('up'); - $strdown = get_string('down'); - $strupdown = "$strup/$strdown"; - - // get a list of possible filters (and translate name if possible) - // note filters can be in the dedicated filters area OR in their - // associated modules - $installedfilters = filter_get_all_installed(); - $filtersettings_new = array(); - foreach ($installedfilters as $path => $strfiltername) { - $settingspath_new = $CFG->dirroot . '/' . $path . '/filtersettings.php'; - if (is_readable($settingspath_new)) { - $filtersettings_new[] = $path; + protected function action_icon($url, $icon, $straction) { + global $CFG; + return '' . + '' . $straction . ' '; + } + + protected function get_table_row($filterinfo, $isfirstrow, $islastactive, $applytostrings) { + global $CFG; + $row = array(); + $filter = $filterinfo->filter; + + // Filter name + $row[] = $this->filternames[$filter]; + + // Disable/off/on + $row[] = popup_form($this->action_url($filter, 'setstate') . '&newstate=', $this->activechoices, + 'active' . basename($filter), $filterinfo->active, '', '', '', true, 'self', '', NULL, get_string('save')); + + // Re-order + $updown = ''; + $spacer = ' '; + if ($filterinfo->active != TEXTFILTER_DISABLED) { + if (!$isfirstrow) { + $updown .= $this->action_icon($this->action_url($filter, 'up'), 'up', $this->strup); + } else { + $updown .= $spacer; + } + if (!$islastactive) { + $updown .= $this->action_icon($this->action_url($filter, 'down'), 'down', $this->strdown); + } else { + $updown .= $spacer; } } + $row[] = $updown; - // get all the currently selected filters - if (!empty($CFG->textfilters)) { - $oldactivefilters = explode(',', $CFG->textfilters); - $oldactivefilters = array_unique($oldactivefilters); - } else { - $oldactivefilters = array(); - } + // Apply to strings. + $row[] = popup_form($this->action_url($filter, 'setapplyto') . '&stringstoo=', $this->applytochoices, + 'applyto' . basename($filter), $applytostrings, '', '', '', true, 'self', '', NULL, get_string('save'), + $filterinfo->active == TEXTFILTER_DISABLED); - // take this opportunity to clean up filters - $activefilters = array(); - foreach ($oldactivefilters as $oldactivefilter) { - if (!empty($oldactivefilter) and array_key_exists($oldactivefilter, $installedfilters)) { - $activefilters[] = $oldactivefilter; - } + // Settings link, if required + $settings = ''; + if (filter_has_global_settings($filter)) { + $settings = '' . $this->strsettings . ''; } + $row[] = $settings; - // Get the list of all filters, and pull the active filters - // to the top. - $displayfilters = array(); - foreach ($activefilters as $activefilter) { - $name = $installedfilters[$activefilter]; - $displayfilters[$activefilter] = $name; - } - foreach ($installedfilters as $key => $filter) { - if (!array_key_exists($key, $displayfilters)) { - $displayfilters[$key] = $filter; - } + return $row; + } + + public function output_html($data, $query='') { + global $CFG; + + $this->activechoices = array( + TEXTFILTER_DISABLED => get_string('disabled', 'filters'), + TEXTFILTER_OFF => get_string('offbutavailable', 'filters'), + TEXTFILTER_ON => get_string('on', 'filters'), + ); + $this->applytochoices = array( + 0 => get_string('content', 'filters'), + 1 => get_string('contentandheadings', 'filters'), + ); + $this->strup = get_string('up'); + $this->strdown = get_string('down'); + $this->strsettings = get_string('settings'); + + $filters = filter_get_global_states(); + + // In case any new filters have been installed, but not put in the table yet. + $this->filternames = filter_get_all_installed(); + $newfilters = $this->filternames; + foreach ($filters as $filter => $notused) { + unset($newfilters[$filter]); } + $stringfilters = filter_get_string_filters(); $return = print_heading(get_string('actfilterhdr', 'filters'), '', 3, 'main', true); $return .= print_box_start('generalbox filtersui', '', true); $table = new object(); - $table->head = array($strname, $strhideshow, $strupdown, $strsettings); - $table->align = array('left', 'center', 'center', 'center'); - $table->width = '90%'; + $table->head = array(get_string('filter'), get_string('isactive', 'filters'), + get_string('order'), get_string('applyto', 'filters'), $this->strsettings); + $table->align = array('left', 'left', 'center', 'left', 'left'); + $table->width = '100%'; $table->data = array(); - $filtersurl = "$CFG->wwwroot/$CFG->admin/filters.php?sesskey=".sesskey(); - $imgurl = "$CFG->pixpath/t"; - - // iterate through filters adding to display table - $updowncount = 1; - $activefilterscount = count($activefilters); - foreach ($displayfilters as $path => $name) { - $upath = urlencode($path); - // get hide/show link - if (in_array($path, $activefilters)) { - $hideshow = ""; - $hideshow .= "pixpath}/i/hide.gif\" class=\"icon\" alt=\"$strhide\" />"; - $hidden = false; - $displayname = "$name"; - } - else { - $hideshow = ""; - $hideshow .= "pixpath}/i/show.gif\" class=\"icon\" alt=\"$strshow\" />"; - $hidden = true; - $displayname = "$name"; - } - - // get up/down link (only if not hidden) - $updown = ''; - if (!$hidden) { - if ($updowncount>1) { - $updown .= ""; - $updown .= "\"$strup\" "; - } - else { - $updown .= "pixpath/spacer.gif\" class=\"icon\" alt=\"\" /> "; - } - if ($updowncount<$activefilterscount) { - $updown .= ""; - $updown .= "\"$strdown\""; - } - else { - $updown .= "pixpath/spacer.gif\" class=\"icon\" alt=\"\" />"; - } - ++$updowncount; + $lastactive = null; + foreach ($filters as $filter => $filterinfo) { + if ($filterinfo->active != TEXTFILTER_DISABLED) { + $lastactive = $filter; } + } - // settings link (if defined) - $settings = ''; - if (in_array($path, $filtersettings_new)) { - $settings = "$strsettings"; + // iterate through filters adding to display table + $firstrow = true; + foreach ($filters as $filter => $filterinfo) { + $applytostrings = isset($stringfilters[$filter]) && $filterinfo->active != TEXTFILTER_DISABLED; + $row = $this->get_table_row($filterinfo, $firstrow, $filter == $lastactive, $applytostrings); + $table->data[] = $row; + if ($filterinfo->active == TEXTFILTER_DISABLED) { + $table->rowclass[] = 'dimmed_text'; + } else { + $table->rowclass[] = ''; } - - // write data into the table object - $table->data[] = array($displayname, $hideshow, $updown, $settings); + $firstrow = false; + } + foreach ($newfilters as $filter => $filtername) { + $filterinfo = new stdClass; + $filterinfo->filter = $filter; + $filterinfo->active = TEXTFILTER_DISABLED; + $row = $this->get_table_row($filterinfo, false, false, false); + $table->data[] = $row; + $table->rowclass[] = 'dimmed_text'; } + $return .= print_table($table, true); - $return .= get_string('tablenosave', 'filters'); + $return .= '

    ' . get_string('tablenosave', 'filters') . '

    '; + $return .= '

    ' . get_string('filterallwarning', 'filters') . '

    '; $return .= print_box_end(true); return highlight($query, $return); } diff --git a/lib/db/access.php b/lib/db/access.php index 341c499..292e34c 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -300,6 +300,17 @@ $moodle_capabilities = array( ) ), + // Permission to manage filter setting overrides in subcontexts. + 'moodle/filter:manage' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'legacy' => array( + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'admin' => CAP_ALLOW, + ) + ), 'moodle/user:create' => array( diff --git a/lib/db/install.php b/lib/db/install.php index d703cad..691bf0f 100644 --- a/lib/db/install.php +++ b/lib/db/install.php @@ -41,24 +41,25 @@ function xmldb_main_install() { /// create default course category $cat = get_course_category(); - - $defaults = array('rolesactive' => '0', // marks fully set up system - 'auth' => 'email', - 'auth_pop3mailbox' => 'INBOX', - 'enrol' => 'manual', - 'enrol_plugins_enabled' => 'manual', - 'style' => 'default', - 'template' => 'default', - 'theme' => 'standardwhite', - 'filter_multilang_converted' => 1, - 'siteidentifier' => random_string(32).$_SERVER['HTTP_HOST'], - 'backup_version' => 2008111700, - 'backup_release' => '2.0 dev', - 'blocks_version' => 2007081300, // might be removed soon - 'mnet_dispatcher_mode' => 'off', - 'sessiontimeout' => 7200, // must be present during roles installation - - ); + $defaults = array( + 'rolesactive' => '0', // marks fully set up system + 'auth' => 'email', + 'auth_pop3mailbox' => 'INBOX', + 'enrol' => 'manual', + 'enrol_plugins_enabled' => 'manual', + 'style' => 'default', + 'template' => 'default', + 'theme' => 'standardwhite', + 'filter_multilang_converted' => 1, + 'siteidentifier' => random_string(32).$_SERVER['HTTP_HOST'], + 'backup_version' => 2008111700, + 'backup_release' => '2.0 dev', + 'blocks_version' => 2007081300, // might be removed soon + 'mnet_dispatcher_mode' => 'off', + 'sessiontimeout' => 7200, // must be present during roles installation + 'stringfilters' => '', // These two are managed in a strange way by the filters + 'filterall' => 0, // setting page, so have to be initialised here. + ); foreach($defaults as $key => $value) { set_config($key, $value); } diff --git a/lib/db/install.xml b/lib/db/install.xml index 4f65217..afe17d3 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -270,7 +270,7 @@ - +
    @@ -284,7 +284,39 @@
    - +
    + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 4944439..a9bd56d 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1524,6 +1524,7 @@ WHERE gradeitemid IS NOT NULL AND grademax IS NOT NULL"); /// Main savepoint reached upgrade_main_savepoint($result, 2009032000); } + if ($result && $oldversion < 2009032001) { /// Copy from role_allow_assign into the new table. $DB->execute('INSERT INTO {role_allow_switch} SELECT * FROM {role_allow_assign}'); @@ -1542,6 +1543,103 @@ WHERE gradeitemid IS NOT NULL AND grademax IS NOT NULL"); upgrade_main_savepoint($result, 2009033100); } + if ($result && $oldversion < 2009040300) { + + /// Define table filter_active to be created + $table = new xmldb_table('filter_active'); + + /// Adding fields to table filter_active + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); + $table->add_field('filter', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('active', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, null, '0'); + + /// Adding keys to table filter_active + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id')); + + /// Adding indexes to table filter_active + $table->add_index('contextid-filter', XMLDB_INDEX_UNIQUE, array('contextid', 'filter')); + + /// Conditionally launch create table for filter_active + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + /// Main savepoint reached + upgrade_main_savepoint($result, 2009040300); + } + + if ($result && $oldversion < 2009040301) { + + /// Define table filter_config to be created + $table = new xmldb_table('filter_config'); + + /// Adding fields to table filter_config + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); + $table->add_field('filter', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, null, null); + $table->add_field('value', XMLDB_TYPE_TEXT, 'small', null, null, null, null, null, null); + + /// Adding keys to table filter_config + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id')); + + /// Adding indexes to table filter_config + $table->add_index('contextid-filter-name', XMLDB_INDEX_UNIQUE, array('contextid', 'filter', 'name')); + + /// Conditionally launch create table for filter_config + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + /// Main savepoint reached + upgrade_main_savepoint($result, 2009040301); + } + + if ($result && $oldversion < 2009040302) { + /// Transfer current settings from $CFG->textfilters + $disabledfilters = filter_get_all_installed(); + if (empty($CFG->textfilters)) { + $activefilters = array(); + } else { + $activefilters = explode(',', $CFG->textfilters); + } + $syscontext = get_context_instance(CONTEXT_SYSTEM); + $sortorder = 1; + foreach ($activefilters as $filter) { + filter_set_global_state($filter, TEXTFILTER_ON, $sortorder); + $sortorder += 1; + unset($disabledfilters[$filter]); + } + foreach ($disabledfilters as $filter => $notused) { + filter_set_global_state($filter, TEXTFILTER_DISABLED, $sortorder); + $sortorder += 1; + } + + /// Main savepoint reached + upgrade_main_savepoint($result, 2009040302); + } + + if ($result && $oldversion < 2009040600) { + /// Ensure that $CFG->stringfilters is set. + if (empty($CFG->stringfilters)) { + if (!empty($CFG->filterall)) { + set_config('stringfilters', $CFG->textfilters); + } else { + set_config('stringfilters', ''); + } + } + + set_config('filterall', !empty($CFG->stringfilters)); + unset_config('textfilters'); + + /// Main savepoint reached + upgrade_main_savepoint($result, 2009040600); + } + return $result; } diff --git a/lib/filelib.php b/lib/filelib.php index 4050407..0d83c78 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -1529,12 +1529,10 @@ function file_modify_html_header($text) { $stylesheetshtml .= ''."\n"; } - $filters = explode(",", $CFG->textfilters); - if (in_array('filter/mediaplugin', $filters)) { + $ufo = ''; + if (filter_is_enabled('filter/mediaplugin')) { // this script is needed by most media filter plugins. $ufo = get_require_js_code(array($CFG->wwwroot . '/lib/ufo.js')); - } else { - $ufo = ''; } preg_match('/\|\/', $text, $matches); diff --git a/lib/filterlib.php b/lib/filterlib.php index 74f203f..c9de713 100644 --- a/lib/filterlib.php +++ b/lib/filterlib.php @@ -1,56 +1,287 @@ courseid = $courseid; - $this->format = $format; - $this->options = $options; +/** + * Library functions for managing text filter plugins. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package moodlecore + */ + +/** + * The states a filter can be in, stored in the filter_active table. + */ +define('TEXTFILTER_ON', 1); +define('TEXTFILTER_INHERIT', 0); +define('TEXTFILTER_OFF', -1); +define('TEXTFILTER_DISABLED', -9999); + +/** + * Class to manage the filtering of strings. It is intended that this class is + * only used by weblib.php. Client code should probably be using the + * format_text and format_string functions. + * + * This class is a singleton. + */ +class filter_manager { + /** This list of active filters, by context, for filtering content. + * An array contextid => array of filter objects. */ + protected $textfilters = array(); + + /** This list of active filters, by context, for filtering strings. + * An array contextid => array of filter objects. */ + protected $stringfilters = array(); + + /** Exploded version of $CFG->stringfilters. */ + protected $stringfilternames = array(); + + /** Holds the singleton instance. */ + protected static $singletoninstance; + + protected function __construct() { + $this->stringfilternames = filter_get_string_filters(); } - public static function addfilter($classname, $obj) { - if (empty(self::$filters[$classname])) { - self::$filters[$classname] = $obj; - return true; - } else { - return false; + /** + * @return the singleton instance. + */ + public static function instance() { + if (is_null(self::$singletoninstance)) { + global $CFG; + if (!empty($CFG->perfdebug)) { + self::$singletoninstance = new performance_measuring_filter_manager(); + } else { + self::$singletoninstance = new self(); + } } + return self::$singletoninstance; } - public static function do_filter($text, $courseid = null) { + /** Load all the filters required by this context. */ + protected function load_filters($context, $courseid) { + $filters = filter_get_active_in_context($context); + $this->textfilters[$context->id] = array(); + $this->stringfilters[$context->id] = array(); + foreach ($filters as $filtername => $localconfig) { + $filter = $this->make_filter_object($filtername, $context, $courseid, $localconfig); + if (is_null($filter)) { + continue; + } + $this->textfilters[$context->id][] = $filter; + if (in_array($filtername, $this->stringfilternames)) { + $this->stringfilters[$context->id][] = $filter; + } + } + } + + /** + * Factory method for creating a filter. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @param $context context object. + * @param $courseid course if. + * @param $localconfig array of local configuration variables for this filter. + * @return moodle_text_filter The filter, or null, if this type of filter is + * not recognised or could not be created. + */ + protected function make_filter_object($filtername, $context, $courseid, $localconfig) { global $CFG; + $path = $CFG->dirroot .'/'. $filtername .'/filter.php'; + if (!is_readable($path)) { + return null; + } + include_once($path); - foreach (self::$filters as $n=>$obj) { - $text = $obj->filter($text); + $filterclassname = basename($filtername) . '_filter'; + if (class_exists($filterclassname)) { + return new $filterclassname($courseid, $context, $localconfig); } - // back compatable with old filter plugins - if (isset($CFG->textfilters)) { - $textfilters = explode(',', $CFG->textfilters); - foreach ($textfilters as $v) { - $text_filter = basename($v).'_filter'; - if (empty(self::$filters[$text_filter]) && is_readable($CFG->dirroot .'/'. $v .'/filter.php')) { - include_once($CFG->dirroot .'/'. $v .'/filter.php'); - if (function_exists($text_filter)) { - $text = $text_filter($courseid, $text); - } - } - } + $legacyfunctionname = basename($filtername) . '_filter'; + if (function_exists($legacyfunctionname)) { + return new legacy_filter($legacyfunctionname, $courseid, $context, $localconfig); + } + + return null; + } + + protected function apply_filter_chain($text, $filterchain) { + foreach ($filterchain as $filter) { + $text = $filter->filter($text); + } + return $text; + } + + protected function get_text_filters($context, $courseid) { + if (!isset($this->textfilters[$context->id])) { + $this->load_filters($context, $courseid); + } + return $this->textfilters[$context->id]; + } + + protected function get_string_filters($context, $courseid) { + if (!isset($this->stringfilters[$context->id])) { + $this->load_filters($context, $courseid); } + return $this->stringfilters[$context->id]; + } + + public function filter_text($text, $context, $courseid) { + $text = $this->apply_filter_chain($text, $this->get_text_filters($context, $courseid)); + /// tags removed for XHTML compatibility + $text = str_replace(array('', ''), '', $text); + return $text; + } + + public function filter_string($string, $context, $courseid) { + return $this->apply_filter_chain($string, $this->get_string_filters($context, $courseid)); + } + + public function text_filtering_hash($context, $courseid) { + $filters = $this->get_text_filters($context, $courseid); + $hashes = array(); + foreach ($filters as $filter) { + $hashes[] = $filter->hash(); + } + return implode('-', $hashes); + } +} + +/** + * Filter manager subclass that does nothing. Having this simplifies the logic + * of format_text, etc. + */ +class null_filter_manager { + public function filter_text($text, $context, $courseid) { + return $text; + } + + public function filter_string($string, $context, $courseid) { return $text; } + public function text_filtering_hash() { + return ''; + } +} + +/** + * Filter manager subclass that tacks how much work it does. + */ +class performance_measuring_filter_manager extends filter_manager { + protected $filterscreated = 0; + protected $textsfiltered = 0; + protected $stringsfiltered = 0; + + protected function make_filter_object($filtername, $context, $courseid, $localconfig) { + $this->filterscreated++; + return parent::make_filter_object($filtername, $context, $courseid, $localconfig); + } + + public function filter_text($text, $context, $courseid) { + $this->textsfiltered++; + return parent::filter_text($text, $context, $courseid); + } + + public function filter_string($string, $context, $courseid) { + $this->stringsfiltered++; + return parent::filter_string($string, $context, $courseid); + } + + public function get_performance_summary() { + return array(array( + 'contextswithfilters' => count($this->textfilters), + 'filterscreated' => $this->filterscreated, + 'textsfiltered' => $this->textsfiltered, + 'stringsfiltered' => $this->stringsfiltered, + ), array( + 'contextswithfilters' => 'Contexts for which filters were loaded', + 'filterscreated' => 'Filters created', + 'textsfiltered' => 'Pieces of content filtered', + 'stringsfiltered' => 'Strings filtered', + )); + } +} + +/** + * Base class for text filters. You just need to override this class and + * implement the filter method. + */ +abstract class moodle_text_filter { + /** The course we are in. */ + protected $courseid; + /** The context we are in. */ + protected $context; + /** Any local configuration for this filter in this context. */ + protected $localconfig; + + /** + * Set any context-specific configuration for this filter. + * @param object $context The current course id. + * @param object $context The current context. + * @param array $config Any context-specific configuration for this filter. + */ + public function __construct($courseid, $context, array $localconfig) { + $this->courseid = $courseid; + $this->context = $context; + $this->localconfig = $localconfig; + } + public function hash() { return __CLASS__; } - // filter plugin must overwrite this function to filter - abstract function filter($text); + /** + * Override this funciton to actually implement the filtering. + * @param $text some HTML content. + * @return the HTML content after the filtering has been applied. + */ + public abstract function filter($text); +} + +/** + * moodle_text_filter implementation that encapsulates an old-style filter that + * only defines a function, not a class. + */ +class legacy_filter extends moodle_text_filter { + protected $filterfunction; + + /** + * Set any context-specific configuration for this filter. + * @param string $filterfunction + * @param object $context The current course id. + * @param object $context The current context. + * @param array $config Any context-specific configuration for this filter. + */ + public function __construct($filterfunction, $courseid, $context, array $localconfig) { + parent::__construct($courseid, $context, $localconfig); + $this->filterfunction = $filterfunction; + } + + public function filter($text) { + return call_user_func($this->filterfunction, $this->courseid, $text); + } } /// Define one exclusive separator that we'll use in the temp saved tags @@ -106,8 +337,9 @@ class filterobject { * @param string $filter the folder name where the filter lives. * @return string the human-readable name for this filter. */ -function filter_get_name($filterlocation, $filter) { - switch ($filterlocation) { +function filter_get_name($filter) { + list($type, $filter) = explode('/', $filter); + switch ($type) { case 'filter': $strfiltername = get_string('filtername', 'filter_' . $filter); if (substr($strfiltername, 0, 2) != '[[') { @@ -119,12 +351,12 @@ function filter_get_name($filterlocation, $filter) { case 'mod': $strfiltername = get_string('filtername', $filter); if (substr($strfiltername, 0, 2) == '[[') { - $strfiltername .= ' (' . $filterlocation . '/' . $filter . ')'; + $strfiltername .= ' (' . $type . '/' . $filter . ')'; } return $strfiltername; default: - throw new coding_exception('Unknown filter location ' . $filterlocation); + throw new coding_exception('Unknown filter type ' . $type); } } @@ -143,7 +375,7 @@ function filter_get_all_installed() { foreach ($filters as $filter) { $path = $filterlocation . '/' . $filter; if (is_readable($CFG->dirroot . '/' . $path . '/filter.php')) { - $strfiltername = filter_get_name($filterlocation, $filter); + $strfiltername = filter_get_name($path); $filternames[$path] = $strfiltername; } } @@ -153,6 +385,411 @@ function filter_get_all_installed() { } /** + * Set the global activated state for a text filter. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @param integer $state One of the values TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_DISABLED. + * @param integer $sortorder (optional) a position in the sortorder to place this filter. + * If not given defaults to: + * No change in order if we are updating an exsiting record, and not changing to or from TEXTFILTER_DISABLED. + * Just after the last currently active filter when adding an unknown filter + * in state TEXTFILTER_ON or TEXTFILTER_OFF, or enabling/diabling an exsisting filter. + * Just after the very last filter when adding an unknown filter in state TEXTFILTER_DISABLED + */ +function filter_set_global_state($filter, $state, $sortorder = false) { + global $DB; + + // Check requested state is valid. + if (!in_array($state, array(TEXTFILTER_ON, TEXTFILTER_OFF, TEXTFILTER_DISABLED))) { + throw new coding_exception("Illegal option '$state' passed to filter_set_global_state. " . + "Must be one of TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_DISABLED."); + } + + // Check sortorder is valid. + if ($sortorder !== false) { + if ($sortorder < 1 || $sortorder > $DB->get_field('filter_active', 'MAX(sortorder)', array()) + 1) { + throw new coding_exception("Invalid sort order passed to filter_set_global_state."); + } + } + + // See if there is an existing record. + $syscontext = get_context_instance(CONTEXT_SYSTEM); + $rec = $DB->get_record('filter_active', array('filter' => $filter, 'contextid' => $syscontext->id)); + if (empty($rec)) { + $insert = true; + $rec = new stdClass; + $rec->filter = $filter; + $rec->contextid = $syscontext->id; + } else { + $insert = false; + if ($sortorder === false && !($rec->active == TEXTFILTER_DISABLED xor $state == TEXTFILTER_DISABLED)) { + $sortorder = $rec->sortorder; + } + } + + // Automatic sort order. + if ($sortorder === false) { + if ($state == TEXTFILTER_DISABLED && $insert) { + $prevmaxsortorder = $DB->get_field('filter_active', 'MAX(sortorder)', array()); + } else { + $prevmaxsortorder = $DB->get_field_select('filter_active', 'MAX(sortorder)', 'active <> ?', array(TEXTFILTER_DISABLED)); + } + if (empty($prevmaxsortorder)) { + $sortorder = 1; + } else { + $sortorder = $prevmaxsortorder + 1; + if (!$insert && $state == TEXTFILTER_DISABLED) { + $sortorder = $prevmaxsortorder; + } + } + } + + // Move any existing records out of the way of the sortorder. + if ($insert) { + $DB->execute('UPDATE {filter_active} SET sortorder = sortorder + 1 WHERE sortorder >= ?', array($sortorder)); + } else if ($sortorder != $rec->sortorder) { + $sparesortorder = $DB->get_field('filter_active', 'MIN(sortorder)', array()) - 1; + $DB->set_field('filter_active', 'sortorder', $sparesortorder, array('filter' => $filter, 'contextid' => $syscontext->id)); + if ($sortorder < $rec->sortorder) { + $DB->execute('UPDATE {filter_active} SET sortorder = sortorder + 1 WHERE sortorder >= ? AND sortorder < ?', + array($sortorder, $rec->sortorder)); + } else if ($sortorder > $rec->sortorder) { + $DB->execute('UPDATE {filter_active} SET sortorder = sortorder - 1 WHERE sortorder <= ? AND sortorder > ?', + array($sortorder, $rec->sortorder)); + } + } + + // Insert/update the new record. + $rec->active = $state; + $rec->sortorder = $sortorder; + if ($insert) { + $DB->insert_record('filter_active', $rec); + } else { + $DB->update_record('filter_active', $rec); + } +} + +/** + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @return boolean is this filter allowed to be used on this site. That is, the + * admin has set the global 'active' setting to On, or Off, but available. + */ +function filter_is_enabled($filter) { + return array_key_exists($filter, filter_get_globally_enabled()); +} + +/** + * Return a list of all the filters that may be in use somewhere. + * @return array where the keys and values are both the filter name, like 'filter/tex'. + */ +function filter_get_globally_enabled() { + static $enabledfilters = null; + if (is_null($enabledfilters)) { + $filters = filter_get_global_states(); + $enabledfilters = array(); + foreach ($filters as $filter => $filerinfo) { + if ($filerinfo->active != TEXTFILTER_DISABLED) { + $enabledfilters[$filter] = $filter; + } + } + } + return $enabledfilters; +} + +/** + * Return the names of the filters that should also be applied to strings + * (when they are enabled). + * @return array where the keys and values are both the filter name, like 'filter/tex'. + */ +function filter_get_string_filters() { + global $CFG; + $stringfilters = array(); + if (!empty($CFG->filterall) && !empty($CFG->stringfilters)) { + $stringfilters = explode(',', $CFG->stringfilters); + $stringfilters = array_combine($stringfilters, $stringfilters); + } + return $stringfilters; +} + +/** + * Sets whether a particular active filter should be applied to all strings by + * format_string, or just used by format_text. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @param boolean $applytostrings if true, this filter will apply to format_string + * and format_text, when it is enabled. + */ +function filter_set_applies_to_strings($filter, $applytostrings) { + $stringfilters = filter_get_string_filters(); + $numstringfilters = count($stringfilters); + if ($applytostrings) { + $stringfilters[$filter] = $filter; + } else { + unset($stringfilters[$filter]); + } + if (count($stringfilters) != $numstringfilters) { + set_config('stringfilters', implode(',', $stringfilters)); + set_config('filterall', !empty($stringfilters)); + } +} + +/** + * Set the local activated state for a text filter. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @param integer $contextid The id of the context to get the local config for. + * @param integer $state One of the values TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_INHERIT. + */ +function filter_set_local_state($filter, $contextid, $state) { + global $DB; + + // Check requested state is valid. + if (!in_array($state, array(TEXTFILTER_ON, TEXTFILTER_OFF, TEXTFILTER_INHERIT))) { + throw new coding_exception("Illegal option '$state' passed to filter_set_local_state. " . + "Must be one of TEXTFILTER_ON, TEXTFILTER_OFF or TEXTFILTER_INHERIT."); + } + + if ($contextid == get_context_instance(CONTEXT_SYSTEM)->id) { + throw new coding_exception('You cannot use filter_set_local_state ' . + 'with $contextid equal to the system context id.'); + } + + if ($state == TEXTFILTER_INHERIT) { + $DB->delete_records('filter_active', array('filter' => $filter, 'contextid' => $contextid)); + return; + } + + $rec = $DB->get_record('filter_active', array('filter' => $filter, 'contextid' => $contextid)); + $insert = false; + if (empty($rec)) { + $insert = true; + $rec = new stdClass; + $rec->filter = $filter; + $rec->contextid = $contextid; + } + + $rec->active = $state; + + if ($insert) { + $DB->insert_record('filter_active', $rec); + } else { + $DB->update_record('filter_active', $rec); + } +} + +/** + * Set a particular local config variable for a filter in a context. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @param integer $contextid The id of the context to get the local config for. + * @param string $name the setting name. + * @param string $value the corresponding value. + */ +function filter_set_local_config($filter, $contextid, $name, $value) { + global $DB; + $rec = $DB->get_record('filter_config', array('filter' => $filter, 'contextid' => $contextid, 'name' => $name)); + $insert = false; + if (empty($rec)) { + $insert = true; + $rec = new stdClass; + $rec->filter = $filter; + $rec->contextid = $contextid; + $rec->name = $name; + } + + $rec->value = $value; + + if ($insert) { + $DB->insert_record('filter_config', $rec); + } else { + $DB->update_record('filter_config', $rec); + } +} + +/** + * Remove a particular local config variable for a filter in a context. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @param integer $contextid The id of the context to get the local config for. + * @param string $name the setting name. + */ +function filter_unset_local_config($filter, $contextid, $name) { + global $DB; + $DB->delete_records('filter_config', array('filter' => $filter, 'contextid' => $contextid, 'name' => $name)); +} + +/** + * Get local config variables for a filter in a context. Normally (when your + * filter is running) you don't need to call this, becuase the config is fetched + * for you automatically. You only need this, for example, when you are getting + * the config so you can show the user an editing from. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @param integer $contextid The ID of the context to get the local config for. + * @return array of name => value pairs. + */ +function filter_get_local_config($filter, $contextid) { + global $DB; + return $DB->get_records_menu('filter_config', array('filter' => $filter, 'contextid' => $contextid), '', 'name,value'); +} + +/** + * This function is for use by backup. Gets all the filter information specific + * to one context. + * @return array with two elements. The first element is an array of objects with + * fields filter and active. These come from the filter_active table. The + * second element is an array of objects with fields filter, name and value + * from the filter_config table. + */ +function filter_get_all_local_settings($contextid) { + global $DB; + $context = get_context_instance(CONTEXT_SYSTEM); + return array( + $DB->get_records('filter_active', array('contextid' => $contextid), 'filter', 'filter,active'), + $DB->get_records('filter_config', array('contextid' => $contextid), 'filter,name', 'filter,name,value'), + ); +} + +/** + * Get the list of active filters, in the order that they should be used + * for a particular context, along with any local configuration variables. + * + * @param object $context a context + * + * @return array an array where the keys are the filter names, for example + * 'filter/tex' or 'mod/glossary' and the values are any local + * configuration for that filter, as an array of name => value pairs + * from the filter_config table. In a lot of cases, this will be an + * empty array. So, an example return value for this function might be + * array('filter/tex' => array(), 'mod/glossary' => array('glossaryid', 123)) + */ +function filter_get_active_in_context($context) { + global $DB; + $contextids = str_replace('/', ',', trim($context->path, '/')); + + // The following SQL is tricky. It is explained on + // http://docs.moodle.org/en/Development:Filter_enable/disable_by_context + $rs = $DB->get_recordset_sql( + "SELECT active.filter, fc.name, fc.value + FROM (SELECT f.filter + FROM {filter_active} f + JOIN {context} ctx ON f.contextid = ctx.id + WHERE ctx.id IN ($contextids) + GROUP BY filter + HAVING MAX(f.active * ctx.depth) > -MIN(f.active * ctx.depth) + ORDER BY MAX(f.sortorder)) active + LEFT JOIN {filter_config} fc ON fc.filter = active.filter AND fc.contextid = $context->id"); + + // Masssage the data into the specified format to return. + $filters = array(); + foreach ($rs as $row) { + if (!isset($filters[$row->filter])) { + $filters[$row->filter] = array(); + } + if (!is_null($row->name)) { + $filters[$row->filter][$row->name] = $row->value; + } + } + + $rs->close(); + + return $filters; +} + +/** + * List all of the filters that are available in this context, and what the + * local and interited states of that filter are. + * @param object $context a context that is not the system context. + * @return array an array with filter names, for example 'filter/tex' or + * 'mod/glossary' as keys. and and the values are objects with fields: + * ->filter filter name, same as the key. + * ->localstate TEXTFILTER_ON/OFF/INHERIT + * ->inheritedstate TEXTFILTER_ON/OFF - the state that will be used if localstate is set to TEXTFILTER_INHERIT. + */ +function filter_get_available_in_context($context) { + global $DB; + + // The complex logic is working out the active state in the parent context, + // so strip the current context from the list. + $contextids = explode('/', trim($context->path, '/')); + array_pop($contextids); + $contextids = implode(',', $contextids); + if (empty($contextids)) { + throw new coding_exception('filter_get_available_in_context cannot be called with the system context.'); + } + + // The following SQL is tricky, in the same way at the SQL in filter_get_active_in_context. + return $DB->get_records_sql( + "SELECT parent_states.filter, + CASE WHEN fa.active IS NULL THEN " . TEXTFILTER_INHERIT . " + ELSE fa.active END AS localstate, + parent_states.inheritedstate + FROM (SELECT f.filter, + CASE WHEN MAX(f.active * ctx.depth) > -MIN(f.active * ctx.depth) THEN " . TEXTFILTER_ON . " + ELSE " . TEXTFILTER_OFF . " END AS inheritedstate + FROM {filter_active} f + JOIN {context} ctx ON f.contextid = ctx.id + WHERE ctx.id IN ($contextids) + GROUP BY filter + HAVING MIN(f.active) > " . TEXTFILTER_DISABLED . " + ORDER BY MAX(f.sortorder)) parent_states + LEFT JOIN {filter_active} fa ON fa.filter = parent_states.filter AND fa.contextid = $context->id"); +} + +/** + * This function is for use by the filter administration page. + * @return array 'filtername' => object with fields 'filter' (=filtername), 'active' and 'sortorder' + */ +function filter_get_global_states() { + global $DB; + $context = get_context_instance(CONTEXT_SYSTEM); + return $DB->get_records('filter_active', array('contextid' => $context->id), 'sortorder', 'filter,active,sortorder'); +} + +/** + * Delete all the data in the database relating to a filter, prior to deleting it. + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + */ +function filter_delete_all_data($filter) { + global $DB; + if (substr($filter, 0, 7) == 'filter/') { + unset_all_config_for_plugin('filter_' . basename($filter)); + } + $DB->delete_records('filter_active', array('filter' => $filter)); + $DB->delete_records('filter_config', array('filter' => $filter)); +} + +/** + * Does this filter have a global settings page in the admin tree? + * (The settings page for a filter must be called, for example, + * filtersettingfiltertex or filtersettingmodglossay.) + * + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @return boolean Whether there should be a 'Settings' link on the config page. + */ +function filter_has_global_settings($filter) { + global $CFG; + $settingspath = $CFG->dirroot . '/' . $filter . '/filtersettings.php'; + return is_readable($settingspath); +} + +/** + * Does this filter have local (per-context) settings? + * + * @param string $filter The filter name, for example 'filter/tex' or 'mod/glossary'. + * @return boolean Whether there should be a 'Settings' link on the manage filters in context page. + */ +function filter_has_local_settings($filter) { + global $CFG; + $settingspath = $CFG->dirroot . '/' . $filter . '/filterlocalsettings.php'; + return is_readable($settingspath); +} + +/** + * Certain types of context (block and user) may not have local filter settings. + * the function checks a context to see whether it may have local config. + * @param object $context a context. + * @return boolean whether this context may have local filter settings. + */ +function filter_context_may_have_filter_settings($context) { + return $context->contextlevel != CONTEXT_BLOCK && $context->contextlevel != CONTEXT_USER; +} + +/** * Process phrases intelligently found within a HTML text (such as adding links) * * param text the text that we are filtering diff --git a/lib/moodlelib.php b/lib/moodlelib.php index bd3cb80..e46cda2 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -7967,6 +7967,16 @@ function get_performance_info() { $info['html'] .= 'Included '.$info['includecount'].' files '; $info['txt'] .= 'includecount: '.$info['includecount'].' '; + $filtermanager = filter_manager::instance(); + if (method_exists($filtermanager, 'get_performance_summary')) { + list($filterinfo, $nicenames) = $filtermanager->get_performance_summary(); + $info = array_merge($filterinfo, $info); + foreach ($filterinfo as $key => $value) { + $info['html'] .= "$nicenames[$key]: $value "; + $info['txt'] .= "$key: $value "; + } + } + if (!empty($PERF->logwrites)) { $info['logwrites'] = $PERF->logwrites; $info['html'] .= 'Log DB writes '.$info['logwrites'].' '; @@ -8362,7 +8372,7 @@ function get_plugin_name($plugin, $type='mod') { } break; case 'filter': - $plugin_name = filter_get_name('filter', $plugin); + $plugin_name = filter_get_name('filter/' . $plugin); break; default: $plugin_name = $plugin; diff --git a/lib/simpletest/filtersettingsperformancetester.php b/lib/simpletest/filtersettingsperformancetester.php new file mode 100644 index 0000000..6ccad54 --- /dev/null +++ b/lib/simpletest/filtersettingsperformancetester.php @@ -0,0 +1,237 @@ +libdir . '/ddllib.php'); + +require_login(); +$syscontext = get_context_instance(CONTEXT_SYSTEM); +require_capability('moodle/site:config', $syscontext); + +$baseurl = $CFG->wwwroot . '/lib/simpletest/filtersettingsperformancetester.php'; + +$title = 'filter_get_active_in_context performance test'; +print_header($title, $title, build_navigation($title)); + +// Complain if we get this far and $CFG->unittestprefix is not set. +if (empty($CFG->unittestprefix)) { + throw new coding_exception('This page requires $CFG->unittestprefix to be set in config.php.'); +} + +$requiredtables = array('context', 'filter_active', 'filter_config'); +$realdb = $DB; +$testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary); +$testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix); +$DB = $testdb; +$dbman = $testdb->get_manager(); +$issetup = 0; +foreach ($requiredtables as $table) { + if ($dbman->table_exists(new xmldb_table($table))) { + $issetup++; + } +} + +switch (optional_param('action', '', PARAM_ACTION)) { + case 'setup': + if ($issetup == 0) { + foreach ($requiredtables as $table) { + $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/lib/db/install.xml', $table); + $issetup++; + } + flush(); + populate_test_database($syscontext, 10, 100, 1000, 5000, 5000); + notify('Test tables created.', 'notifysuccess'); + } else if ($issetup == count($requiredtables)) { + notify('Test tables are already set up.', 'notifysuccess'); + } else { + notify('Something is wrong, please delete the test tables and try again.'); + } + break; + + case 'teardown': + foreach ($requiredtables as $tablename) { + $table = new xmldb_table($tablename); + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + } + $issetup = 0; + notify('Test tables dropped.', 'notifysuccess'); + break; + + case 'test': + if ($issetup != count($requiredtables)) { + notify('Something is wrong, please delete the test tables and try again.'); + } else { + $contexts = $DB->get_records('context'); + $numcalls = 1000; + $basetime = run_tests('noop', $contexts, $numcalls, 0); + run_tests('simple_get_record_by_id', $contexts, $numcalls, $basetime); + run_tests('filter_get_active_in_context', $contexts, $numcalls, $basetime); + } + break; +} + +if ($issetup == count($requiredtables)) { + echo '

    Total of ' . $DB->count_records('context') . ' contexts, ' . + $DB->count_records('filter_active') . ' filter_active and ' . + $DB->count_records('filter_config') . ' filter_config rows in the database.

    '; +} + +$DB = $realdb; + +echo '
    '; +print_single_button($baseurl, array('action' => 'setup'), 'Set up test tables', 'get', '', false, '', $issetup > 0); +print_single_button($baseurl, array('action' => 'teardown'), 'Drop test tables', 'get', '', false, '', $issetup == 0); +print_single_button($baseurl, array('action' => 'test'), 'Run tests', 'get', '', false, '', $issetup != count($requiredtables)); +echo '
    '; + +print_footer(); + +function noop($context) { +} + +function simple_get_record_by_id($context) { + global $DB; + $DB->get_record('context', array('id' => $context->id)); +} + +function run_tests($function, $contexts, $numcalls, $basetime) { + set_time_limit(120); + $startime = microtime(true); + for ($j = 0; $j < $numcalls; $j++) { + $function($contexts[array_rand($contexts)]); + } + $duration = microtime(true) - $startime; + print_result_line($duration, $basetime, $numcalls, 'calls to ' . $function); + return $duration; +} + +function print_result_line($duration, $basetime, $numcalls, $action1, $action2 = 'calls per second') { + echo '

    Time for ' . format_float($numcalls, 0) . ' ' . $action1 . ': ' . + format_float($duration - $basetime, 3) . 's (' . format_float($duration, 3) . ' - ' . + format_float($basetime, 3) . 's) which is ' . + format_float(($numcalls / ($duration - $basetime)), 0) . ' ' . $action2 . ".

    \n"; + flush(); +} + +function populate_test_database($syscontext, $numcategories, $numcourses, $nummodules, $numoverrides, $numconfigs) { + global $DB; + set_time_limit(600); + $syscontext->id = $DB->insert_record('context', $syscontext); + + // Category contexts. + $categoryparents = array($syscontext); + $categories = array(); + for ($i = 0; $i < $numcategories; $i++) { + $context = insert_context(CONTEXT_COURSECAT, $i, $categoryparents[array_rand($categoryparents)]); + $categoryparents[] = $context; + $categories[$context->id] = $context; + } + notify('Created ' . $numcategories . ' course category contexts.', 'notifysuccess'); flush(); + + // Course contexts. + $courses = array(); + for ($i = 0; $i < $numcourses; $i++) { + $context = insert_context(CONTEXT_COURSE, $i, $categories[array_rand($categories)]); + $courses[$context->id] = $context; + } + notify('Created ' . $numcourses . ' course contexts.', 'notifysuccess'); flush(); + + // Activities contexts. + $mods = array(); + $prog = new progress_bar('modbar', 500, true); + $DB->begin_sql(); + for ($i = 0; $i < $nummodules; $i++) { + $context = insert_context(CONTEXT_MODULE, $i, $courses[array_rand($courses)]); + $mods[$context->id] = $context; + if ($i % 50) { + $prog->update($i, $nummodules, ''); + } + } + $DB->commit_sql(); + notify('Created ' . $nummodules . ' module contexts.', 'notifysuccess'); flush(); + + $contexts = $categories + $courses + $mods; + + // Global settings. + $installedfilters = filter_get_all_installed(); + $counts = array(TEXTFILTER_DISABLED => 0, TEXTFILTER_OFF => 0, TEXTFILTER_ON => 0); + foreach ($installedfilters as $filter => $notused) { + $state = array_rand($counts); + filter_set_global_state($filter, $state); + $counts[$state]++; + } + notify('Set global setting: ' . $counts[TEXTFILTER_DISABLED] . ' disabled, ' . + $counts[TEXTFILTER_OFF] . ' off and ' . $counts[TEXTFILTER_ON] . ' on.', 'notifysuccess'); flush(); + + // Local overrides. + $localstates = array(TEXTFILTER_OFF => 0, TEXTFILTER_ON => 0); + $prog = new progress_bar('locbar', 500, true); + $DB->begin_sql(); + for ($i = 0; $i < $numoverrides; $i++) { + filter_set_local_state(array_rand($installedfilters), array_rand($contexts), array_rand($localstates)); + if ($i % 50) { + $prog->update($i, $numoverrides, ''); + } + } + $DB->commit_sql(); + notify('Set ' . $numoverrides . ' local overrides.', 'notifysuccess'); flush(); + + // Local config. + $variablenames = array('frog' => 0, 'toad' => 0, 'elver' => 0, 'eft' => 0, 'tadpole' => 0); + $prog = new progress_bar('confbar', 500, true); + $DB->begin_sql(); + for ($i = 0; $i < $numconfigs; $i++) { + filter_set_local_config(array_rand($installedfilters), array_rand($contexts), + array_rand($variablenames), random_string(rand(20, 40))); + if ($i % 50) { + $prog->update($i, $numconfigs, ''); + } + } + $DB->commit_sql(); + notify('Set ' . $numconfigs . ' local configs.', 'notifysuccess'); flush(); +} + +function insert_context($contextlevel, $instanceid, $parent) { + global $DB; + $context = new stdClass; + $context->contextlevel = $contextlevel; + $context->instanceid = $instanceid; + $context->depth = $parent->depth + 1; + $context->id = $DB->insert_record('context', $context); + $context->path = $parent->path . '/' . $context->id; + $DB->set_field('context', 'path', $context->path, array('id' => $context->id)); + return $context; +} + diff --git a/lib/simpletest/testfilterconfig.php b/lib/simpletest/testfilterconfig.php new file mode 100644 index 0000000..7f14440 --- /dev/null +++ b/lib/simpletest/testfilterconfig.php @@ -0,0 +1,642 @@ +libdir . '/filterlib.php'); + +/** + * Test functions that affect filter_active table with contextid = $syscontextid. + */ +class filter_active_global_test extends UnitTestCaseUsingDatabase { + private $syscontextid; + + public function setUp() { + parent::setUp(); + + // Make sure accesslib has cached a sensible system context object + // before we switch to the test DB. + $this->syscontextid = get_context_instance(CONTEXT_SYSTEM)->id; + + // Create the table we need and switch to test DB. + $this->create_test_table('filter_active', 'lib'); + $this->switch_to_test_db(); + } + + private function assert_only_one_filter_globally($filter, $state) { + $recs = $this->testdb->get_records('filter_active'); + $this->assertEqual(1, count($recs), 'More than one record returned %s.'); + $rec = reset($recs); + $expectedrec = new stdClass; + $expectedrec->filter = $filter; + $expectedrec->contextid = $this->syscontextid; + $expectedrec->active = $state; + $expectedrec->sortorder = 1; + $this->assert(new CheckSpecifiedFieldsExpectation($expectedrec), $rec); + } + + private function assert_global_sort_order($filters) { + $sortedfilters = $this->testdb->get_records_menu('filter_active', + array('contextid' => $this->syscontextid), 'sortorder', 'sortorder,filter'); + $testarray = array(); + $index = 1; + foreach($filters as $filter) { + $testarray[$index++] = $filter; + } + $this->assertEqual($testarray, $sortedfilters); + } + + public function test_set_filter_globally_on() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/name', TEXTFILTER_ON, 1); + // Validate. + $this->assert_only_one_filter_globally('filter/name', TEXTFILTER_ON); + } + + public function test_set_filter_globally_off() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/name', TEXTFILTER_OFF, 1); + // Validate. + $this->assert_only_one_filter_globally('filter/name', TEXTFILTER_OFF); + } + + public function test_set_filter_globally_disabled() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/name', TEXTFILTER_DISABLED, 1); + // Validate. + $this->assert_only_one_filter_globally('filter/name', TEXTFILTER_DISABLED); + } + + public function test_global_config_exception_on_invalid_state() { + // Setup fixture. + // Set expectation. + $this->expectException(); + // Exercise SUT. + filter_set_global_state('filter/name', 0, 1); + } + + public function test_set_no_sortorder_clash() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/one', TEXTFILTER_DISABLED, 1); + filter_set_global_state('filter/two', TEXTFILTER_DISABLED, 1); + // Validate - should have pushed other filters down. + $this->assert_global_sort_order(array('filter/two', 'filter/one')); + } + + public function test_auto_sort_order_disabled() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/one', TEXTFILTER_DISABLED); + filter_set_global_state('filter/two', TEXTFILTER_DISABLED); + // Validate. + $this->assert_global_sort_order(array('filter/one', 'filter/two')); + } + + public function test_auto_sort_order_enabled() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/one', TEXTFILTER_ON); + filter_set_global_state('filter/two', TEXTFILTER_OFF); + // Validate. + $this->assert_global_sort_order(array('filter/one', 'filter/two')); + } + + public function test_auto_sort_order_mixed() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/0', TEXTFILTER_DISABLED); + filter_set_global_state('filter/1', TEXTFILTER_ON); + filter_set_global_state('filter/2', TEXTFILTER_DISABLED); + filter_set_global_state('filter/3', TEXTFILTER_OFF); + // Validate. + $this->assert_global_sort_order(array('filter/1', 'filter/3', 'filter/0', 'filter/2')); + } + + public function test_update_existing_dont_duplicate() { + // Setup fixture. + // Exercise SUT. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_global_state('filter/name', TEXTFILTER_OFF); + // Validate. + $this->assert_only_one_filter_globally('filter/name', TEXTFILTER_OFF); + } + + public function test_sort_order_not_too_low() { + // Setup fixture. + filter_set_global_state('filter/1', TEXTFILTER_ON); + // Set expectation. + $this->expectException(); + // Exercise SUT. + filter_set_global_state('filter/2', TEXTFILTER_ON, 0); + } + + public function test_sort_order_not_too_high() { + // Setup fixture. + filter_set_global_state('filter/1', TEXTFILTER_ON); + // Set expectation. + $this->expectException(); + // Exercise SUT. + filter_set_global_state('filter/2', TEXTFILTER_ON, 3); + } + + public function test_update_reorder_down() { + // Setup fixture. + filter_set_global_state('filter/1', TEXTFILTER_ON); + filter_set_global_state('filter/2', TEXTFILTER_ON); + filter_set_global_state('filter/3', TEXTFILTER_ON); + // Exercise SUT. + filter_set_global_state('filter/2', TEXTFILTER_ON, 1); + // Validate. + $this->assert_global_sort_order(array('filter/2', 'filter/1', 'filter/3')); + } + + public function test_update_reorder_up() { + // Setup fixture. + filter_set_global_state('filter/1', TEXTFILTER_ON); + filter_set_global_state('filter/2', TEXTFILTER_ON); + filter_set_global_state('filter/3', TEXTFILTER_ON); + filter_set_global_state('filter/4', TEXTFILTER_ON); + // Exercise SUT. + filter_set_global_state('filter/2', TEXTFILTER_ON, 3); + // Validate. + $this->assert_global_sort_order(array('filter/1', 'filter/3', 'filter/2', 'filter/4')); + } + + public function test_auto_sort_order_change_to_enabled() { + // Setup fixture. + filter_set_global_state('filter/1', TEXTFILTER_ON); + filter_set_global_state('filter/2', TEXTFILTER_DISABLED); + filter_set_global_state('filter/3', TEXTFILTER_DISABLED); + // Exercise SUT. + filter_set_global_state('filter/3', TEXTFILTER_ON); + // Validate. + $this->assert_global_sort_order(array('filter/1', 'filter/3', 'filter/2')); + } + + public function test_auto_sort_order_change_to_disabled() { + // Setup fixture. + filter_set_global_state('filter/1', TEXTFILTER_ON); + filter_set_global_state('filter/2', TEXTFILTER_ON); + filter_set_global_state('filter/3', TEXTFILTER_DISABLED); + // Exercise SUT. + filter_set_global_state('filter/1', TEXTFILTER_DISABLED); + // Validate. + $this->assert_global_sort_order(array('filter/2', 'filter/1', 'filter/3')); + } + + public function test_filter_get_global_states() { + // Setup fixture. + filter_set_global_state('filter/1', TEXTFILTER_ON); + filter_set_global_state('filter/2', TEXTFILTER_OFF); + filter_set_global_state('filter/3', TEXTFILTER_DISABLED); + // Exercise SUT. + $filters = filter_get_global_states(); + // Validate. + $this->assertEqual(array( + 'filter/1' => (object) array('filter' => 'filter/1', 'active' => TEXTFILTER_ON, 'sortorder' => 1), + 'filter/2' => (object) array('filter' => 'filter/2', 'active' => TEXTFILTER_OFF, 'sortorder' => 2), + 'filter/3' => (object) array('filter' => 'filter/3', 'active' => TEXTFILTER_DISABLED, 'sortorder' => 3) + ), $filters); + } +} + +/** + * Test functions that affect filter_active table with contextid = $syscontextid. + */ +class filter_active_local_test extends UnitTestCaseUsingDatabase { + public function setUp() { + parent::setUp(); + + // Create the table we need and switch to test DB. + $this->create_test_table('filter_active', 'lib'); + $this->switch_to_test_db(); + } + + private function assert_only_one_local_setting($filter, $contextid, $state) { + $recs = $this->testdb->get_records('filter_active'); + $this->assertEqual(1, count($recs), 'More than one record returned %s.'); + $rec = reset($recs); + $expectedrec = new stdClass; + $expectedrec->filter = $filter; + $expectedrec->contextid = $contextid; + $expectedrec->active = $state; + $this->assert(new CheckSpecifiedFieldsExpectation($expectedrec), $rec); + } + + private function assert_no_local_setting() { + $this->assertEqual(0, $this->testdb->count_records('filter_active')); + } + + public function test_local_on() { + // Exercise SUT. + filter_set_local_state('filter/name', 123, TEXTFILTER_ON); + // Validate. + $this->assert_only_one_local_setting('filter/name', 123, TEXTFILTER_ON); + } + + public function test_local_off() { + // Exercise SUT. + filter_set_local_state('filter/name', 123, TEXTFILTER_OFF); + // Validate. + $this->assert_only_one_local_setting('filter/name', 123, TEXTFILTER_OFF); + } + + public function test_local_inherit() { + global $DB; + // Exercise SUT. + filter_set_local_state('filter/name', 123, TEXTFILTER_INHERIT); + // Validate. + $this->assert_no_local_setting(); + } + + public function test_local_invalid_state_throws_exception() { + // Set expectation. + $this->expectException(); + // Exercise SUT. + filter_set_local_state('filter/name', 123, -9999); + } + + public function test_throws_exception_when_setting_global() { + // Set expectation. + $this->expectException(); + // Exercise SUT. + filter_set_local_state('filter/name', get_context_instance(CONTEXT_SYSTEM)->id, TEXTFILTER_INHERIT); + } + + public function test_local_inherit_deletes_existing() { + global $DB; + // Setup fixture. + filter_set_local_state('filter/name', 123, TEXTFILTER_INHERIT); + // Exercise SUT. + filter_set_local_state('filter/name', 123, TEXTFILTER_INHERIT); + // Validate. + $this->assert_no_local_setting(); + } +} + +/** + * Test functions that use just the filter_config table. + */ +class filter_config_test extends UnitTestCaseUsingDatabase { + public function setUp() { + parent::setUp(); + + // Create the table we need and switch to test DB. + $this->create_test_table('filter_config', 'lib'); + $this->switch_to_test_db(); + } + + private function assert_only_one_config($filter, $context, $name, $value) { + $recs = $this->testdb->get_records('filter_config'); + $this->assertEqual(1, count($recs), 'More than one record returned %s.'); + $rec = reset($recs); + $expectedrec = new stdClass; + $expectedrec->filter = $filter; + $expectedrec->contextid = $context; + $expectedrec->name = $name; + $expectedrec->value = $value; + $this->assert(new CheckSpecifiedFieldsExpectation($expectedrec), $rec); + } + + public function test_set_new_config() { + // Exercise SUT. + filter_set_local_config('filter/name', 123, 'settingname', 'An arbitrary value'); + // Validate. + $this->assert_only_one_config('filter/name', 123, 'settingname', 'An arbitrary value'); + } + + public function test_update_existing_config() { + // Setup fixture. + filter_set_local_config('filter/name', 123, 'settingname', 'An arbitrary value'); + // Exercise SUT. + filter_set_local_config('filter/name', 123, 'settingname', 'A changed value'); + // Validate. + $this->assert_only_one_config('filter/name', 123, 'settingname', 'A changed value'); + } + + public function test_filter_get_local_config() { + // Setup fixture. + filter_set_local_config('filter/name', 123, 'setting1', 'An arbitrary value'); + filter_set_local_config('filter/name', 123, 'setting2', 'Another arbitrary value'); + filter_set_local_config('filter/name', 122, 'settingname', 'Value from another context'); + filter_set_local_config('filter/other', 123, 'settingname', 'Someone else\'s value'); + // Exercise SUT. + $config = filter_get_local_config('filter/name', 123); + // Validate. + $this->assertEqual(array('setting1' => 'An arbitrary value', 'setting2' => 'Another arbitrary value'), $config); + } +} + +class filter_get_active_available_in_context_test extends UnitTestCaseUsingDatabase { + private $syscontext; + private $childcontext; + private $childcontext2; + + public function setUp() { + parent::setUp(); + + // Make sure accesslib has cached a sensible system context object + // before we switch to the test DB. + $this->syscontext = get_context_instance(CONTEXT_SYSTEM); + + // Create the table we need and switch to test DB. + $this->create_test_tables(array('filter_active', 'filter_config', 'context'), 'lib'); + $this->switch_to_test_db(); + + // Set up systcontext in the test database. + $this->testdb->insert_record('context', $this->syscontext); + $this->syscontext->id = 1; + + // Set up a child context. + $this->childcontext = new stdClass; + $this->childcontext->contextlevel = CONTEXT_COURSECAT; + $this->childcontext->instanceid = 1; + $this->childcontext->depth = 2; + $this->childcontext->path = '/1/2'; + $this->testdb->insert_record('context', $this->childcontext); + $this->childcontext->id = 2; + + // Set up a grandchild context. + $this->childcontext2 = new stdClass; + $this->childcontext2->contextlevel = CONTEXT_COURSE; + $this->childcontext2->instanceid = 2; + $this->childcontext2->depth = 3; + $this->childcontext2->path = '/1/2/3'; + $this->testdb->insert_record('context', $this->childcontext2); + $this->childcontext2->id = 3; + } + + private function assert_filter_list($expectedfilters, $filters) { + $this->assert(new ArraysHaveSameValuesExpectation($expectedfilters), array_keys($filters)); + } + + public function test_globally_on_is_returned() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + // Exercise SUT. + $filters = filter_get_active_in_context($this->syscontext); + // Validate. + $this->assert_filter_list(array('filter/name'), $filters); + // Check no config returned correctly. + $this->assertEqual(array(), $filters['filter/name']); + } + + public function test_globally_off_not_returned() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_OFF); + // Exercise SUT. + $filters = filter_get_active_in_context($this->childcontext2); + // Validate. + $this->assert_filter_list(array(), $filters); + } + + public function test_globally_off_overridden() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_OFF); + filter_set_local_state('filter/name', $this->childcontext->id, TEXTFILTER_ON); + // Exercise SUT. + $filters = filter_get_active_in_context($this->childcontext2); + // Validate. + $this->assert_filter_list(array('filter/name'), $filters); + } + + public function test_globally_on_overridden() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_local_state('filter/name', $this->childcontext->id, TEXTFILTER_OFF); + // Exercise SUT. + $filters = filter_get_active_in_context($this->childcontext2); + // Validate. + $this->assert_filter_list(array(), $filters); + } + + public function test_globally_disabled_not_overridden() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_DISABLED); + filter_set_local_state('filter/name', $this->childcontext->id, TEXTFILTER_ON); + // Exercise SUT. + $filters = filter_get_active_in_context($this->syscontext); + // Validate. + $this->assert_filter_list(array(), $filters); + } + + public function test_single_config_returned() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_local_config('filter/name', $this->childcontext->id, 'settingname', 'A value'); + // Exercise SUT. + $filters = filter_get_active_in_context($this->childcontext); + // Validate. + $this->assertEqual(array('settingname' => 'A value'), $filters['filter/name']); + } + + public function test_multi_config_returned() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_local_config('filter/name', $this->childcontext->id, 'settingname', 'A value'); + filter_set_local_config('filter/name', $this->childcontext->id, 'anothersettingname', 'Another value'); + // Exercise SUT. + $filters = filter_get_active_in_context($this->childcontext); + // Validate. + $this->assertEqual(array('settingname' => 'A value', 'anothersettingname' => 'Another value'), $filters['filter/name']); + } + + public function test_config_from_other_context_not_returned() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_local_config('filter/name', $this->childcontext->id, 'settingname', 'A value'); + filter_set_local_config('filter/name', $this->childcontext2->id, 'anothersettingname', 'Another value'); + // Exercise SUT. + $filters = filter_get_active_in_context($this->childcontext2); + // Validate. + $this->assertEqual(array('anothersettingname' => 'Another value'), $filters['filter/name']); + } + + public function test_config_from_other_filter_not_returned() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_local_config('filter/name', $this->childcontext->id, 'settingname', 'A value'); + filter_set_local_config('filter/other', $this->childcontext->id, 'anothersettingname', 'Another value'); + // Exercise SUT. + $filters = filter_get_active_in_context($this->childcontext); + // Validate. + $this->assertEqual(array('settingname' => 'A value'), $filters['filter/name']); + } + + protected function assert_one_available_filter($filter, $localstate, $inheritedstate, $filters) { + $this->assertEqual(1, count($filters), 'More than one record returned %s.'); + $rec = $filters[$filter]; + $expectedrec = new stdClass; + $expectedrec->filter = $filter; + $expectedrec->localstate = $localstate; + $expectedrec->inheritedstate = $inheritedstate; + $this->assert(new CheckSpecifiedFieldsExpectation($expectedrec), $rec); + } + + public function test_available_in_context_localoverride() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_local_state('filter/name', $this->childcontext->id, TEXTFILTER_OFF); + // Exercise SUT. + $filters = filter_get_available_in_context($this->childcontext); + // Validate. + $this->assert_one_available_filter('filter/name', TEXTFILTER_OFF, TEXTFILTER_ON, $filters); + } + + public function test_available_in_context_nolocaloverride() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_local_state('filter/name', $this->childcontext->id, TEXTFILTER_OFF); + // Exercise SUT. + $filters = filter_get_available_in_context($this->childcontext2); + // Validate. + $this->assert_one_available_filter('filter/name', TEXTFILTER_INHERIT, TEXTFILTER_OFF, $filters); + } + + public function test_available_in_context_disabled_not_returned() { + // Setup fixture. + filter_set_global_state('filter/name', TEXTFILTER_DISABLED); + filter_set_local_state('filter/name', $this->childcontext->id, TEXTFILTER_ON); + // Exercise SUT. + $filters = filter_get_available_in_context($this->childcontext); + // Validate. + $this->assertEqual(array(), $filters); + } + + public function test_available_in_context_exception_with_syscontext() { + // Set expectation. + $this->expectException(); + // Exercise SUT. + filter_get_available_in_context($this->syscontext); + } +} + +class filter_delete_all_data_test extends UnitTestCaseUsingDatabase { + public function setUp() { + parent::setUp(); + + // Create the table we need and switch to test DB. + $this->create_test_tables(array('filter_active', 'filter_config', 'config', 'config_plugins'), 'lib'); + $this->switch_to_test_db(); + } + + public function test_filter_delete_all_data_filter() { + // Setup fixture. + $syscontext = get_context_instance(CONTEXT_SYSTEM); + filter_set_global_state('filter/name', TEXTFILTER_ON); + filter_set_global_state('filter/other', TEXTFILTER_ON); + filter_set_local_config('filter/name', $syscontext->id, 'settingname', 'A value'); + filter_set_local_config('filter/other', $syscontext->id, 'settingname', 'Other value'); + set_config('configname', 'A config value', 'filter_name'); + set_config('configname', 'Other config value', 'filter_other'); + // Exercise SUT. + filter_delete_all_data('filter/name'); + // Validate. + $this->assertEqual(1, $this->testdb->count_records('filter_active')); + $this->assertTrue($this->testdb->record_exists('filter_active', array('filter' => 'filter/other'))); + $this->assertEqual(1, $this->testdb->count_records('filter_config')); + $this->assertTrue($this->testdb->record_exists('filter_config', array('filter' => 'filter/other'))); + $expectedconfig = new stdClass; + $expectedconfig->configname = 'Other config value'; + $this->assertEqual($expectedconfig, get_config('filter_other')); + $this->assertFalse(get_config('filter_name')); + } +} + +class filter_filter_set_applies_to_strings extends UnitTestCaseUsingDatabase { + protected $origcfgstringfilters; + protected $origcfgfilterall; + + public function setUp() { + global $CFG; + parent::setUp(); + + // Create the table we need and switch to test DB. + $this->create_test_table('config', 'lib'); + $this->switch_to_test_db(); + + // Store original $CFG; + $this->origcfgstringfilters = $CFG->stringfilters; + $this->origcfgfilterall = $CFG->filterall; + } + + public function tearDown() { + $CFG->stringfilters = $this->origcfgstringfilters; + $CFG->filterall = $this->origcfgfilterall; + + parent::tearDown(); + } + + public function test_set() { + global $CFG; + // Setup fixture. + $CFG->filterall = 0; + $CFG->stringfilters = ''; + // Exercise SUT. + filter_set_applies_to_strings('filter/name', true); + // Validate. + $this->assertEqual('filter/name', $CFG->stringfilters); + $this->assertTrue($CFG->filterall); + } + + public function test_unset_to_empty() { + global $CFG; + // Setup fixture. + $CFG->filterall = 1; + $CFG->stringfilters = 'filter/name'; + // Exercise SUT. + filter_set_applies_to_strings('filter/name', false); + // Validate. + $this->assertEqual('', $CFG->stringfilters); + $this->assertFalse($CFG->filterall); + } + + public function test_unset_multi() { + global $CFG; + // Setup fixture. + $CFG->filterall = 1; + $CFG->stringfilters = 'filter/name,filter/other'; + // Exercise SUT. + filter_set_applies_to_strings('filter/name', false); + // Validate. + $this->assertEqual('filter/other', $CFG->stringfilters); + $this->assertTrue($CFG->filterall); + } +} +?> diff --git a/lib/simpletest/testfiltermanager.php b/lib/simpletest/testfiltermanager.php new file mode 100644 index 0000000..f032ce9 --- /dev/null +++ b/lib/simpletest/testfiltermanager.php @@ -0,0 +1,129 @@ +libdir . '/filterlib.php'); + +class testable_filter_manager extends filter_manager { + public function __construct() { + parent::__construct(); + } + public function make_filter_object($filtername, $context, $courseid, $localconfig) { + return parent::make_filter_object($filtername, $context, $courseid, $localconfig); + } + public function apply_filter_chain($text, $filterchain) { + return parent::apply_filter_chain($text, $filterchain); + } +} + +/** + * Test functions that affect filter_active table with contextid = $syscontextid. + */ +class filter_manager_test extends UnitTestCase { + protected $filtermanager; + protected $olddirroot; + + public function setUp() { + global $CFG; + $this->filtermanager = new testable_filter_manager(); + $this->olddirroot = $CFG->dirroot; + $CFG->dirroot = $CFG->dataroot . '/temp'; + } + + public function tearDown() { + global $CFG; + $CFG->dirroot = $this->olddirroot; + } + + /** Basically does file_put_contents, but ensures the directory exists first. */ + protected function write_file($path, $content) { + global $CFG; + make_upload_directory(str_replace($CFG->dataroot . '/', '', dirname($path))); + file_put_contents($path, $content); + } + + public function test_make_filter_object_newstyle() { + global $CFG; + $this->write_file($CFG->dirroot . '/filter/makenewstyletest/filter.php', <<filtermanager->make_filter_object('filter/makenewstyletest', null, 1, array()); + $this->assertIsA($filter, 'moodle_text_filter'); + $this->assertNotA($filter, 'legacy_filter'); + } + + public function test_make_filter_object_legacy() { + global $CFG; + $this->write_file($CFG->dirroot . '/filter/makelegacytest/filter.php', <<filtermanager->make_filter_object('filter/makelegacytest', null, 1, array()); + $this->assertIsA($filter, 'legacy_filter'); + } + + public function test_make_filter_object_missing() { + $this->assertNull($this->filtermanager->make_filter_object('filter/nonexistant', null, 1, array())); + } + + public function test_apply_filter_chain() { + $filterchain = array(new doubleup_test_filter(null, 1, array()), new killfrogs_test_filter(null, 1, array())); + $this->assertEqual('pawn pawn', $this->filtermanager->apply_filter_chain('frogspawn', $filterchain)); + } +} + +class doubleup_test_filter extends moodle_text_filter { + public function filter($text) { + return $text . ' ' . $text; + } +} + +class killfrogs_test_filter extends moodle_text_filter { + public function filter($text) { + return str_replace('frogs', '', $text); + } +} + +?> diff --git a/lib/weblib.php b/lib/weblib.php index aa704f6..d94903f 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -1253,27 +1253,17 @@ function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL $courseid = $COURSE->id; } - // if filter plugin is OOP format, add it to filter list and append hash - // value to $hashstr - if (!empty($CFG->textfilters)) { - require_once($CFG->libdir.'/filterlib.php'); - $textfilters = explode(',', $CFG->textfilters); - foreach ($textfilters as $textfilter) { - if (is_readable($CFG->dirroot .'/'. $textfilter .'/filter.php')) { - include_once($CFG->dirroot .'/'. $textfilter .'/filter.php'); - $text_filter = basename($textfilter).'_filter'; - if (class_exists($text_filter)) { - $obj = new $text_filter($courseid, $format, $options); - filter_base::addfilter($text_filter, $obj); - $hashstr .= $obj->hash(); - } - } - } + if ($options->filter) { + $filtermanager = filter_manager::instance(); + } else { + $filtermanager = new null_filter_manager(); } + $context = get_context_instance(CONTEXT_SYSTEM); // TODO change, once we have $PAGE->context. + if (!empty($CFG->cachetext) and empty($options->nocache)) { - $hashstr .= $text.'-'.(int)$courseid.'-'.current_language().'-'.(int)$format.(int)$options->trusttext.(int)$options->noclean.(int)$options->smiley.(int)$options->filter.(int)$options->para.(int)$options->newlines; - // for debug filtering system - // $hashstr .= time(); + $hashstr .= $text.'-'.$filtermanager->text_filtering_hash($context, $courseid).'-'.(int)$courseid.'-'.current_language().'-'. + (int)$format.(int)$options->trusttext.(int)$options->noclean.(int)$options->smiley. + (int)$options->filter.(int)$options->para.(int)$options->newlines; $time = time() - $CFG->cachetext; $md5key = md5($hashstr); @@ -1316,8 +1306,6 @@ function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL $text = trusttext_strip($text); } - $CFG->currenttextiscacheable = true; // Default status - can be changed by any filter - switch ($format) { case FORMAT_HTML: if ($options->smiley) { @@ -1326,9 +1314,7 @@ function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL if (!$options->noclean) { $text = clean_text($text, FORMAT_HTML); } - if ($options->filter) { - $text = filter_base::do_filter($text, $courseid); - } + $text = $filtermanager->filter_text($text, $context, $courseid); break; case FORMAT_PLAIN: @@ -1354,10 +1340,7 @@ function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL if (!$options->noclean) { $text = clean_text($text, FORMAT_HTML); } - - if ($options->filter) { - $text = filter_base::do_filter($text, $courseid); - } + $text = $filtermanager->filter_text($text, $context, $courseid); break; default: // FORMAT_MOODLE or anything else @@ -1365,14 +1348,20 @@ function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL if (!$options->noclean) { $text = clean_text($text, FORMAT_HTML); } - - if ($options->filter) { - $text = filter_base::do_filter($text, $courseid); - } + $text = $filtermanager->filter_text($text, $context, $courseid); break; } - if (empty($options->nocache) and !empty($CFG->cachetext) and $CFG->currenttextiscacheable) { + // Warn people that we have removed this old mechanism, just in case they + // were stupid enough to rely on it. + if (isset($CFG->currenttextiscacheable)) { + debugging('Once upon a time, Moodle had a truly evil use of global variables ' . + 'called $CFG->currenttextiscacheable. The good news is that this no ' . + 'longer exists. The bad news is that you seem to be using a filter that '. + 'relies on it. Please seek out and destroy that filter code.', DEBUG_DEVELOPER); + } + + if (empty($options->nocache) and !empty($CFG->cachetext)) { if (CLI_SCRIPT) { // special static cron cache - no need to store it in db if its not already there if (count($croncache) > 150) { @@ -1457,7 +1446,7 @@ function reset_text_filters_cache() { * @param int $courseid Current course as filters can, potentially, use it * @return string */ -function format_string ($string, $striplinks=true, $courseid=NULL ) { +function format_string($string, $striplinks=true, $courseid=NULL ) { global $CFG, $COURSE; @@ -1485,7 +1474,8 @@ function format_string ($string, $striplinks=true, $courseid=NULL ) { $string = preg_replace("/\&(?![a-zA-Z0-9#]{1,8};)/", "&", $string); if (!empty($CFG->filterall)) { - $string = filter_string($string, $courseid); + $context = get_context_instance(CONTEXT_SYSTEM); // TODO change, once we have $PAGE->context. + $string = filter_manager::instance()->filter_string($string, $context, $courseid); } // If the site requires it, strip ALL tags from this string @@ -1551,16 +1541,11 @@ function format_text_email($text, $format) { /** * Given some text in HTML format, this function will pass it - * through any filters that have been defined in $CFG->textfilterx - * The variable defines a filepath to a file containing the - * filter function. The file must contain a variable called - * $textfilter_function which contains the name of the function - * with $courseid and $text parameters + * through any filters that have been configured for this context. * * @param string $text The text to be passed through format filters - * @param int $courseid ? - * @return string - * @todo Finish documenting this function + * @param int $courseid The current course. + * @return string the filtered string. */ function filter_text($text, $courseid=NULL) { global $CFG, $COURSE; @@ -1569,85 +1554,9 @@ function filter_text($text, $courseid=NULL) { $courseid = $COURSE->id; // (copied from format_text) } - if (!empty($CFG->textfilters)) { - require_once($CFG->libdir.'/filterlib.php'); - $textfilters = explode(',', $CFG->textfilters); - foreach ($textfilters as $textfilter) { - if (is_readable($CFG->dirroot .'/'. $textfilter .'/filter.php')) { - include_once($CFG->dirroot .'/'. $textfilter .'/filter.php'); - $functionname = basename($textfilter).'_filter'; - if (function_exists($functionname)) { - $text = $functionname($courseid, $text); - } - } - } - } - - /// tags removed for XHTML compatibility - $text = str_replace('', '', $text); - $text = str_replace('', '', $text); - - return $text; -} - - -/** - * Given a string (short text) in HTML format, this function will pass it - * through any filters that have been defined in $CFG->stringfilters - * The variable defines a filepath to a file containing the - * filter function. The file must contain a variable called - * $textfilter_function which contains the name of the function - * with $courseid and $text parameters - * - * @param string $string The text to be passed through format filters - * @param int $courseid The id of a course - * @return string - */ -function filter_string($string, $courseid=NULL) { - global $CFG, $COURSE; - - if (empty($CFG->textfilters)) { // All filters are disabled anyway so quit - return $string; - } - - if (empty($courseid)) { - $courseid = $COURSE->id; - } - - require_once($CFG->libdir.'/filterlib.php'); - - if (isset($CFG->stringfilters)) { // We have a predefined list to use, great! - if (empty($CFG->stringfilters)) { // but it's blank, so finish now - return $string; - } - $stringfilters = explode(',', $CFG->stringfilters); // ..use the list we have - - } else { // Otherwise try to derive a list from textfilters - if (strpos($CFG->textfilters, 'filter/multilang') !== false) { // Multilang is here - $stringfilters = array('filter/multilang'); // Let's use just that - $CFG->stringfilters = 'filter/multilang'; // Save it for next time through - } else { - $CFG->stringfilters = ''; // Save the result and return - return $string; - } - } - + $context = get_context_instance(CONTEXT_SYSTEM); // TODO change, once we have $PAGE->context. - foreach ($stringfilters as $stringfilter) { - if (is_readable($CFG->dirroot .'/'. $stringfilter .'/filter.php')) { - include_once($CFG->dirroot .'/'. $stringfilter .'/filter.php'); - $functionname = basename($stringfilter).'_filter'; - if (function_exists($functionname)) { - $string = $functionname($courseid, $string); - } - } - } - - /// tags removed for XHTML compatibility - $string = str_replace('', '', $string); - $string = str_replace('', '', $string); - - return $string; + return filter_manager::instance()->filter_text($text, $context, $courseid); } /** @@ -1698,6 +1607,7 @@ function trusttext_mark($text) { return $text; } } + function trusttext_after_edit(&$text, $context) { if (has_capability('moodle/site:trustcontent', $context)) { $text = trusttext_strip($text); diff --git a/theme/standard/styles_layout.css b/theme/standard/styles_layout.css index b9f930e..2305627 100644 --- a/theme/standard/styles_layout.css +++ b/theme/standard/styles_layout.css @@ -1115,6 +1115,7 @@ body#admin-modules table.generaltable td.c0 #admin-qtypes #qtypes img.spacer { width: 16px; } +#filter-manage .buttons, #admin-roles-allow .buttons, #admin-roles-manage .buttons, #admin-roles-define .buttons, @@ -1183,6 +1184,7 @@ body#admin-modules table.generaltable td.c0 #admin-roles-define .mform { width: 100%; } +#filter-manage .backlink, #admin-roles-manage .backlink, #admin-roles-define .backlink, #admin-roles-explain .backlink, diff --git a/version.php b/version.php index 1995fe6..18608bc 100644 --- a/version.php +++ b/version.php @@ -6,7 +6,7 @@ // This is compared against the values stored in the database to determine // whether upgrades should be performed (see lib/db/*.php) - $version = 2009040100; // YYYYMMDD = date of the last version bump + $version = 2009040600; // YYYYMMDD = date of the last version bump // XX = daily increments $release = '2.0 dev (Build: 20090409)'; // Human-friendly version name